Skip to content

Commit

Permalink
Implement relationships in boolean exp, order_by and predicates (#254)
Browse files Browse the repository at this point in the history
V3_GIT_ORIGIN_REV_ID: bc0fb85552f141f7e887d61c15c5e455f87ac02a
  • Loading branch information
Naveenaidu authored and hasura-bot committed Jan 23, 2024
1 parent fed4371 commit d177c6f
Show file tree
Hide file tree
Showing 80 changed files with 7,472 additions and 609 deletions.
27 changes: 27 additions & 0 deletions v3/engine/src/execute/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use gql::{ast::common as ast, http::GraphQLError};
use lang_graphql as gql;
use open_dds::{
models::ModelName,
relationships::RelationshipName,
session_variables::SessionVariable,
types::{CustomTypeName, FieldName},
Expand All @@ -23,6 +24,32 @@ pub enum InternalDeveloperError {
field_name: ast::Name,
},

#[error("No source data connector specified for relationship argument field {argument_name} of type {type_name}")]
NoTargetSourceDataConnectorForRelationshipArgument {
argument_name: ast::Name,
type_name: Qualified<CustomTypeName>,
},

#[error("No source model specified for relationship argument field {argument_name} of type {type_name}")]
NoSourceModelForRelationshipArgument {
argument_name: ast::Name,
type_name: Qualified<CustomTypeName>,
},

#[error("No data connector specified for source model {source_model_name} which has a relationship argument field {argument_name} of type {type_name}")]
NoModelSourceDataConnectorForRelationshipArgument {
argument_name: ast::Name,
type_name: Qualified<CustomTypeName>,
source_model_name: Qualified<ModelName>,
},

#[error("No type mappings specified for source model {source_model_name} which has a relationship argument field {argument_name} of type {type_name}")]
NoModelSourceTypeMappingsForRelationshipArgument {
argument_name: ast::Name,
type_name: Qualified<CustomTypeName>,
source_model_name: Qualified<ModelName>,
},

#[error("No function/procedure specified for command field {field_name} of type {type_name}")]
NoFunctionOrProcedure {
type_name: ast::TypeName,
Expand Down
307 changes: 223 additions & 84 deletions v3/engine/src/execute/ir/filter.rs
Original file line number Diff line number Diff line change
@@ -1,116 +1,234 @@
use std::collections::BTreeMap;

use indexmap::IndexMap;
use lang_graphql::ast::common as ast;
use lang_graphql::normalized_ast;
use ndc_client as gdc;
use serde::Serialize;

use crate::execute::error;
use crate::schema::types;
use crate::execute::model_tracking::{count_model, UsagesCounts};
use crate::schema::types::output_type::relationship::FilterRelationshipAnnotation;
use crate::schema::types::{self};
use crate::schema::types::{InputAnnotation, ModelInputAnnotation};
use crate::schema::GDS;

use super::relationship::LocalModelRelationshipInfo;
use crate::execute::ir::selection_set::NDCRelationshipName;

#[derive(Debug, Serialize)]
pub(crate) struct ResolvedFilterExpression<'s> {
pub expressions: Vec<gdc::models::Expression>,
// relationships that were used in the filter expression. This is helpful
// for collecting relatinships and sending collection_relationships
pub relationships: BTreeMap<NDCRelationshipName, LocalModelRelationshipInfo<'s>>,
}

/// Generates the IR for GraphQL 'where' boolean expression
pub(crate) fn resolve_filter_expression(
fields: &IndexMap<ast::Name, normalized_ast::InputField<'_, GDS>>,
) -> Result<Vec<gdc::models::Expression>, error::Error> {
pub(crate) fn resolve_filter_expression<'s>(
fields: &IndexMap<ast::Name, normalized_ast::InputField<'s, GDS>>,
usage_counts: &mut UsagesCounts,
) -> Result<ResolvedFilterExpression<'s>, error::Error> {
let mut expressions = Vec::new();
for (_field_name, field) in fields {
match field.info.generic {
// "_and"
types::Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelFilterArgument {
field: types::ModelFilterArgument::AndOp,
},
)) => {
let values = field.value.as_list()?;
let expression = gdc::models::Expression::And {
expressions: values
.iter()
.map(|value| {
Ok(gdc::models::Expression::And {
expressions: resolve_filter_expression(value.as_object()?)?,
})
})
.collect::<Result<Vec<gdc::models::Expression>, error::Error>>()?,
};
expressions.push(expression);
let mut relationships = BTreeMap::new();
for field in fields.values() {
let relationship_paths = Vec::new();
let expression =
build_filter_expression(field, relationship_paths, &mut relationships, usage_counts)?;
expressions.extend(expression);
}
let resolved_filter_expression = ResolvedFilterExpression {
expressions,
relationships,
};
Ok(resolved_filter_expression)
}

// Build the NDC filter expression by traversing the relationships when present
pub(crate) fn build_filter_expression<'s>(
field: &normalized_ast::InputField<'s, GDS>,
// The path to access the relationship column. If the column is a
// non-relationship column, this will be empty. The paths contains the names
// of relationships (in order) that needs to be traversed to access the
// column.
mut relationship_paths: Vec<NDCRelationshipName>,
relationships: &mut BTreeMap<NDCRelationshipName, LocalModelRelationshipInfo<'s>>,
usage_counts: &mut UsagesCounts,
) -> Result<Vec<gdc::models::Expression>, error::Error> {
match field.info.generic {
// "_and"
types::Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelFilterArgument {
field: types::ModelFilterArgument::AndOp,
},
)) => {
let mut expressions = Vec::new();
let values = field.value.as_list()?;

for value in values.iter() {
let resolved_filter_expression =
resolve_filter_expression(value.as_object()?, usage_counts)?;
expressions.extend(resolved_filter_expression.expressions);
relationships.extend(resolved_filter_expression.relationships);
}
// "_or"
types::Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelFilterArgument {
field: types::ModelFilterArgument::OrOp,
},
)) => {
let values = field.value.as_list()?;
let expression = gdc::models::Expression::Or {
expressions: values
.iter()
.map(|value| {
Ok(gdc::models::Expression::And {
expressions: resolve_filter_expression(value.as_object()?)?,
})
})
.collect::<Result<Vec<gdc::models::Expression>, error::Error>>()?,

let expression = gdc::models::Expression::And { expressions };

Ok(vec![expression])
}
// "_or"
types::Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelFilterArgument {
field: types::ModelFilterArgument::OrOp,
},
)) => {
let mut expressions = Vec::new();
let values = field.value.as_list()?;

for value in values.iter() {
let resolved_filter_expression =
resolve_filter_expression(value.as_object()?, usage_counts)?;
expressions.extend(resolved_filter_expression.expressions);
relationships.extend(resolved_filter_expression.relationships);
}

let expression = gdc::models::Expression::Or { expressions };

Ok(vec![expression])
}
// "_not"
types::Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelFilterArgument {
field: types::ModelFilterArgument::NotOp,
},
)) => {
let mut expressions = Vec::new();
let value = field.value.as_object()?;

let resolved_filter_expression = resolve_filter_expression(value, usage_counts)?;
relationships.extend(resolved_filter_expression.relationships);

expressions.push(gdc::models::Expression::Not {
expression: Box::new(gdc::models::Expression::And {
expressions: resolved_filter_expression.expressions,
}),
});
Ok(expressions)
}
// The column that we want to use for filtering. If the column happens
// to be a relationship column, we'll have to join all the paths to
// specify NDC, what relationships needs to be traversed to access this
// column. The order decides how to access the column.
types::Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelFilterArgument {
field: types::ModelFilterArgument::Field { ndc_column: column },
},
)) => {
let mut expressions = Vec::new();
for (op_name, op_value) in field.value.as_object()? {
let expression = match op_name.as_str() {
"_eq" => build_binary_comparison_expression(
gdc::models::BinaryComparisonOperator::Equal,
column.clone(),
&op_value.value,
&relationship_paths,
),
"_is_null" => build_is_null_expression(
column.clone(),
&op_value.value,
&relationship_paths,
)?,
other => {
let operator = gdc::models::BinaryComparisonOperator::Other {
name: other.to_string(),
};
build_binary_comparison_expression(
operator,
column.clone(),
&op_value.value,
&relationship_paths,
)
}
};
expressions.push(expression);
expressions.push(expression)
}
// "_not"
types::Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelFilterArgument {
field: types::ModelFilterArgument::NotOp,
},
)) => {
let value = field.value.as_object()?;
expressions.push(gdc::models::Expression::Not {
expression: Box::new(gdc::models::Expression::And {
expressions: resolve_filter_expression(value)?,
Ok(expressions)
}
// Relationship field used for filtering.
// This relationship can either point to another relationship or a column.
types::Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelFilterArgument {
field:
types::ModelFilterArgument::RelationshipField(FilterRelationshipAnnotation {
relationship_name,
relationship_type,
source_type,
source_data_connector,
source_type_mappings,
target_source,
target_type,
target_model_name,
mappings,
}),
})
}
types::Annotation::Input(InputAnnotation::Model(
ModelInputAnnotation::ModelFilterArgument {
field: types::ModelFilterArgument::Field { ndc_column: column },
},
)) => {
// Add the target model being used in the usage counts
count_model(target_model_name.clone(), usage_counts);

let ndc_relationship_name = NDCRelationshipName::new(source_type, relationship_name)?;
relationships.insert(
ndc_relationship_name.clone(),
LocalModelRelationshipInfo {
relationship_name,
relationship_type,
source_type,
source_data_connector,
source_type_mappings,
target_source,
target_type,
mappings,
},
)) => {
for (op_name, op_value) in field.value.as_object()? {
let expression = match op_name.as_str() {
"_eq" => build_binary_comparison_expression(
gdc::models::BinaryComparisonOperator::Equal,
column.clone(),
&op_value.value,
),
"_is_null" => build_is_null_expression(column.clone(), &op_value.value)?,
other => {
let operator = gdc::models::BinaryComparisonOperator::Other {
name: other.to_string(),
};
build_binary_comparison_expression(
operator,
column.clone(),
&op_value.value,
)
}
};
expressions.push(expression)
}
);

let mut expressions = Vec::new();

// This map contains the relationships or the columns of the
// relationship that needs to be used for ordering.
let argument_value_map = field.value.as_object()?;
// Add the current relationship to the relationship paths.
relationship_paths.push(ndc_relationship_name);
// Keep track of relationship paths as we keep traversing down the
// relationships.
for argument in argument_value_map.values() {
let expression = build_filter_expression(
argument,
relationship_paths.clone(),
relationships,
usage_counts,
)?;
expressions.extend(expression);
}
annotation => Err(error::InternalEngineError::UnexpectedAnnotation {
annotation: annotation.clone(),
})?,
Ok(expressions)
}
annotation => Err(error::InternalEngineError::UnexpectedAnnotation {
annotation: annotation.clone(),
})?,
}
Ok(expressions)
}

/// Generate a binary comparison operator
fn build_binary_comparison_expression(
operator: gdc::models::BinaryComparisonOperator,
column: String,
value: &normalized_ast::Value<'_, GDS>,
relationship_paths: &Vec<NDCRelationshipName>,
) -> gdc::models::Expression {
let path_elements = build_path_elements(relationship_paths);

gdc::models::Expression::BinaryComparisonOperator {
column: gdc::models::ComparisonTarget::Column {
name: column,
path: vec![],
path: path_elements,
},
operator,
value: gdc::models::ComparisonValue::Scalar {
Expand All @@ -123,12 +241,15 @@ fn build_binary_comparison_expression(
fn build_is_null_expression(
column: String,
value: &normalized_ast::Value<'_, GDS>,
relationship_paths: &Vec<NDCRelationshipName>,
) -> Result<gdc::models::Expression, error::Error> {
let path_elements = build_path_elements(relationship_paths);

// Build an 'IsNull' unary comparison expression
let unary_comparison_expression = gdc::models::Expression::UnaryComparisonOperator {
column: gdc::models::ComparisonTarget::Column {
name: column,
path: vec![],
path: path_elements,
},
operator: gdc::models::UnaryComparisonOperator::IsNull,
};
Expand All @@ -144,3 +265,21 @@ fn build_is_null_expression(
})
}
}

pub fn build_path_elements(
relationship_paths: &Vec<NDCRelationshipName>,
) -> Vec<gdc::models::PathElement> {
let mut path_elements = Vec::new();
for path in relationship_paths {
path_elements.push(gdc::models::PathElement {
relationship: path.0.clone(),
arguments: BTreeMap::new(),
// 'AND' predicate indicates that the column can be accessed
// by joining all the relationships paths provided
predicate: Box::new(gdc::models::Expression::And {
expressions: Vec::new(),
}),
})
}
path_elements
}
Loading

0 comments on commit d177c6f

Please sign in to comment.