Skip to content

Commit 9841f43

Browse files
authored
Merge branch 'main' into paritosh/engine-1551
2 parents 272382b + 6b98c5d commit 9841f43

24 files changed

+1068
-113
lines changed

changelog.md

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88

99
### Fixed
1010

11+
- Native operations will now interpret missing arguments as null values for that argument, instead of causing an error.
12+
- Pre and post-check arguments in v2 mutations can now either be missing or null and both will be interpreted as an always true predicate. Previously a null value would have caused an error.
13+
- In v2 update mutations update columns explicitly set to null (as opposed to being missing or being set with their `_set` value object) are now correctly interpreted as "no update should be made to that column", instead of causing an error.
14+
1115
## [v2.0.0] - 2025-01-09
1216

1317
### Changed

crates/query-engine/translation/src/translation/mutation/v2/common.rs

+24-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
//! Some common helper functions.
22
3+
use crate::translation::error::Error;
34
use crate::translation::error::Warning;
45
use ndc_models as models;
56
use nonempty::NonEmpty;
67
use query_engine_metadata::metadata;
7-
use std::collections::BTreeSet;
8+
use std::collections::{BTreeMap, BTreeSet};
89

910
/// Create a description string for keys. For example:
1011
/// > "'TrackId' key", or
@@ -103,9 +104,28 @@ pub struct CheckArgument {
103104
pub description: String,
104105
}
105106

106-
// Default check argument/constraint
107-
pub fn default_constraint() -> serde_json::Value {
108-
serde_json::json!({"type": "and", "expressions": []})
107+
/// Gets a nullable predicate argument value from an arguments map.
108+
/// If the argument is missing or null, it is defaulted to an always true predicate.
109+
pub fn get_nullable_predicate_argument(
110+
argument_name: &models::ArgumentName,
111+
arguments: &BTreeMap<models::ArgumentName, serde_json::Value>,
112+
) -> Result<models::Expression, Error> {
113+
Ok(arguments
114+
.get(argument_name)
115+
.map(|pre_predicate_json| {
116+
serde_json::from_value::<Option<models::Expression>>(pre_predicate_json.clone())
117+
.map_err(|_| {
118+
Error::UnexpectedStructure(format!(
119+
"Argument '{}' should have an ndc-spec Expression structure",
120+
argument_name.clone()
121+
))
122+
})
123+
})
124+
.transpose()?
125+
.flatten()
126+
.unwrap_or_else(|| models::Expression::And {
127+
expressions: vec![],
128+
})) // Always true predicate
109129
}
110130

111131
// the old default was to prefix generated mutations with `v2_` or `v1_`

crates/query-engine/translation/src/translation/mutation/v2/delete.rs

+3-13
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use query_engine_metadata::metadata::database;
1111
use query_engine_sql::sql;
1212
use std::collections::BTreeMap;
1313

14-
use super::common::{self, default_constraint, CheckArgument};
14+
use super::common::{self, get_nullable_predicate_argument, CheckArgument};
1515

1616
/// A representation of an auto-generated delete mutation.
1717
///
@@ -140,18 +140,8 @@ pub fn translate(
140140
.collect::<Result<Vec<sql::ast::Expression>, Error>>()?;
141141

142142
// Build the `pre_check` argument boolean expression.
143-
let default_constraint = default_constraint();
144-
let predicate_json = arguments
145-
.get(&mutation.pre_check.argument_name)
146-
.unwrap_or(&default_constraint);
147-
148-
let predicate: models::Expression = serde_json::from_value(predicate_json.clone())
149-
.map_err(|_| {
150-
Error::UnexpectedStructure(format!(
151-
"Argument '{}' should have an ndc-spec Expression structure",
152-
mutation.pre_check.argument_name.clone()
153-
))
154-
})?;
143+
let predicate =
144+
get_nullable_predicate_argument(&mutation.pre_check.argument_name, arguments)?;
155145

156146
let predicate_expression = filtering::translate(
157147
env,

crates/query-engine/translation/src/translation/mutation/v2/insert.rs

+3-16
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use query_engine_metadata::metadata::database;
1111
use query_engine_sql::sql;
1212
use std::collections::{BTreeMap, BTreeSet};
1313

14-
use super::common::{self, default_constraint, CheckArgument};
14+
use super::common::{self, get_nullable_predicate_argument, CheckArgument};
1515

1616
/// A representation of an auto-generated insert mutation.
1717
///
@@ -214,9 +214,7 @@ pub fn translate(
214214
) -> Result<(sql::ast::Insert, sql::ast::ColumnAlias), Error> {
215215
let object = arguments
216216
.get(&mutation.objects_argument_name)
217-
.ok_or(Error::ArgumentNotFound(
218-
mutation.objects_argument_name.clone(),
219-
))?;
217+
.ok_or_else(|| Error::ArgumentNotFound(mutation.objects_argument_name.clone()))?;
220218

221219
let (columns, from) = translate_objects_to_columns_and_values(env, state, mutation, object)?;
222220

@@ -229,18 +227,7 @@ pub fn translate(
229227
};
230228

231229
// Build the `post_check` argument boolean expression.
232-
let default_constraint = default_constraint();
233-
let predicate_json = arguments
234-
.get(&mutation.post_check.argument_name)
235-
.unwrap_or(&default_constraint);
236-
237-
let predicate: models::Expression =
238-
serde_json::from_value(predicate_json.clone()).map_err(|_| {
239-
Error::UnexpectedStructure(format!(
240-
"Argument '{}' should have an ndc-spec Expression structure",
241-
mutation.post_check.argument_name.clone()
242-
))
243-
})?;
230+
let predicate = get_nullable_predicate_argument(&mutation.post_check.argument_name, arguments)?;
244231

245232
let predicate_expression = filtering::translate(
246233
env,

crates/query-engine/translation/src/translation/mutation/v2/update.rs

+20-39
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use query_engine_metadata::metadata::database;
1212
use query_engine_sql::sql;
1313
use std::collections::BTreeMap;
1414

15-
use super::common::{self, default_constraint, CheckArgument};
15+
use super::common::{self, get_nullable_predicate_argument, CheckArgument};
1616

1717
/// A representation of an auto-generated update mutation.
1818
///
@@ -109,9 +109,9 @@ pub fn translate(
109109
UpdateMutation::UpdateByKey(mutation) => {
110110
let object = arguments
111111
.get(&mutation.update_columns_argument_name)
112-
.ok_or(Error::ArgumentNotFound(
113-
mutation.update_columns_argument_name.clone(),
114-
))?;
112+
.ok_or_else(|| {
113+
Error::ArgumentNotFound(mutation.update_columns_argument_name.clone())
114+
})?;
115115

116116
let set = parse_update_columns(env, state, mutation, object)?;
117117

@@ -156,37 +156,16 @@ pub fn translate(
156156
current_table: table_name_and_reference,
157157
};
158158

159-
// Set default constrainst
160-
let default_constraint = default_constraint();
161-
162159
// Build the `pre_constraint` argument boolean expression.
163-
let pre_predicate_json = arguments
164-
.get(&mutation.pre_check.argument_name)
165-
.unwrap_or(&default_constraint);
166-
167-
let pre_predicate: models::Expression =
168-
serde_json::from_value(pre_predicate_json.clone()).map_err(|_| {
169-
Error::UnexpectedStructure(format!(
170-
"Argument '{}' should have an ndc-spec Expression structure",
171-
mutation.pre_check.argument_name.clone()
172-
))
173-
})?;
160+
let pre_predicate =
161+
get_nullable_predicate_argument(&mutation.pre_check.argument_name, arguments)?;
174162

175163
let pre_predicate_expression =
176164
filtering::translate(env, state, &root_and_current_tables, &pre_predicate)?;
177165

178166
// Build the `post_constraint` argument boolean expression.
179-
let post_predicate_json = arguments
180-
.get(&mutation.post_check.argument_name)
181-
.unwrap_or(&default_constraint);
182-
183-
let post_predicate: models::Expression =
184-
serde_json::from_value(post_predicate_json.clone()).map_err(|_| {
185-
Error::UnexpectedStructure(format!(
186-
"Argument '{}' should have an ndc-spec Expression structure",
187-
mutation.post_check.argument_name.clone()
188-
))
189-
})?;
167+
let post_predicate =
168+
get_nullable_predicate_argument(&mutation.post_check.argument_name, arguments)?;
190169

191170
let post_predicate_expression =
192171
filtering::translate(env, state, &root_and_current_tables, &post_predicate)?;
@@ -234,17 +213,18 @@ fn parse_update_columns(
234213
// For each field, look up the column name in the table
235214
// and update it and the value into the map.
236215
for (name, value) in object {
237-
let column_info = mutation.table_columns.get(name.as_str()).ok_or(
216+
let column_info = mutation.table_columns.get(name.as_str()).ok_or_else(|| {
238217
Error::ColumnNotFoundInCollection(
239218
name.clone().into(),
240219
mutation.collection_name.clone(),
241-
),
242-
)?;
220+
)
221+
})?;
243222

244-
columns_to_values.insert(
245-
sql::ast::ColumnName(column_info.name.clone()),
246-
parse_update_column(env, state, &name.as_str().into(), column_info, value)?,
247-
);
223+
if let Some(value) =
224+
parse_update_column(env, state, &name.as_str().into(), column_info, value)?
225+
{
226+
columns_to_values.insert(sql::ast::ColumnName(column_info.name.clone()), value);
227+
}
248228
}
249229
Ok(())
250230
}
@@ -275,7 +255,7 @@ fn parse_update_column(
275255
column_name: &models::FieldName,
276256
column_info: &metadata::database::ColumnInfo,
277257
object: &serde_json::Value,
278-
) -> Result<sql::ast::MutationValueExpression, Error> {
258+
) -> Result<Option<sql::ast::MutationValueExpression>, Error> {
279259
match object {
280260
serde_json::Value::Object(object) => {
281261
let vec = object.into_iter().collect::<Vec<_>>();
@@ -289,9 +269,9 @@ fn parse_update_column(
289269
}
290270
// _set operation.
291271
if *operation == "_set" {
292-
Ok(sql::ast::MutationValueExpression::Expression(
272+
Ok(Some(sql::ast::MutationValueExpression::Expression(
293273
values::translate(env, state, value, &column_info.r#type)?,
294-
))
274+
)))
295275
}
296276
// Operation is not supported.
297277
else {
@@ -304,6 +284,7 @@ fn parse_update_column(
304284
}
305285
}
306286
}
287+
serde_json::Value::Null => Ok(None),
307288
// Unexpected structures.
308289
serde_json::Value::Array(_) => Err(Error::UnexpectedStructure(format!(
309290
"array structure in update column '{column_name}' argument. Expecting an object.",

crates/query-engine/translation/src/translation/query/native_queries.rs

+36-18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! Handle native queries translation after building the query.
22
3+
use std::borrow::Cow;
4+
35
use ndc_models as models;
46
use ref_cast::RefCast;
57

@@ -43,33 +45,49 @@ pub fn translate(
4345
.map(|part| match part {
4446
metadata::NativeQueryPart::Text(text) => Ok(sql::ast::RawSql::RawText(text)),
4547
metadata::NativeQueryPart::Parameter(param) => {
46-
let typ = match native_query
48+
let (typ, nullable) = match native_query
4749
.info
4850
.arguments
4951
.get(models::ArgumentName::ref_cast(&param))
5052
{
5153
None => Err(Error::ArgumentNotFound(param.to_string().into())),
52-
Some(argument) => Ok(argument.r#type.clone()),
54+
Some(argument) => Ok((&argument.r#type, &argument.nullable)),
5355
}?;
54-
let exp = match native_query
56+
let argument = native_query
5557
.arguments
5658
.get(models::ArgumentName::ref_cast(&param))
57-
{
58-
None => Err(Error::ArgumentNotFound(param.to_string().into())),
59-
Some(argument) => match argument {
60-
models::Argument::Literal { value } => {
61-
values::translate(env, &mut translation_state, value, &typ)
62-
}
63-
models::Argument::Variable { name } => match &variables_table {
64-
Err(err) => Err(err.clone()),
65-
Ok(variables_table) => variables::translate(
66-
env,
67-
&mut translation_state,
68-
variables_table.clone(),
69-
name,
70-
&typ,
71-
),
59+
.map_or_else(
60+
|| {
61+
// If the argument is missing ...
62+
match nullable {
63+
// ... and the type is nullable, we treat this like a null value has been passed explicitly ...
64+
metadata::Nullable::Nullable => {
65+
Ok(Cow::Owned(models::Argument::Literal {
66+
value: serde_json::Value::Null,
67+
}))
68+
}
69+
// ... but if the type is non-nullable, we should have received a value and this is error
70+
metadata::Nullable::NonNullable => {
71+
Err(Error::ArgumentNotFound(param.to_string().into()))
72+
}
73+
}
7274
},
75+
|arg| Ok(Cow::Borrowed(arg)),
76+
)?;
77+
78+
let exp = match argument.as_ref() {
79+
models::Argument::Literal { value } => {
80+
values::translate(env, &mut translation_state, value, typ)
81+
}
82+
models::Argument::Variable { name } => match &variables_table {
83+
Err(err) => Err(err.clone()),
84+
Ok(variables_table) => variables::translate(
85+
env,
86+
&mut translation_state,
87+
variables_table.clone(),
88+
name,
89+
typ,
90+
),
7391
},
7492
}?;
7593
Ok(sql::ast::RawSql::Expression(exp))

crates/query-engine/translation/src/translation/query/sorting.rs

+6-11
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ struct Column(models::FieldName);
9797
/// An aggregate operation to select from a table used in an order by.
9898
#[derive(Debug)]
9999
enum Aggregate {
100-
CountStarAggregate,
101-
SingleColumnAggregate {
100+
CountStar,
101+
SingleColumn {
102102
column: models::FieldName,
103103
function: models::AggregateFunctionName,
104104
},
@@ -167,12 +167,7 @@ fn group_elements(elements: &[models::OrderByElement]) -> Vec<OrderByElementGrou
167167
),
168168
models::OrderByTarget::StarCountAggregate { path } => aggregate_element_groups.insert(
169169
hash_path(path),
170-
(
171-
i,
172-
path,
173-
element.order_direction,
174-
Aggregate::CountStarAggregate,
175-
),
170+
(i, path, element.order_direction, Aggregate::CountStar),
176171
),
177172
models::OrderByTarget::SingleColumnAggregate {
178173
path,
@@ -185,7 +180,7 @@ fn group_elements(elements: &[models::OrderByElement]) -> Vec<OrderByElementGrou
185180
i,
186181
path,
187182
element.order_direction,
188-
Aggregate::SingleColumnAggregate {
183+
Aggregate::SingleColumn {
189184
column: column.clone(),
190185
function: function.clone(),
191186
},
@@ -694,7 +689,7 @@ fn translate_targets(
694689
.iter()
695690
.map(|element| {
696691
match &element.element {
697-
Aggregate::CountStarAggregate => {
692+
Aggregate::CountStar => {
698693
let column_alias = sql::helpers::make_column_alias("count".to_string());
699694
Ok(OrderBySelectExpression {
700695
index: element.index,
@@ -706,7 +701,7 @@ fn translate_targets(
706701
aggregate: Some(sql::ast::Function::Unknown("COUNT".to_string())),
707702
})
708703
}
709-
Aggregate::SingleColumnAggregate { column, function } => {
704+
Aggregate::SingleColumn { column, function } => {
710705
let selected_column = target_collection.lookup_column(column)?;
711706
// we are going to deliberately use the table column name and not an alias we get from
712707
// the query request because this is internal to the sorting mechanism.

crates/query-engine/translation/tests/goldenfiles/mutations/v2_insert/request.json

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
},
1313
{
1414
"name": "The Other Band"
15+
},
16+
{
17+
"name": "The Null Band",
18+
"id": null
1519
}
1620
],
1721
"post_check": {

0 commit comments

Comments
 (0)