use fathomdb_engine::{EngineError, GroupedQueryRows, QueryRows};
use fathomdb_query::{
BuilderValidationError, CompileError, CompiledGroupedQuery, CompiledQuery,
CompiledRetrievalPlan, CompiledSearchPlan, CompiledVectorSearch, QueryAst, QueryBuilder,
QueryStep, SearchRows, TextQuery, compile_grouped_query, compile_search,
compile_search_plan_from_queries, compile_vector_search,
};
use crate::Engine;
fn validate_fusable_property_path(
engine: &Engine,
kind: &str,
path: &str,
method: &str,
) -> Result<(), BuilderValidationError> {
if kind.is_empty() {
return Err(BuilderValidationError::KindRequiredForFusion {
method: method.to_owned(),
});
}
let schema = engine.describe_fts_property_schema(kind).map_err(|_| {
BuilderValidationError::MissingPropertyFtsSchema {
kind: kind.to_owned(),
}
})?;
let schema = schema.ok_or_else(|| BuilderValidationError::MissingPropertyFtsSchema {
kind: kind.to_owned(),
})?;
if !schema.property_paths.iter().any(|p| p == path) {
return Err(BuilderValidationError::PathNotIndexed {
kind: kind.to_owned(),
path: path.to_owned(),
});
}
Ok(())
}
fn filter_builder_kind(builder: &QueryBuilder) -> Option<&str> {
for step in &builder.ast().steps {
if let QueryStep::Filter(fathomdb_query::Predicate::KindEq(kind)) = step {
return Some(kind.as_str());
}
}
None
}
#[must_use]
pub struct NodeQueryBuilder<'e> {
engine: &'e Engine,
inner: QueryBuilder,
}
impl<'e> NodeQueryBuilder<'e> {
pub(crate) fn new(engine: &'e Engine, kind: impl Into<String>) -> Self {
Self {
engine,
inner: QueryBuilder::nodes(kind),
}
}
pub fn search(self, query: impl Into<String>, limit: usize) -> SearchBuilder<'e> {
SearchBuilder::new(
self.engine,
self.inner.ast().root_kind.clone(),
query,
limit,
)
}
pub fn text_search(self, query: impl Into<String>, limit: usize) -> TextSearchBuilder<'e> {
TextSearchBuilder {
engine: self.engine,
inner: self.inner.text_search(query, limit),
attribution_requested: false,
}
}
pub fn vector_search(self, query: impl Into<String>, limit: usize) -> VectorSearchBuilder<'e> {
VectorSearchBuilder::new(
self.engine,
self.inner.ast().root_kind.clone(),
query,
limit,
)
}
pub fn traverse(
mut self,
direction: fathomdb_query::TraverseDirection,
label: impl Into<String>,
max_depth: usize,
) -> Self {
self.inner = self.inner.traverse(direction, label, max_depth);
self
}
pub fn filter_logical_id_eq(mut self, logical_id: impl Into<String>) -> Self {
self.inner = self.inner.filter_logical_id_eq(logical_id);
self
}
pub fn filter_kind_eq(mut self, kind: impl Into<String>) -> Self {
self.inner = self.inner.filter_kind_eq(kind);
self
}
pub fn filter_source_ref_eq(mut self, source_ref: impl Into<String>) -> Self {
self.inner = self.inner.filter_source_ref_eq(source_ref);
self
}
pub fn filter_content_ref_not_null(mut self) -> Self {
self.inner = self.inner.filter_content_ref_not_null();
self
}
pub fn filter_content_ref_eq(mut self, content_ref: impl Into<String>) -> Self {
self.inner = self.inner.filter_content_ref_eq(content_ref);
self
}
pub fn filter_json_text_eq(
mut self,
path: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.inner = self.inner.filter_json_text_eq(path, value);
self
}
pub fn filter_json_bool_eq(mut self, path: impl Into<String>, value: bool) -> Self {
self.inner = self.inner.filter_json_bool_eq(path, value);
self
}
pub fn filter_json_integer_gt(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_integer_gt(path, value);
self
}
pub fn filter_json_integer_gte(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_integer_gte(path, value);
self
}
pub fn filter_json_integer_lt(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_integer_lt(path, value);
self
}
pub fn filter_json_integer_lte(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_integer_lte(path, value);
self
}
pub fn filter_json_timestamp_gt(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_timestamp_gt(path, value);
self
}
pub fn filter_json_timestamp_gte(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_timestamp_gte(path, value);
self
}
pub fn filter_json_timestamp_lt(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_timestamp_lt(path, value);
self
}
pub fn filter_json_timestamp_lte(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_timestamp_lte(path, value);
self
}
pub fn filter_json_fused_text_eq(
mut self,
path: impl Into<String>,
value: impl Into<String>,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
let kind = self.inner.ast().root_kind.clone();
validate_fusable_property_path(self.engine, &kind, &path, "filter_json_fused_text_eq")?;
self.inner = self.inner.filter_json_fused_text_eq_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_text_in(
mut self,
path: impl Into<String>,
values: Vec<String>,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
let kind = self.inner.ast().root_kind.clone();
validate_fusable_property_path(self.engine, &kind, &path, "filter_json_fused_text_in")?;
self.inner = self.inner.filter_json_fused_text_in_unchecked(path, values);
Ok(self)
}
pub fn filter_json_text_in(mut self, path: impl Into<String>, values: Vec<String>) -> Self {
self.inner = self.inner.filter_json_text_in(path, values);
self
}
pub fn filter_json_fused_timestamp_gt(
mut self,
path: impl Into<String>,
value: i64,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
let kind = self.inner.ast().root_kind.clone();
validate_fusable_property_path(
self.engine,
&kind,
&path,
"filter_json_fused_timestamp_gt",
)?;
self.inner = self
.inner
.filter_json_fused_timestamp_gt_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_timestamp_gte(
mut self,
path: impl Into<String>,
value: i64,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
let kind = self.inner.ast().root_kind.clone();
validate_fusable_property_path(
self.engine,
&kind,
&path,
"filter_json_fused_timestamp_gte",
)?;
self.inner = self
.inner
.filter_json_fused_timestamp_gte_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_timestamp_lt(
mut self,
path: impl Into<String>,
value: i64,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
let kind = self.inner.ast().root_kind.clone();
validate_fusable_property_path(
self.engine,
&kind,
&path,
"filter_json_fused_timestamp_lt",
)?;
self.inner = self
.inner
.filter_json_fused_timestamp_lt_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_timestamp_lte(
mut self,
path: impl Into<String>,
value: i64,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
let kind = self.inner.ast().root_kind.clone();
validate_fusable_property_path(
self.engine,
&kind,
&path,
"filter_json_fused_timestamp_lte",
)?;
self.inner = self
.inner
.filter_json_fused_timestamp_lte_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_bool_eq(
mut self,
path: impl Into<String>,
value: bool,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
let kind = self.inner.ast().root_kind.clone();
validate_fusable_property_path(self.engine, &kind, &path, "filter_json_fused_bool_eq")?;
self.inner = self.inner.filter_json_fused_bool_eq_unchecked(path, value);
Ok(self)
}
pub fn expand(
mut self,
slot: impl Into<String>,
direction: fathomdb_query::TraverseDirection,
label: impl Into<String>,
max_depth: usize,
filter: Option<fathomdb_query::Predicate>,
edge_filter: Option<fathomdb_query::Predicate>,
) -> Self {
self.inner = self
.inner
.expand(slot, direction, label, max_depth, filter, edge_filter);
self
}
pub fn limit(mut self, limit: usize) -> Self {
self.inner = self.inner.limit(limit);
self
}
#[must_use]
pub fn as_builder(&self) -> &QueryBuilder {
&self.inner
}
#[must_use]
pub fn into_builder(self) -> QueryBuilder {
self.inner
}
#[must_use]
pub fn into_ast(self) -> fathomdb_query::QueryAst {
self.inner.into_ast()
}
pub fn compile(&self) -> Result<CompiledQuery, CompileError> {
self.inner.compile()
}
pub fn compile_grouped(&self) -> Result<CompiledGroupedQuery, CompileError> {
self.inner.compile_grouped()
}
pub fn execute(&self) -> Result<QueryRows, EngineError> {
let compiled = self
.inner
.compile()
.map_err(|e| EngineError::InvalidConfig(format!("query compilation failed: {e}")))?;
self.engine.coordinator().execute_compiled_read(&compiled)
}
pub fn execute_grouped(self) -> Result<GroupedQueryRows, EngineError> {
let compiled = self.inner.compile_grouped().map_err(|e| {
EngineError::InvalidConfig(format!("grouped query compilation failed: {e}"))
})?;
self.engine
.coordinator()
.execute_compiled_grouped_read(&compiled)
}
}
#[must_use]
pub struct TextSearchBuilder<'e> {
engine: &'e Engine,
inner: QueryBuilder,
attribution_requested: bool,
}
impl TextSearchBuilder<'_> {
pub fn with_match_attribution(mut self) -> Self {
self.attribution_requested = true;
self
}
pub fn filter_logical_id_eq(mut self, logical_id: impl Into<String>) -> Self {
self.inner = self.inner.filter_logical_id_eq(logical_id);
self
}
pub fn filter_kind_eq(mut self, kind: impl Into<String>) -> Self {
self.inner = self.inner.filter_kind_eq(kind);
self
}
pub fn filter_source_ref_eq(mut self, source_ref: impl Into<String>) -> Self {
self.inner = self.inner.filter_source_ref_eq(source_ref);
self
}
pub fn filter_content_ref_not_null(mut self) -> Self {
self.inner = self.inner.filter_content_ref_not_null();
self
}
pub fn filter_content_ref_eq(mut self, content_ref: impl Into<String>) -> Self {
self.inner = self.inner.filter_content_ref_eq(content_ref);
self
}
pub fn filter_json_text_eq(
mut self,
path: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.inner = self.inner.filter_json_text_eq(path, value);
self
}
pub fn filter_json_bool_eq(mut self, path: impl Into<String>, value: bool) -> Self {
self.inner = self.inner.filter_json_bool_eq(path, value);
self
}
pub fn filter_json_integer_gt(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_integer_gt(path, value);
self
}
pub fn filter_json_integer_gte(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_integer_gte(path, value);
self
}
pub fn filter_json_integer_lt(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_integer_lt(path, value);
self
}
pub fn filter_json_integer_lte(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_integer_lte(path, value);
self
}
pub fn filter_json_timestamp_gt(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_timestamp_gt(path, value);
self
}
pub fn filter_json_timestamp_gte(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_timestamp_gte(path, value);
self
}
pub fn filter_json_timestamp_lt(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_timestamp_lt(path, value);
self
}
pub fn filter_json_timestamp_lte(mut self, path: impl Into<String>, value: i64) -> Self {
self.inner = self.inner.filter_json_timestamp_lte(path, value);
self
}
pub fn filter_json_fused_text_eq(
mut self,
path: impl Into<String>,
value: impl Into<String>,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
let kind = self.inner.ast().root_kind.clone();
validate_fusable_property_path(self.engine, &kind, &path, "filter_json_fused_text_eq")?;
self.inner = self.inner.filter_json_fused_text_eq_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_text_in(
mut self,
path: impl Into<String>,
values: Vec<String>,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
let kind = self.inner.ast().root_kind.clone();
validate_fusable_property_path(self.engine, &kind, &path, "filter_json_fused_text_in")?;
self.inner = self.inner.filter_json_fused_text_in_unchecked(path, values);
Ok(self)
}
pub fn filter_json_text_in(mut self, path: impl Into<String>, values: Vec<String>) -> Self {
self.inner = self.inner.filter_json_text_in(path, values);
self
}
pub fn filter_json_fused_timestamp_gt(
mut self,
path: impl Into<String>,
value: i64,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
let kind = self.inner.ast().root_kind.clone();
validate_fusable_property_path(
self.engine,
&kind,
&path,
"filter_json_fused_timestamp_gt",
)?;
self.inner = self
.inner
.filter_json_fused_timestamp_gt_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_timestamp_gte(
mut self,
path: impl Into<String>,
value: i64,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
let kind = self.inner.ast().root_kind.clone();
validate_fusable_property_path(
self.engine,
&kind,
&path,
"filter_json_fused_timestamp_gte",
)?;
self.inner = self
.inner
.filter_json_fused_timestamp_gte_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_timestamp_lt(
mut self,
path: impl Into<String>,
value: i64,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
let kind = self.inner.ast().root_kind.clone();
validate_fusable_property_path(
self.engine,
&kind,
&path,
"filter_json_fused_timestamp_lt",
)?;
self.inner = self
.inner
.filter_json_fused_timestamp_lt_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_timestamp_lte(
mut self,
path: impl Into<String>,
value: i64,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
let kind = self.inner.ast().root_kind.clone();
validate_fusable_property_path(
self.engine,
&kind,
&path,
"filter_json_fused_timestamp_lte",
)?;
self.inner = self
.inner
.filter_json_fused_timestamp_lte_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_bool_eq(
mut self,
path: impl Into<String>,
value: bool,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
let kind = self.inner.ast().root_kind.clone();
validate_fusable_property_path(self.engine, &kind, &path, "filter_json_fused_bool_eq")?;
self.inner = self.inner.filter_json_fused_bool_eq_unchecked(path, value);
Ok(self)
}
pub fn limit(mut self, limit: usize) -> Self {
self.inner = self.inner.limit(limit);
self
}
pub fn traverse(
mut self,
direction: fathomdb_query::TraverseDirection,
label: impl Into<String>,
max_depth: usize,
) -> Self {
self.inner = self.inner.traverse(direction, label, max_depth);
self
}
pub fn expand(
mut self,
slot: impl Into<String>,
direction: fathomdb_query::TraverseDirection,
label: impl Into<String>,
max_depth: usize,
filter: Option<fathomdb_query::Predicate>,
edge_filter: Option<fathomdb_query::Predicate>,
) -> Self {
self.inner = self
.inner
.expand(slot, direction, label, max_depth, filter, edge_filter);
self
}
#[must_use]
pub fn as_builder(&self) -> &QueryBuilder {
&self.inner
}
pub fn compile(&self) -> Result<CompiledQuery, CompileError> {
self.inner.compile()
}
pub fn compile_grouped(&self) -> Result<CompiledGroupedQuery, CompileError> {
self.inner.compile_grouped()
}
#[must_use]
pub fn into_ast(self) -> fathomdb_query::QueryAst {
self.inner.into_ast()
}
pub fn execute(&self) -> Result<SearchRows, EngineError> {
let mut compiled = compile_search(self.inner.ast())
.map_err(|e| EngineError::InvalidConfig(format!("search compilation failed: {e}")))?;
compiled.attribution_requested = self.attribution_requested;
self.engine.coordinator().execute_compiled_search(&compiled)
}
}
#[must_use]
pub struct FallbackSearchBuilder<'e> {
engine: &'e Engine,
strict: TextQuery,
relaxed: Option<TextQuery>,
limit: usize,
attribution_requested: bool,
filter_builder: QueryBuilder,
}
impl<'e> FallbackSearchBuilder<'e> {
pub(crate) fn new(
engine: &'e Engine,
strict: impl Into<String>,
relaxed: Option<&str>,
limit: usize,
) -> Self {
let strict = TextQuery::parse(&strict.into());
let relaxed = relaxed.map(TextQuery::parse);
let filter_builder = QueryBuilder::nodes(String::new()).text_search("", 0);
Self {
engine,
strict,
relaxed,
limit,
attribution_requested: false,
filter_builder,
}
}
pub fn with_match_attribution(mut self) -> Self {
self.attribution_requested = true;
self
}
pub fn filter_logical_id_eq(mut self, logical_id: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_logical_id_eq(logical_id);
self
}
pub fn filter_kind_eq(mut self, kind: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_kind_eq(kind);
self
}
pub fn filter_source_ref_eq(mut self, source_ref: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_source_ref_eq(source_ref);
self
}
pub fn filter_content_ref_not_null(mut self) -> Self {
self.filter_builder = self.filter_builder.filter_content_ref_not_null();
self
}
pub fn filter_content_ref_eq(mut self, content_ref: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_content_ref_eq(content_ref);
self
}
pub fn filter_json_text_eq(
mut self,
path: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.filter_builder = self.filter_builder.filter_json_text_eq(path, value);
self
}
pub fn filter_json_bool_eq(mut self, path: impl Into<String>, value: bool) -> Self {
self.filter_builder = self.filter_builder.filter_json_bool_eq(path, value);
self
}
pub fn filter_json_integer_gt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_gt(path, value);
self
}
pub fn filter_json_integer_gte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_gte(path, value);
self
}
pub fn filter_json_integer_lt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_lt(path, value);
self
}
pub fn filter_json_integer_lte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_lte(path, value);
self
}
pub fn filter_json_timestamp_gt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_gt(path, value);
self
}
pub fn filter_json_timestamp_gte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_gte(path, value);
self
}
pub fn filter_json_timestamp_lt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_lt(path, value);
self
}
pub fn filter_json_timestamp_lte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_lte(path, value);
self
}
pub fn filter_json_fused_text_eq(
mut self,
path: impl Into<String>,
value: impl Into<String>,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
let kind = filter_builder_kind(&self.filter_builder)
.ok_or_else(|| BuilderValidationError::KindRequiredForFusion {
method: "filter_json_fused_text_eq".to_owned(),
})?
.to_owned();
validate_fusable_property_path(self.engine, &kind, &path, "filter_json_fused_text_eq")?;
self.filter_builder = self
.filter_builder
.filter_json_fused_text_eq_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_text_in(
mut self,
path: impl Into<String>,
values: Vec<String>,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
let kind = filter_builder_kind(&self.filter_builder)
.ok_or_else(|| BuilderValidationError::KindRequiredForFusion {
method: "filter_json_fused_text_in".to_owned(),
})?
.to_owned();
validate_fusable_property_path(self.engine, &kind, &path, "filter_json_fused_text_in")?;
self.filter_builder = self
.filter_builder
.filter_json_fused_text_in_unchecked(path, values);
Ok(self)
}
pub fn filter_json_text_in(mut self, path: impl Into<String>, values: Vec<String>) -> Self {
self.filter_builder = self.filter_builder.filter_json_text_in(path, values);
self
}
pub fn filter_json_fused_timestamp_gt(
mut self,
path: impl Into<String>,
value: i64,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
let kind = filter_builder_kind(&self.filter_builder)
.ok_or_else(|| BuilderValidationError::KindRequiredForFusion {
method: "filter_json_fused_timestamp_gt".to_owned(),
})?
.to_owned();
validate_fusable_property_path(
self.engine,
&kind,
&path,
"filter_json_fused_timestamp_gt",
)?;
self.filter_builder = self
.filter_builder
.filter_json_fused_timestamp_gt_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_timestamp_gte(
mut self,
path: impl Into<String>,
value: i64,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
let kind = filter_builder_kind(&self.filter_builder)
.ok_or_else(|| BuilderValidationError::KindRequiredForFusion {
method: "filter_json_fused_timestamp_gte".to_owned(),
})?
.to_owned();
validate_fusable_property_path(
self.engine,
&kind,
&path,
"filter_json_fused_timestamp_gte",
)?;
self.filter_builder = self
.filter_builder
.filter_json_fused_timestamp_gte_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_timestamp_lt(
mut self,
path: impl Into<String>,
value: i64,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
let kind = filter_builder_kind(&self.filter_builder)
.ok_or_else(|| BuilderValidationError::KindRequiredForFusion {
method: "filter_json_fused_timestamp_lt".to_owned(),
})?
.to_owned();
validate_fusable_property_path(
self.engine,
&kind,
&path,
"filter_json_fused_timestamp_lt",
)?;
self.filter_builder = self
.filter_builder
.filter_json_fused_timestamp_lt_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_timestamp_lte(
mut self,
path: impl Into<String>,
value: i64,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
let kind = filter_builder_kind(&self.filter_builder)
.ok_or_else(|| BuilderValidationError::KindRequiredForFusion {
method: "filter_json_fused_timestamp_lte".to_owned(),
})?
.to_owned();
validate_fusable_property_path(
self.engine,
&kind,
&path,
"filter_json_fused_timestamp_lte",
)?;
self.filter_builder = self
.filter_builder
.filter_json_fused_timestamp_lte_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_bool_eq(
mut self,
path: impl Into<String>,
value: bool,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
let kind = filter_builder_kind(&self.filter_builder)
.ok_or_else(|| BuilderValidationError::KindRequiredForFusion {
method: "filter_json_fused_bool_eq".to_owned(),
})?
.to_owned();
validate_fusable_property_path(self.engine, &kind, &path, "filter_json_fused_bool_eq")?;
self.filter_builder = self
.filter_builder
.filter_json_fused_bool_eq_unchecked(path, value);
Ok(self)
}
pub fn compile_plan(&self) -> Result<CompiledSearchPlan, CompileError> {
let mut ast = self.filter_builder.clone().into_ast();
ast.root_kind = String::new();
compile_search_plan_from_queries(
&ast,
self.strict.clone(),
self.relaxed.clone(),
self.limit,
self.attribution_requested,
)
}
pub fn execute(&self) -> Result<SearchRows, EngineError> {
let plan = self
.compile_plan()
.map_err(|e| EngineError::InvalidConfig(format!("search compilation failed: {e}")))?;
self.engine
.coordinator()
.execute_compiled_search_plan(&plan)
}
}
#[must_use]
pub struct VectorSearchBuilder<'e> {
engine: &'e Engine,
root_kind: String,
query: String,
limit: usize,
attribution_requested: bool,
filter_builder: QueryBuilder,
}
impl<'e> VectorSearchBuilder<'e> {
pub(crate) fn new(
engine: &'e Engine,
root_kind: impl Into<String>,
query: impl Into<String>,
limit: usize,
) -> Self {
let root_kind = root_kind.into();
let filter_builder = QueryBuilder::nodes(root_kind.clone()).vector_search("", 0);
Self {
engine,
root_kind,
query: query.into(),
limit,
attribution_requested: false,
filter_builder,
}
}
pub fn with_match_attribution(mut self) -> Self {
self.attribution_requested = true;
self
}
pub fn filter_logical_id_eq(mut self, logical_id: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_logical_id_eq(logical_id);
self
}
pub fn filter_kind_eq(mut self, kind: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_kind_eq(kind);
self
}
pub fn filter_source_ref_eq(mut self, source_ref: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_source_ref_eq(source_ref);
self
}
pub fn filter_content_ref_not_null(mut self) -> Self {
self.filter_builder = self.filter_builder.filter_content_ref_not_null();
self
}
pub fn filter_content_ref_eq(mut self, content_ref: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_content_ref_eq(content_ref);
self
}
pub fn filter_json_text_eq(
mut self,
path: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.filter_builder = self.filter_builder.filter_json_text_eq(path, value);
self
}
pub fn filter_json_bool_eq(mut self, path: impl Into<String>, value: bool) -> Self {
self.filter_builder = self.filter_builder.filter_json_bool_eq(path, value);
self
}
pub fn filter_json_integer_gt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_gt(path, value);
self
}
pub fn filter_json_integer_gte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_gte(path, value);
self
}
pub fn filter_json_integer_lt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_lt(path, value);
self
}
pub fn filter_json_integer_lte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_lte(path, value);
self
}
pub fn filter_json_timestamp_gt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_gt(path, value);
self
}
pub fn filter_json_timestamp_gte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_gte(path, value);
self
}
pub fn filter_json_timestamp_lt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_lt(path, value);
self
}
pub fn filter_json_timestamp_lte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_lte(path, value);
self
}
pub fn filter_json_fused_text_eq(
mut self,
path: impl Into<String>,
value: impl Into<String>,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
validate_fusable_property_path(
self.engine,
&self.root_kind,
&path,
"filter_json_fused_text_eq",
)?;
self.filter_builder = self
.filter_builder
.filter_json_fused_text_eq_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_text_in(
mut self,
path: impl Into<String>,
values: Vec<String>,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
validate_fusable_property_path(
self.engine,
&self.root_kind,
&path,
"filter_json_fused_text_in",
)?;
self.filter_builder = self
.filter_builder
.filter_json_fused_text_in_unchecked(path, values);
Ok(self)
}
pub fn filter_json_text_in(mut self, path: impl Into<String>, values: Vec<String>) -> Self {
self.filter_builder = self.filter_builder.filter_json_text_in(path, values);
self
}
pub fn filter_json_fused_timestamp_gt(
mut self,
path: impl Into<String>,
value: i64,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
validate_fusable_property_path(
self.engine,
&self.root_kind,
&path,
"filter_json_fused_timestamp_gt",
)?;
self.filter_builder = self
.filter_builder
.filter_json_fused_timestamp_gt_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_timestamp_gte(
mut self,
path: impl Into<String>,
value: i64,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
validate_fusable_property_path(
self.engine,
&self.root_kind,
&path,
"filter_json_fused_timestamp_gte",
)?;
self.filter_builder = self
.filter_builder
.filter_json_fused_timestamp_gte_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_timestamp_lt(
mut self,
path: impl Into<String>,
value: i64,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
validate_fusable_property_path(
self.engine,
&self.root_kind,
&path,
"filter_json_fused_timestamp_lt",
)?;
self.filter_builder = self
.filter_builder
.filter_json_fused_timestamp_lt_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_timestamp_lte(
mut self,
path: impl Into<String>,
value: i64,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
validate_fusable_property_path(
self.engine,
&self.root_kind,
&path,
"filter_json_fused_timestamp_lte",
)?;
self.filter_builder = self
.filter_builder
.filter_json_fused_timestamp_lte_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_bool_eq(
mut self,
path: impl Into<String>,
value: bool,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
validate_fusable_property_path(
self.engine,
&self.root_kind,
&path,
"filter_json_fused_bool_eq",
)?;
self.filter_builder = self
.filter_builder
.filter_json_fused_bool_eq_unchecked(path, value);
Ok(self)
}
pub fn compile_plan(&self) -> Result<CompiledVectorSearch, CompileError> {
let mut ast = self.filter_builder.clone().into_ast();
ast.root_kind.clone_from(&self.root_kind);
let mut compiled = compile_vector_search(&ast)?;
compiled.query_text.clone_from(&self.query);
compiled.limit = self.limit;
compiled.attribution_requested = self.attribution_requested;
Ok(compiled)
}
pub fn execute(&self) -> Result<SearchRows, EngineError> {
let plan = self
.compile_plan()
.map_err(|e| EngineError::InvalidConfig(format!("search compilation failed: {e}")))?;
self.engine
.coordinator()
.execute_compiled_vector_search(&plan)
}
}
#[must_use]
pub struct SearchBuilder<'e> {
engine: &'e Engine,
root_kind: String,
query: String,
limit: usize,
attribution_requested: bool,
filter_builder: QueryBuilder,
}
impl<'e> SearchBuilder<'e> {
pub(crate) fn new(
engine: &'e Engine,
root_kind: impl Into<String>,
query: impl Into<String>,
limit: usize,
) -> Self {
let root_kind = root_kind.into();
let filter_builder = QueryBuilder::nodes(root_kind.clone()).text_search("", 0);
Self {
engine,
root_kind,
query: query.into(),
limit,
attribution_requested: false,
filter_builder,
}
}
pub fn with_match_attribution(mut self) -> Self {
self.attribution_requested = true;
self
}
pub fn filter_logical_id_eq(mut self, logical_id: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_logical_id_eq(logical_id);
self
}
pub fn filter_kind_eq(mut self, kind: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_kind_eq(kind);
self
}
pub fn filter_source_ref_eq(mut self, source_ref: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_source_ref_eq(source_ref);
self
}
pub fn filter_content_ref_not_null(mut self) -> Self {
self.filter_builder = self.filter_builder.filter_content_ref_not_null();
self
}
pub fn filter_content_ref_eq(mut self, content_ref: impl Into<String>) -> Self {
self.filter_builder = self.filter_builder.filter_content_ref_eq(content_ref);
self
}
pub fn filter_json_text_eq(
mut self,
path: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.filter_builder = self.filter_builder.filter_json_text_eq(path, value);
self
}
pub fn filter_json_bool_eq(mut self, path: impl Into<String>, value: bool) -> Self {
self.filter_builder = self.filter_builder.filter_json_bool_eq(path, value);
self
}
pub fn filter_json_integer_gt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_gt(path, value);
self
}
pub fn filter_json_integer_gte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_gte(path, value);
self
}
pub fn filter_json_integer_lt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_lt(path, value);
self
}
pub fn filter_json_integer_lte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_integer_lte(path, value);
self
}
pub fn filter_json_timestamp_gt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_gt(path, value);
self
}
pub fn filter_json_timestamp_gte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_gte(path, value);
self
}
pub fn filter_json_timestamp_lt(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_lt(path, value);
self
}
pub fn filter_json_timestamp_lte(mut self, path: impl Into<String>, value: i64) -> Self {
self.filter_builder = self.filter_builder.filter_json_timestamp_lte(path, value);
self
}
pub fn filter_json_fused_text_eq(
mut self,
path: impl Into<String>,
value: impl Into<String>,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
validate_fusable_property_path(
self.engine,
&self.root_kind,
&path,
"filter_json_fused_text_eq",
)?;
self.filter_builder = self
.filter_builder
.filter_json_fused_text_eq_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_text_in(
mut self,
path: impl Into<String>,
values: Vec<String>,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
validate_fusable_property_path(
self.engine,
&self.root_kind,
&path,
"filter_json_fused_text_in",
)?;
self.filter_builder = self
.filter_builder
.filter_json_fused_text_in_unchecked(path, values);
Ok(self)
}
pub fn filter_json_text_in(mut self, path: impl Into<String>, values: Vec<String>) -> Self {
self.filter_builder = self.filter_builder.filter_json_text_in(path, values);
self
}
pub fn filter_json_fused_timestamp_gt(
mut self,
path: impl Into<String>,
value: i64,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
validate_fusable_property_path(
self.engine,
&self.root_kind,
&path,
"filter_json_fused_timestamp_gt",
)?;
self.filter_builder = self
.filter_builder
.filter_json_fused_timestamp_gt_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_timestamp_gte(
mut self,
path: impl Into<String>,
value: i64,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
validate_fusable_property_path(
self.engine,
&self.root_kind,
&path,
"filter_json_fused_timestamp_gte",
)?;
self.filter_builder = self
.filter_builder
.filter_json_fused_timestamp_gte_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_timestamp_lt(
mut self,
path: impl Into<String>,
value: i64,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
validate_fusable_property_path(
self.engine,
&self.root_kind,
&path,
"filter_json_fused_timestamp_lt",
)?;
self.filter_builder = self
.filter_builder
.filter_json_fused_timestamp_lt_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_timestamp_lte(
mut self,
path: impl Into<String>,
value: i64,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
validate_fusable_property_path(
self.engine,
&self.root_kind,
&path,
"filter_json_fused_timestamp_lte",
)?;
self.filter_builder = self
.filter_builder
.filter_json_fused_timestamp_lte_unchecked(path, value);
Ok(self)
}
pub fn filter_json_fused_bool_eq(
mut self,
path: impl Into<String>,
value: bool,
) -> Result<Self, BuilderValidationError> {
let path = path.into();
validate_fusable_property_path(
self.engine,
&self.root_kind,
&path,
"filter_json_fused_bool_eq",
)?;
self.filter_builder = self
.filter_builder
.filter_json_fused_bool_eq_unchecked(path, value);
Ok(self)
}
pub fn compile_plan(&self) -> Result<CompiledRetrievalPlan, CompileError> {
let mut ast: QueryAst = self.filter_builder.clone().into_ast();
ast.root_kind.clone_from(&self.root_kind);
let mut replaced = false;
for step in &mut ast.steps {
if let QueryStep::TextSearch {
query: TextQuery::Empty,
limit: 0,
} = step
{
*step = QueryStep::Search {
query: self.query.clone(),
limit: self.limit,
};
replaced = true;
break;
}
}
debug_assert!(
replaced,
"SearchBuilder filter accumulator must contain the seed TextSearch step"
);
let mut plan = fathomdb_query::compile_retrieval_plan(&ast)?;
plan.text.strict.attribution_requested = self.attribution_requested;
if let Some(relaxed) = plan.text.relaxed.as_mut() {
relaxed.attribution_requested = self.attribution_requested;
}
Ok(plan)
}
pub fn execute(&self) -> Result<SearchRows, EngineError> {
let plan = self
.compile_plan()
.map_err(|e| EngineError::InvalidConfig(format!("search compilation failed: {e}")))?;
self.engine
.coordinator()
.execute_retrieval_plan(&plan, &self.query)
}
pub fn expand(
mut self,
slot: impl Into<String>,
direction: fathomdb_query::TraverseDirection,
label: impl Into<String>,
max_depth: usize,
filter: Option<fathomdb_query::Predicate>,
edge_filter: Option<fathomdb_query::Predicate>,
) -> Self {
self.filter_builder =
self.filter_builder
.expand(slot, direction, label, max_depth, filter, edge_filter);
self
}
pub fn compile_grouped(&self) -> Result<CompiledGroupedQuery, CompileError> {
let mut ast: QueryAst = self.filter_builder.clone().into_ast();
ast.root_kind.clone_from(&self.root_kind);
let mut replaced = false;
for step in &mut ast.steps {
if let QueryStep::TextSearch {
query: TextQuery::Empty,
limit: 0,
} = step
{
*step = QueryStep::TextSearch {
query: TextQuery::parse(&self.query),
limit: self.limit,
};
replaced = true;
break;
}
}
debug_assert!(
replaced,
"SearchBuilder filter accumulator must contain the seed TextSearch step"
);
compile_grouped_query(&ast)
}
pub fn execute_grouped(self) -> Result<GroupedQueryRows, EngineError> {
let compiled = self.compile_grouped().map_err(|e| {
EngineError::InvalidConfig(format!("grouped query compilation failed: {e}"))
})?;
self.engine
.coordinator()
.execute_compiled_grouped_read(&compiled)
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::panic)]
mod tests {
use super::{FallbackSearchBuilder, VectorSearchBuilder};
use crate::{BuilderValidationError, Engine, EngineOptions};
use fathomdb_query::Predicate;
use tempfile::NamedTempFile;
fn open_engine_with_schema(register: bool) -> (NamedTempFile, Engine) {
let db = NamedTempFile::new().expect("temporary db");
let engine = Engine::open(EngineOptions::new(db.path())).expect("engine opens");
if register {
engine
.register_fts_property_schema(
"Note",
&["$.title".to_owned(), "$.body".to_owned()],
None,
)
.expect("register fts property schema");
}
(db, engine)
}
#[test]
fn node_query_fused_text_eq_requires_registered_schema() {
let (_db, engine) = open_engine_with_schema(false);
let result = engine
.query("Note")
.filter_json_fused_text_eq("$.title", "hello");
let Err(err) = result else {
panic!("must reject fused filter without schema");
};
assert!(
matches!(err, BuilderValidationError::MissingPropertyFtsSchema { ref kind } if kind == "Note"),
"expected MissingPropertyFtsSchema, got {err:?}"
);
}
#[test]
fn node_query_fused_text_eq_rejects_path_not_in_schema() {
let (_db, engine) = open_engine_with_schema(true);
let result = engine
.query("Note")
.filter_json_fused_text_eq("$.not_covered", "hello");
let Err(err) = result else {
panic!("path not in schema must be rejected");
};
assert!(
matches!(err, BuilderValidationError::PathNotIndexed { ref kind, ref path } if kind == "Note" && path == "$.not_covered"),
"expected PathNotIndexed, got {err:?}"
);
}
#[test]
fn node_query_fused_text_eq_succeeds_with_registered_schema() {
let (_db, engine) = open_engine_with_schema(true);
let builder = engine
.query("Note")
.filter_json_fused_text_eq("$.title", "hello")
.expect("fused filter with registered schema must succeed");
let compiled = builder.compile().expect("compile");
assert!(
compiled.sql.contains("json_extract(src.properties, ?"),
"fused filter must emit against src.properties, got {}",
compiled.sql
);
}
#[test]
fn text_search_fused_timestamp_gt_validates_and_compiles() {
let (_db, engine) = open_engine_with_schema(true);
engine
.register_fts_property_schema("Note2", &["$.written_at".to_owned()], None)
.expect("register Note2 schema");
let builder = engine
.query("Note2")
.text_search("budget", 5)
.filter_json_fused_timestamp_gt("$.written_at", 1_700_000_000)
.expect("fused timestamp gt must succeed with schema");
let _ = builder.compile().expect("compile succeeds");
}
#[test]
fn vector_search_fused_text_eq_validates() {
let (_db, engine) = open_engine_with_schema(true);
let result = VectorSearchBuilder::new(&engine, "NoSchema", "q", 5)
.filter_json_fused_text_eq("$.title", "hello");
let Err(err) = result else {
panic!("missing schema must error");
};
assert!(
matches!(err, BuilderValidationError::MissingPropertyFtsSchema { .. }),
"expected MissingPropertyFtsSchema, got {err:?}"
);
let ok = VectorSearchBuilder::new(&engine, "Note", "q", 5)
.filter_json_fused_text_eq("$.title", "hello");
assert!(ok.is_ok(), "registered kind must succeed");
}
#[test]
fn fallback_search_fused_text_eq_requires_kind_binding() {
let (_db, engine) = open_engine_with_schema(true);
let result = FallbackSearchBuilder::new(&engine, "budget", None, 10)
.filter_json_fused_text_eq("$.title", "hello");
let Err(err) = result else {
panic!("no kind binding must error");
};
assert!(
matches!(err, BuilderValidationError::KindRequiredForFusion { .. }),
"expected KindRequiredForFusion, got {err:?}"
);
let ok = FallbackSearchBuilder::new(&engine, "budget", None, 10)
.filter_kind_eq("Note")
.filter_json_fused_text_eq("$.title", "hello");
assert!(ok.is_ok(), "kind-bound fallback fused filter must succeed");
}
#[test]
fn unified_search_fused_text_eq_validates() {
let (_db, engine) = open_engine_with_schema(true);
let ok = engine
.query("Note")
.search("hello", 5)
.filter_json_fused_text_eq("$.title", "hello");
assert!(
ok.is_ok(),
"unified search builder must accept fused filter"
);
let result = engine
.query("Unknown")
.search("hello", 5)
.filter_json_fused_text_eq("$.title", "hello");
let Err(err) = result else {
panic!("missing schema must error");
};
assert!(matches!(
err,
BuilderValidationError::MissingPropertyFtsSchema { .. }
));
}
#[test]
fn existing_filter_json_text_eq_still_compiles_unchanged_regression() {
let (_db, engine) = open_engine_with_schema(false);
let compiled = engine
.query("Note")
.text_search("budget", 5)
.filter_json_text_eq("$.status", "active")
.compile()
.expect("compile");
assert!(
compiled
.sql
.contains("\n AND json_extract(n.properties, ?"),
"filter_json_text_eq must emit into outer WHERE, got {}",
compiled.sql
);
}
#[test]
fn fallback_builder_filter_kind_eq_fuses_without_explicit_text_search_step() {
let db = NamedTempFile::new().expect("temporary db");
let engine =
Engine::open(EngineOptions::new(db.path())).expect("engine opens for unit test");
let builder = FallbackSearchBuilder::new(&engine, "budget", Some("budget OR nothing"), 10)
.filter_kind_eq("Goal");
let plan = builder.compile_plan().expect("compile plan");
assert!(
plan.strict
.fusable_filters
.iter()
.any(|p| matches!(p, Predicate::KindEq(k) if k == "Goal")),
"KindEq(\"Goal\") must land in strict.fusable_filters (got {:?})",
plan.strict.fusable_filters
);
assert!(
plan.strict.residual_filters.is_empty(),
"strict.residual_filters should be empty for a single kind filter (got {:?})",
plan.strict.residual_filters
);
let relaxed = plan
.relaxed
.as_ref()
.expect("relaxed branch present when caller supplied a relaxed query");
assert!(
relaxed
.fusable_filters
.iter()
.any(|p| matches!(p, Predicate::KindEq(k) if k == "Goal")),
"KindEq(\"Goal\") must also land in relaxed.fusable_filters (got {:?})",
relaxed.fusable_filters
);
}
}