Skip to content

Commit 06821e5

Browse files
danieljharveyhasura-bot
authored andcommitted
Diff execution plans in tests (#1688)
<!-- The PR description should answer 2 important questions: --> ### What All the tests were passing, but since we're going to diff execution plans in prod, need to remove false positives, so this PR changes our engine tests to diff execution plans between old and new pipelines. We fix everything to make these pass. Next step is to actually run these in prod and emit traces for failures. V3_GIT_ORIGIN_REV_ID: 289e07016b6210d052736697ca3c71760ebade40
1 parent 61d45cd commit 06821e5

File tree

12 files changed

+280
-99
lines changed

12 files changed

+280
-99
lines changed

v3/Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

v3/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ serde_json = { version = "1", features = ["preserve_order"] }
138138
serde_path_to_error = "0.1"
139139
serde_with = { version = "3", features = ["indexmap_2"] }
140140
sha2 = "0.10"
141+
similar-asserts = "1.7"
141142
smol_str = "0.1"
142143
strum = "0.26"
143144
strum_macros = "0.26"

v3/crates/engine/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ graphql-ir = { path = "../graphql/ir" }
7070
criterion = { workspace = true }
7171
goldenfile = { workspace = true }
7272
pretty_assertions = { workspace = true }
73+
similar-asserts = { workspace = true }
7374
tokio-test = { workspace = true }
7475

7576
[package.metadata.cargo-machete]

v3/crates/engine/tests/common.rs

+74
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,15 @@ pub fn test_execution_expectation_for_multiple_ndc_versions(
402402
"OpenDD pipeline",
403403
);
404404

405+
// check execution plans are the same
406+
diff_execution_plan(
407+
&schema,
408+
&arc_resolved_metadata.clone(),
409+
session,
410+
&request_headers,
411+
&raw_request,
412+
);
413+
405414
responses.push(http_response);
406415
}
407416
}
@@ -479,6 +488,16 @@ pub fn test_execution_expectation_for_multiple_ndc_versions(
479488
&open_dd_response.inner(),
480489
"OpenDD pipeline",
481490
);
491+
492+
// check execution plans are the same
493+
diff_execution_plan(
494+
&schema,
495+
&arc_resolved_metadata.clone(),
496+
session,
497+
&request_headers,
498+
&raw_request,
499+
);
500+
482501
responses.push(http_response);
483502
}
484503
}
@@ -770,3 +789,58 @@ async fn run_query_graphql_ws(
770789
}
771790
response
772791
}
792+
793+
/// Compare executions plans. If they fail to build, ignore them.
794+
pub fn diff_execution_plan(
795+
schema: &'_ lang_graphql::schema::Schema<GDS>,
796+
metadata: &'_ Arc<metadata_resolve::Metadata>,
797+
session: &Session,
798+
request_headers: &reqwest::header::HeaderMap,
799+
raw_request: &lang_graphql::http::RawRequest,
800+
) {
801+
// parse the raw request into a GQL query
802+
if let Ok(query) = graphql_frontend::parse_query(&raw_request.query) {
803+
// normalize the parsed GQL query
804+
if let Ok(normalized_request) =
805+
graphql_frontend::normalize_request(schema, session, query, raw_request)
806+
{
807+
// generate IR
808+
if let Ok(old_ir) = graphql_frontend::build_ir(
809+
GraphqlRequestPipeline::Old,
810+
schema,
811+
metadata,
812+
session,
813+
request_headers,
814+
&normalized_request,
815+
) {
816+
// construct a plan to execute the request
817+
if let Ok(old_request_plan) = graphql_frontend::build_request_plan(
818+
&old_ir,
819+
metadata,
820+
session,
821+
request_headers,
822+
) {
823+
// generate IR
824+
if let Ok(new_ir) = graphql_frontend::build_ir(
825+
GraphqlRequestPipeline::OpenDd,
826+
schema,
827+
metadata,
828+
session,
829+
request_headers,
830+
&normalized_request,
831+
) {
832+
// construct a plan to execute the request
833+
if let Ok(new_request_plan) = graphql_frontend::build_request_plan(
834+
&new_ir,
835+
metadata,
836+
session,
837+
request_headers,
838+
) {
839+
similar_asserts::assert_eq!(old_request_plan, new_request_plan);
840+
}
841+
}
842+
}
843+
}
844+
}
845+
}
846+
}

v3/crates/graphql/ir/src/plan/filter.rs

+16-16
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,6 @@ pub(crate) fn plan_filter_expression(
3434
);
3535
}
3636

37-
if let Some(filter) = relationship_join_filter {
38-
expressions.push(
39-
plan_expression(filter, relationships, &mut remote_predicates, unique_number).map_err(
40-
|plan_error| {
41-
plan_error::Error::Internal(plan_error::InternalError::InternalGeneric {
42-
description: plan_error.to_string(),
43-
})
44-
},
45-
)?,
46-
);
47-
}
48-
4937
if let Some(filter) = &query_filter.additional_filter {
5038
expressions.push(
5139
plan_expression(filter, relationships, &mut remote_predicates, unique_number).map_err(
@@ -69,8 +57,20 @@ pub(crate) fn plan_filter_expression(
6957
expressions.push(planned_expression);
7058
}
7159

72-
Ok((
73-
ResolvedFilterExpression::mk_and(expressions).remove_always_true_expression(),
74-
remote_predicates,
75-
))
60+
if let Some(filter) = relationship_join_filter {
61+
expressions.push(
62+
plan_expression(filter, relationships, &mut remote_predicates, unique_number).map_err(
63+
|plan_error| {
64+
plan_error::Error::Internal(plan_error::InternalError::InternalGeneric {
65+
description: plan_error.to_string(),
66+
})
67+
},
68+
)?,
69+
);
70+
}
71+
72+
let resolved_filter_expression =
73+
ResolvedFilterExpression::mk_and(expressions).remove_always_true_expression();
74+
75+
Ok((resolved_filter_expression, remote_predicates))
7676
}

v3/crates/graphql/ir/src/plan/selection_set.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use plan_types::{
1717
SourceFieldAlias, TargetField,
1818
};
1919
use plan_types::{NdcFieldAlias, NdcRelationshipName, Relationship, UniqueNumber};
20-
use std::collections::{BTreeMap, HashMap};
20+
use std::collections::BTreeMap;
2121

2222
pub(crate) fn plan_nested_selection(
2323
nested_selection: &NestedSelection<'_>,
@@ -256,7 +256,7 @@ pub(crate) fn plan_selection_set(
256256
ir,
257257
relationship_info,
258258
} => {
259-
let mut join_mapping = HashMap::new();
259+
let mut join_mapping = BTreeMap::new();
260260
for ((src_field_alias, src_field), target_field) in &relationship_info.join_mapping
261261
{
262262
let ndc_field_alias = process_remote_relationship_field_mapping(
@@ -304,7 +304,7 @@ pub(crate) fn plan_selection_set(
304304
ir,
305305
relationship_info,
306306
} => {
307-
let mut join_mapping = HashMap::new();
307+
let mut join_mapping = BTreeMap::new();
308308

309309
for ((src_field_alias, src_field), target_field) in &relationship_info.join_mapping
310310
{

v3/crates/graphql/ir/src/relationship.rs

+6-3
Original file line numberDiff line numberDiff line change
@@ -851,9 +851,12 @@ pub fn build_remote_relationship<'s>(
851851
}
852852
}
853853

854-
remote_relationships_ir
855-
.filter_clause
856-
.relationship_join_filter = Some(Expression::mk_and(relationship_join_filter_expressions));
854+
if !relationship_join_filter_expressions.is_empty() {
855+
remote_relationships_ir
856+
.filter_clause
857+
.relationship_join_filter =
858+
Some(Expression::mk_and(relationship_join_filter_expressions));
859+
}
857860
remote_relationships_ir.variable_arguments = variable_arguments;
858861

859862
let rel_info = RemoteModelRelationshipInfo { join_mapping };

v3/crates/plan-types/src/execution_plan/filter.rs

+6-4
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,21 @@ impl ResolvedFilterExpression {
5454
if expressions.len() == 1 {
5555
expressions.into_iter().next().unwrap()
5656
}
57-
// If all subexpressions are also `and`, we can flatten into a single `and`
58-
// ie. and([and([x,y]), and([a,b])]) == and([x,y,a,b])
57+
// If any subexpressions are also `and`, we can flatten into a single `and`
58+
// ie. and([and([x,y]), and([a,b])]) == and([x,y,a,b]) or
59+
// and([a, and([b,c])]) == and([a,b,c])
5960
else if expressions
6061
.iter()
61-
.all(|expr| matches!(expr, ResolvedFilterExpression::And { .. }))
62+
.any(|expr| matches!(expr, ResolvedFilterExpression::And { .. }))
6263
{
6364
let subexprs = expressions
6465
.into_iter()
6566
.flat_map(|expr| match expr {
6667
ResolvedFilterExpression::And { expressions } => expressions,
67-
_ => vec![],
68+
other => vec![other],
6869
})
6970
.collect();
71+
7072
ResolvedFilterExpression::And {
7173
expressions: subexprs,
7274
}

v3/crates/plan-types/src/execution_plan/remote_joins.rs

+2-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ use open_dds::arguments::ArgumentName;
77
use open_dds::types::FieldName;
88
use serde::Serialize;
99
use std::collections::BTreeMap;
10-
use std::collections::HashMap;
1110
use std::sync::Arc;
1211

1312
/// This tree structure captures all the locations (in the selection set IR) where
@@ -115,15 +114,15 @@ pub struct RemoteJoin {
115114
/// NDC node to execute on a data connector
116115
pub target_ndc_execution: query::QueryExecutionPlan,
117116
/// Mapping of the fields in source to fields in target.
118-
/// The HashMap has the following info -
117+
/// The BTreeMap has the following info -
119118
/// - key: is the field name in the source
120119
/// - value->first item: is the alias we create for the
121120
/// source field. If the user did not request the join field in the
122121
/// selection set, we include the join mapping field and call it a phantom
123122
/// field.
124123
/// - value->second item: is the target NDC field. This could be a model
125124
/// field or an argument name.
126-
pub join_mapping: HashMap<SourceFieldName, (SourceFieldAlias, TargetField)>,
125+
pub join_mapping: BTreeMap<SourceFieldName, (SourceFieldAlias, TargetField)>,
127126
/// Represents how to process the join response.
128127
pub process_response_as: ProcessResponseAs,
129128
/// Represents the type of the remote join

v3/crates/plan-types/src/expression.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -82,19 +82,21 @@ impl<'s> Expression<'s> {
8282
if expressions.len() == 1 {
8383
expressions.into_iter().next().unwrap()
8484
}
85-
// If all subexpressions are also `and`, we can flatten into a single `and`
85+
// If any subexpressions are also `and`, we can flatten into a single `and`
8686
// ie. and([and([x,y]), and([a,b])]) == and([x,y,a,b])
87+
// ie. and([a, and([b,c])]) == and([a,b,c])
8788
else if expressions
8889
.iter()
89-
.all(|expr| matches!(expr, Expression::And { .. }))
90+
.any(|expr| matches!(expr, Expression::And { .. }))
9091
{
9192
let subexprs = expressions
9293
.into_iter()
9394
.flat_map(|expr| match expr {
9495
Expression::And { expressions } => expressions,
95-
_ => vec![],
96+
other => vec![other],
9697
})
9798
.collect();
99+
98100
Expression::And {
99101
expressions: subexprs,
100102
}

0 commit comments

Comments
 (0)