Filtering
The next step is to filter the rows based on the provided predicate expression:
let filtered: Vec<Row> = (match &query.predicate {
None => Ok(sorted),
Some(expr) => {
let mut filtered: Vec<Row> = vec![];
for item in sorted {
let root = match root {
Root::CurrentRow => &item,
Root::ExplicitRow(root) => root,
};
if eval_expression(
collection_relationships,
variables,
state,
expr,
root,
&item,
)? {
filtered.push(item);
}
}
Ok(filtered)
}
})?;
As we can see, the function delegates to the eval_expression
function in order to evaluate the predicate on each row.
Evaluating expressions
The eval_expression
function evaluates a predicate by pattern matching on the type of the expression expr
, and returns a boolean value indicating whether the current row matches the predicate:
fn eval_expression(
collection_relationships: &BTreeMap<models::RelationshipName, models::Relationship>,
variables: &BTreeMap<models::VariableName, serde_json::Value>,
state: &AppState,
expr: &models::Expression,
root: &Row,
item: &Row,
) -> Result<bool> {
Logical expressions
The first category of expression types are the logical expressions - and (conjunction), or (disjunction) and not (negation) - whose evaluators are straightforward:
- To evaluate a conjunction/disjunction of subexpressions, we evaluate all of the subexpressions to booleans, and find the conjunction/disjunction of those boolean values respectively.
- To evaluate the negation of a subexpression, we evaluate the subexpression to a boolean value, and negate the boolean.
match expr {
models::Expression::And { expressions } => {
for expr in expressions {
if !eval_expression(collection_relationships, variables, state, expr, root, item)? {
return Ok(false);
}
}
Ok(true)
}
models::Expression::Or { expressions } => {
for expr in expressions {
if eval_expression(collection_relationships, variables, state, expr, root, item)? {
return Ok(true);
}
}
Ok(false)
}
models::Expression::Not { expression } => {
let b = eval_expression(
collection_relationships,
variables,
state,
expression,
root,
item,
)?;
Ok(!b)
}
Unary Operators
The next category of expressions are the unary operators. The only unary operator is the IsNull
operator, which is evaluated by evaluating the operator's comparison target, and then comparing the result to null
:
models::Expression::UnaryComparisonOperator { column, operator } => match operator {
models::UnaryComparisonOperator::IsNull => {
let vals = eval_comparison_target(
collection_relationships,
variables,
state,
column,
root,
item,
)?;
Ok(vals.iter().any(serde_json::Value::is_null))
}
},
To evaluate the comparison target, we delegate to the eval_comparison_target
function, which pattern matches:
- A column is evaluated using the
eval_path
function, which we will cover when we talk about relationships. - A root collection column (that is, a column from the root collection, or collection used by the nearest enclosing
Query
) is evaluated usingeval_column
. You may have noticed the additional argument,root
, which has been passed down through every function call so far - this is to track the root collection for exactly this case.
fn eval_comparison_target(
collection_relationships: &BTreeMap<models::RelationshipName, models::Relationship>,
variables: &BTreeMap<models::VariableName, serde_json::Value>,
state: &AppState,
target: &models::ComparisonTarget,
root: &Row,
item: &Row,
) -> Result<Vec<serde_json::Value>> {
match target {
models::ComparisonTarget::Column {
name,
field_path,
path,
} => {
let rows = eval_path(collection_relationships, variables, state, path, item)?;
let mut values = vec![];
for row in &rows {
let value = eval_column_field_path(row, name, field_path, &BTreeMap::new())?;
values.push(value);
}
Ok(values)
}
models::ComparisonTarget::RootCollectionColumn { name, field_path } => {
let value = eval_column_field_path(root, name, field_path, &BTreeMap::new())?;
Ok(vec![value])
}
}
}
Binary Operators
The next category of expressions are the binary operators. Binary operators can be standard or custom.
The only standard binary operators are the equal
and in
operators.
equal
evaluated by evaluating its comparison target and comparison value, and comparing them for equality:
models::Expression::BinaryComparisonOperator {
column,
operator,
value,
} => match operator.as_str() {
"eq" => {
let left_vals = eval_comparison_target(
collection_relationships,
variables,
state,
column,
root,
item,
)?;
let right_vals = eval_comparison_value(
collection_relationships,
variables,
state,
value,
root,
item,
)?;
for left_val in &left_vals {
for right_val in &right_vals {
if left_val == right_val {
return Ok(true);
}
}
}
Ok(false)
}
The in
operator is evaluated by evaluating its comparison target, and all of its comparison values, and testing whether the evaluated target appears in the list of evaluated values:
"in" => {
let left_vals = eval_comparison_target(
collection_relationships,
variables,
state,
column,
root,
item,
)?;
let right_val_sets = eval_comparison_value(
collection_relationships,
variables,
state,
value,
root,
item,
)?;
for comparison_value in &right_val_sets {
let right_vals = comparison_value.as_array().ok_or((
StatusCode::BAD_REQUEST,
Json(models::ErrorResponse {
message: "expected array".into(),
details: serde_json::Value::Null,
}),
))?;
for left_val in &left_vals {
for right_val in right_vals {
if left_val == right_val {
return Ok(true);
}
}
}
}
Ok(false)
}
The reference implementation provides a single custom binary operator as an example, which is the like
operator on strings:
"like" => {
let column_vals = eval_comparison_target(
collection_relationships,
variables,
state,
column,
root,
item,
)?;
let regex_vals = eval_comparison_value(
collection_relationships,
variables,
state,
value,
root,
item,
)?;
for column_val in &column_vals {
for regex_val in ®ex_vals {
let column_str = column_val.as_str().ok_or((
StatusCode::BAD_REQUEST,
Json(models::ErrorResponse {
message: "column is not a string".into(),
details: serde_json::Value::Null,
}),
))?;
let regex_str = regex_val.as_str().ok_or((
StatusCode::BAD_REQUEST,
Json(models::ErrorResponse {
message: " ".into(),
details: serde_json::Value::Null,
}),
))?;
let regex = Regex::new(regex_str).map_err(|_| {
(
StatusCode::BAD_REQUEST,
Json(models::ErrorResponse {
message: "invalid regular expression".into(),
details: serde_json::Value::Null,
}),
)
})?;
if regex.is_match(column_str) {
return Ok(true);
}
}
}
Ok(false)
}
"in" => {
let left_vals = eval_comparison_target(
collection_relationships,
variables,
state,
column,
root,
item,
)?;
let right_val_sets = eval_comparison_value(
collection_relationships,
variables,
state,
value,
root,
item,
)?;
for comparison_value in &right_val_sets {
let right_vals = comparison_value.as_array().ok_or((
StatusCode::BAD_REQUEST,
Json(models::ErrorResponse {
message: "expected array".into(),
details: serde_json::Value::Null,
}),
))?;
for left_val in &left_vals {
for right_val in right_vals {
if left_val == right_val {
return Ok(true);
}
}
}
}
Ok(false)
}
_ => Err((
StatusCode::BAD_REQUEST,
Json(models::ErrorResponse {
message: " ".into(),
details: serde_json::Value::Null,
}),
)),
EXISTS
expressions
An EXISTS
expression is evaluated by recursively evaluating a Query
on a related collection, and testing to see whether the resulting RowSet
contains any rows:
models::Expression::Exists {
in_collection,
predicate,
} => {
let query = models::Query {
aggregates: None,
fields: Some(IndexMap::new()),
limit: None,
offset: None,
order_by: None,
predicate: predicate.clone().map(|e| *e),
};
let collection = eval_in_collection(
collection_relationships,
item,
variables,
state,
in_collection,
)?;
let row_set = execute_query(
collection_relationships,
variables,
state,
&query,
Root::ExplicitRow(root),
collection,
)?;
let rows: Vec<IndexMap<_, _>> = row_set.rows.ok_or((
StatusCode::INTERNAL_SERVER_ERROR,
Json(models::ErrorResponse {
message: " ".into(),
details: serde_json::Value::Null,
}),
))?;
Ok(!rows.is_empty())