diff --git a/crates/query-engine/translation/src/translation/mutation/delete.rs b/crates/query-engine/translation/src/translation/mutation/delete.rs index af35ca1ff..7df8c8c5f 100644 --- a/crates/query-engine/translation/src/translation/mutation/delete.rs +++ b/crates/query-engine/translation/src/translation/mutation/delete.rs @@ -7,8 +7,9 @@ use crate::translation::query::values::translate_json_value; use ndc_sdk::models; use query_engine_metadata::metadata; use query_engine_metadata::metadata::database; +use query_engine_sql::sql; use query_engine_sql::sql::ast; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, VecDeque}; /// A representation of an auto-generated delete mutation. /// @@ -84,31 +85,50 @@ pub fn translate_delete( by_column, .. } => { - let predicate_json = arguments - .get("%predicate") - .ok_or(Error::ArgumentNotFound("%predicate".to_string()))?; + // The root table we are going to be deleting from. + let table = ast::TableReference::DBTable { + schema: schema_name.clone(), + table: table_name.clone(), + }; + + let table_alias = state.make_table_alias(table_name.0.clone()); + + let table_name_and_reference = TableNameAndReference { + name: collection_name.clone(), + reference: ast::TableReference::AliasedTable(table_alias.clone()), + }; + let from = ast::From::Table { + reference: table, + alias: table_alias.clone(), + }; + + // Build the `UNIQUE_KEY = ` boolean expression. let unique_key = arguments .get(&by_column.name) .ok_or(Error::ArgumentNotFound(by_column.name.clone()))?; let key_value = translate_json_value(state, unique_key, &by_column.r#type).unwrap(); - let table = ast::TableReference::DBTable { - schema: schema_name.clone(), - table: table_name.clone(), + let unique_expression = ast::Expression::BinaryOperation { + left: Box::new(ast::Expression::ColumnReference( + ast::ColumnReference::TableColumn { + table: ast::TableReference::AliasedTable(table_alias), + name: ast::ColumnName(by_column.name.clone()), + }, + )), + right: Box::new(key_value), + operator: ast::BinaryOperator("=".to_string()), }; - let table_alias = state.make_table_alias(table_name.0.clone()); + // Build the `%predicate` argument boolean expression. + let predicate_json = arguments + .get("%predicate") + .ok_or(Error::ArgumentNotFound("%predicate".to_string()))?; let predicate: models::Expression = serde_json::from_value(predicate_json.clone()) .map_err(|_| Error::ArgumentNotFound("%predicate".to_string()))?; - let table_name_and_reference = TableNameAndReference { - name: collection_name.clone(), - reference: ast::TableReference::AliasedTable(table_alias.clone()), - }; - let (predicate_expression, joins) = filtering::translate_expression( env, state, @@ -119,30 +139,43 @@ pub fn translate_delete( &predicate, )?; - let where_expr = ast::Expression::BinaryOperation { - left: Box::new(ast::Expression::ColumnReference( - ast::ColumnReference::TableColumn { - table: ast::TableReference::AliasedTable(table_alias.clone()), - name: ast::ColumnName(by_column.name.clone()), - }, - )), - right: Box::new(key_value), - operator: ast::BinaryOperator("=".to_string()), - }; + // We build the where clause depending on whether joins are involved in the predicate or not. + let mut joins = VecDeque::from(joins); + let first = joins.pop_front(); - let from = ast::From::Table { - reference: table, - alias: table_alias, - }; - - let where_ = if joins.is_empty() { - ast::Expression::And { - left: Box::new(where_expr), + let where_ = match first { + // If no joins are involved, we just AND the unique expression and the predicate expression. + None => Ok(ast::Expression::And { + left: Box::new(unique_expression), right: Box::new(predicate_expression), + }), + // If joins are involved, we wrap them in an EXISTS expression over selecting the first + // table in the joins list, joining with the rest of them. + Some(first) => { + let mut select = match first { + ast::Join::LeftOuterJoinLateral(join) => *join.select, + ast::Join::InnerJoinLateral(join) => *join.select, + ast::Join::CrossJoinLateral(join) => *join.select, + ast::Join::CrossJoin(join) => *join.select, + }; + + select.joins = Vec::from(joins); + + // AND between the join key, the unique expression, and the predicate expression. + select.where_ = sql::ast::Where(sql::ast::Expression::And { + left: Box::new(select.where_.0), + right: Box::new(ast::Expression::And { + left: Box::new(unique_expression), + right: Box::new(predicate_expression), + }), + }); + + // Wrap in EXISTS. + Ok(sql::ast::Expression::Exists { + select: Box::new(select), + }) } - } else { - todo!() - }; + }?; Ok(ast::Delete { from, diff --git a/crates/query-engine/translation/src/translation/query/mod.rs b/crates/query-engine/translation/src/translation/query/mod.rs index 461c2ed7d..2701148c5 100644 --- a/crates/query-engine/translation/src/translation/query/mod.rs +++ b/crates/query-engine/translation/src/translation/query/mod.rs @@ -3,7 +3,7 @@ mod aggregates; pub mod filtering; pub mod native_queries; -mod relationships; +pub mod relationships; pub mod root; mod sorting; pub mod values; diff --git a/crates/tests/tests-common/goldenfiles/mutations/delete_invoice_line.json b/crates/tests/tests-common/goldenfiles/mutations/delete_invoice_line.json index 4cbb3f048..ee5162ac0 100644 --- a/crates/tests/tests-common/goldenfiles/mutations/delete_invoice_line.json +++ b/crates/tests/tests-common/goldenfiles/mutations/delete_invoice_line.json @@ -4,21 +4,30 @@ "type": "procedure", "name": "v1_delete_InvoiceLine_by_InvoiceLineId", "arguments": { - "InvoiceLineId": 91, + "InvoiceLineId": 90, "%predicate": { "type": "binary_comparison_operator", "column": { "type": "column", - "name": "InvoiceLineId", - "path": [] + "name": "TrackId", + "path": [ + { + "relationship": "InvoiceLineTrack", + "arguments": {}, + "predicate": { + "type": "and", + "expressions": [] + } + } + ] }, "operator": { "type": "other", - "name": "_gt" + "name": "_eq" }, "value": { "type": "scalar", - "value": 340 + "value": 512 } } }, @@ -34,5 +43,14 @@ } } ], - "collection_relationships": {} + "collection_relationships": { + "InvoiceLineTrack": { + "column_mapping": { + "TrackId": "TrackId" + }, + "relationship_type": "object", + "target_collection": "Track", + "arguments": {} + } + } }