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 scopes: Vec<&Row> = match root {
Root::PushCurrentRow(scopes) => {
let mut scopes = scopes.to_vec();
scopes.push(&item);
scopes
}
Root::Reset => vec![&item],
};
if eval_expression(
collection_relationships,
variables,
state,
expr,
&scopes,
&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,
scopes: &[&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,
scopes,
item,
)? {
return Ok(false);
}
}
Ok(true)
}
models::Expression::Or { expressions } => {
for expr in expressions {
if eval_expression(
collection_relationships,
variables,
state,
expr,
scopes,
item,
)? {
return Ok(true);
}
}
Ok(false)
}
models::Expression::Not { expression } => {
let b = eval_expression(
collection_relationships,
variables,
state,
expression,
scopes,
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,
item,
)?;
Ok(vals.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_column_field_path
function. - An aggregate is evaluated using
eval_path
(which we will talk more about when we get to relationships) andeval_aggregate
(which we will talk about when we get to aggregates).
fn eval_comparison_target(
collection_relationships: &BTreeMap<models::RelationshipName, models::Relationship>,
variables: &BTreeMap<models::VariableName, serde_json::Value>,
state: &AppState,
target: &models::ComparisonTarget,
item: &Row,
) -> Result<serde_json::Value> {
match target {
models::ComparisonTarget::Column {
name,
arguments,
field_path,
} => eval_column_field_path(variables, item, name, field_path.as_deref(), arguments),
models::ComparisonTarget::Aggregate { aggregate, path } => {
let rows: Vec<Row> = eval_path(
collection_relationships,
variables,
state,
path,
&[item.clone()],
)?;
eval_aggregate(variables, aggregate, &rows)
}
}
}
Binary Operators
The next category of expressions are the binary operators. Binary operators can be standard or custom.
Binary operators are evaluated by evaluating their comparison target and comparison value, and comparing them using a specific comparison operator:
models::Expression::BinaryComparisonOperator {
column,
operator,
value,
} => {
let left_val =
eval_comparison_target(collection_relationships, variables, state, column, item)?;
let right_vals = eval_comparison_value(
collection_relationships,
variables,
value,
state,
scopes,
item,
)?;
eval_comparison_operator(operator, &left_val, &right_vals)
}
The standard binary comparison operators are:
- The equality operator,
equal
, - The set membership operator,
in
, - Comparison operators
less_than
,less_than_or_equal
,greater_than
, andgreater_than_or_equal
, - String comparisons
contains
,icontains
,starts_with
,istarts_with
,ends_with
,iends_with
andlike
.
equal
is evaluated by evaluating its comparison target and comparison value, and comparing them for equality:
"eq" => {
for right_val in right_vals {
if left_val == right_val {
return Ok(true);
}
}
Ok(false)
}
The ordering comparisons (less_than
, less_than_or_equal
, greater_than
, and greater_than_or_equal
) depend on their type, so first we need to determine the type of the comparison target and dispatch on it to eval_partial_ord_comparison
to perform the actual comparisons:
"gt" | "lt" | "gte" | "lte" => {
if let Some(column_int) = left_val.as_i64() {
eval_partial_ord_comparison(operator, &column_int, right_vals, |right_val| {
right_val.as_i64().ok_or_else(|| {
(
StatusCode::BAD_REQUEST,
Json(models::ErrorResponse {
message: "value is not an integer".into(),
details: serde_json::Value::Null,
}),
)
})
})
} else if let Some(column_float) = left_val.as_f64() {
eval_partial_ord_comparison(operator, &column_float, right_vals, |right_val| {
right_val.as_f64().ok_or_else(|| {
(
StatusCode::BAD_REQUEST,
Json(models::ErrorResponse {
message: "value is not a float".into(),
details: serde_json::Value::Null,
}),
)
})
})
} else if let Some(column_string) = left_val.as_str() {
eval_partial_ord_comparison(operator, &column_string, right_vals, |right_val| {
right_val.as_str().ok_or_else(|| {
(
StatusCode::BAD_REQUEST,
Json(models::ErrorResponse {
message: "value is not a string".into(),
details: serde_json::Value::Null,
}),
)
})
})
} else {
Err((
StatusCode::BAD_REQUEST,
Json(models::ErrorResponse {
message: format!(
"column is does not support comparison operator {operator}"
),
details: serde_json::Value::Null,
}),
))
}
}
fn eval_partial_ord_comparison<'a, T, FConvert>(
operator: &ndc_models::ComparisonOperatorName,
left_value: &T,
right_values: &'a [serde_json::Value],
convert: FConvert,
) -> Result<bool>
where
T: PartialOrd,
FConvert: Fn(&'a serde_json::Value) -> Result<T>,
{
for right_val in right_values {
let right_val = convert(right_val)?;
let op = operator.as_str();
if op == "gt" && *left_value > right_val
|| op == "lt" && *left_value < right_val
|| op == "gte" && *left_value >= right_val
|| op == "lte" && *left_value <= 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" => {
for comparison_value in right_vals {
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 right_val in right_vals {
if left_val == right_val {
return Ok(true);
}
}
}
Ok(false)
}
String comparison operators are evaluated similarly:
"contains" | "icontains" | "starts_with" | "istarts_with" | "ends_with" | "iends_with" => {
if let Some(left_str) = left_val.as_str() {
for right_val in right_vals {
let right_str = right_val.as_str().ok_or_else(|| {
(
StatusCode::BAD_REQUEST,
Json(models::ErrorResponse {
message: "value is not a string".into(),
details: serde_json::Value::Null,
}),
)
})?;
let op = operator.as_str();
let left_str_lower = left_str.to_lowercase();
let right_str_lower = right_str.to_lowercase();
if op == "contains" && left_str.contains(right_str)
|| op == "icontains" && left_str_lower.contains(&right_str_lower)
|| op == "starts_with" && left_str.starts_with(right_str)
|| op == "istarts_with" && left_str_lower.starts_with(&right_str_lower)
|| op == "ends_with" && left_str.ends_with(right_str)
|| op == "iends_with" && left_str_lower.ends_with(&right_str_lower)
{
return Ok(true);
}
}
Ok(false)
} else {
Err((
StatusCode::BAD_REQUEST,
Json(models::ErrorResponse {
message: format!(
"comparison operator {operator} is only supported on strings"
),
details: serde_json::Value::Null,
}),
))
}
}
The reference implementation provides a single custom binary operator as an example, which is the like
operator on strings:
"like" => {
for regex_val in right_vals {
let column_str = left_val.as_str().ok_or((
StatusCode::BAD_REQUEST,
Json(models::ErrorResponse {
message: "regex 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: "regex is invalid".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)
}
Scalar Array Comparison Operators
The next category of expressions are the scalar array comparison operators. First we must evaluate the comparison target and then we can evaluate the array comparison itself.
models::Expression::ArrayComparison { column, comparison } => {
let left_val =
eval_comparison_target(collection_relationships, variables, state, column, item)?;
eval_array_comparison(
collection_relationships,
variables,
&left_val,
comparison,
state,
scopes,
item,
)
}
Evaluating the array comparison is done using eval_array_comparison
. In it, we can evaluate the two standard operators we have: contains
and is_empty
.
contains
simply evaluates the comparison value and then tests whether the array from the comparison target contains any of the comparison values.
models::ArrayComparison::Contains { value } => {
let right_vals = eval_comparison_value(
collection_relationships,
variables,
value,
state,
scopes,
item,
)?;
for right_val in right_vals {
if left_val_array.contains(&right_val) {
return Ok(true);
}
}
Ok(false)
}
is_empty
simply checks is the comparison target array is empty:
models::ArrayComparison::IsEmpty => Ok(left_val_array.is_empty()),
EXISTS
expressions
An EXISTS
expression is evaluated by recursively evaluating a Query
on another source of rows, 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),
groups: None,
};
let collection = eval_in_collection(
collection_relationships,
item,
variables,
state,
in_collection,
)?;
let row_set = execute_query(
collection_relationships,
variables,
state,
&query,
Root::PushCurrentRow(scopes),
collection,
)?;
let rows: Vec<IndexMap<_, _>> = row_set.rows.ok_or((
StatusCode::INTERNAL_SERVER_ERROR,
Json(models::ErrorResponse {
message: "expected 'rows'".into(),
details: serde_json::Value::Null,
}),
))?;
Ok(!rows.is_empty())
Note in particular, we push the current row onto the stack of scopes
before executing the inner query, so that references to columns in those scopes can be resolved correctly.
The source of the rows is defined by in_collection
, which we evaluate with eval_in_collection
in order to get the rows to evaluate the inner query against. There are four different sources of rows.
ExistsInCollection::Related
The first source of rows is a related collection. We first find the specified relationship, and then use eval_path_element
to get the rows across that relationship from the current row:
models::ExistsInCollection::Related {
field_path,
relationship,
arguments,
} => {
let relationship = collection_relationships.get(relationship).ok_or((
StatusCode::BAD_REQUEST,
Json(models::ErrorResponse {
message: "relationship is undefined".into(),
details: serde_json::Value::Null,
}),
))?;
let source = vec![item.clone()];
eval_path_element(
collection_relationships,
variables,
state,
relationship,
arguments,
&source,
field_path.as_deref(),
&None,
)
}
ExistsInCollection::Unrelated
The second source of rows is an unrelated collection. This simply returns all rows in that collection by using get_collection_by_name
:
models::ExistsInCollection::Unrelated {
collection,
arguments,
} => {
let arguments = arguments
.iter()
.map(|(k, v)| Ok((k.clone(), eval_relationship_argument(variables, item, v)?)))
.collect::<Result<BTreeMap<_, _>>>()?;
get_collection_by_name(collection, &arguments, state)
}
ExistsInCollection::NestedCollection
The third source of rows is a nested collection. This allows us to source our rows from a nested array of objects in a column on the current row. We do this using eval_column_field_path
.
ndc_models::ExistsInCollection::NestedCollection {
column_name,
field_path,
arguments,
} => {
let value =
eval_column_field_path(variables, item, column_name, Some(field_path), arguments)?;
serde_json::from_value(value).map_err(|_| {
(
StatusCode::BAD_REQUEST,
Json(models::ErrorResponse {
message: "nested collection must be an array of objects".into(),
details: serde_json::Value::Null,
}),
)
})
}
ExistsInCollection::NestedScalarCollection
The fourth source of rows is a nested scalar collection. This allows us to read a nested array of scalars from a column on the current row (using eval_column_field_path
) and create a virtual row for each element in the array, placing the array element into a __value
field on the row:
models::ExistsInCollection::NestedScalarCollection {
field_path,
column_name,
arguments,
} => {
let value =
eval_column_field_path(variables, item, column_name, Some(field_path), arguments)?;
let value_array = value.as_array().ok_or_else(|| {
(
StatusCode::BAD_REQUEST,
Json(models::ErrorResponse {
message: "nested scalar collection column value must be an array".into(),
details: serde_json::Value::Null,
}),
)
})?;
let wrapped_array_values = value_array
.iter()
.map(|v| BTreeMap::from([(models::FieldName::from("__value"), v.clone())]))
.collect();
Ok(wrapped_array_values)