diff --git a/crates/query-engine/sql/src/sql/ast.rs b/crates/query-engine/sql/src/sql/ast.rs index 369b90384..8f93511d7 100644 --- a/crates/query-engine/sql/src/sql/ast.rs +++ b/crates/query-engine/sql/src/sql/ast.rs @@ -81,6 +81,7 @@ pub enum Returning { pub enum SelectList { SelectList(Vec<(ColumnAlias, Expression)>), SelectStar, + SelectStarComposite(Expression), } /// A FROM clause diff --git a/crates/query-engine/sql/src/sql/convert.rs b/crates/query-engine/sql/src/sql/convert.rs index 62c4c685f..3e91f11f2 100644 --- a/crates/query-engine/sql/src/sql/convert.rs +++ b/crates/query-engine/sql/src/sql/convert.rs @@ -92,6 +92,11 @@ impl SelectList { SelectList::SelectStar => { sql.append_syntax("*"); } + SelectList::SelectStarComposite(expr) => { + sql.append_syntax("("); + expr.to_sql(sql); + sql.append_syntax(").*"); + } } } } diff --git a/crates/query-engine/sql/src/sql/helpers.rs b/crates/query-engine/sql/src/sql/helpers.rs index 8de5809b7..a808a6641 100644 --- a/crates/query-engine/sql/src/sql/helpers.rs +++ b/crates/query-engine/sql/src/sql/helpers.rs @@ -80,6 +80,20 @@ pub fn make_column_alias(name: String) -> ColumnAlias { // SELECTs // +/// Build a simple 'SELECT (exp).*' +pub fn select_composite(exp: Expression) -> Select { + Select { + with: empty_with(), + select_list: SelectList::SelectStarComposite(exp), + from: None, + joins: vec![], + where_: Where(empty_where()), + group_by: empty_group_by(), + order_by: empty_order_by(), + limit: empty_limit(), + } +} + /// Build a simple select with a select list and the rest are empty. pub fn simple_select(select_list: Vec<(ColumnAlias, Expression)>) -> Select { Select { @@ -471,6 +485,8 @@ pub fn select_mutation_rowset( final_select } +/// Turn all rows of a query result into a single json array of objects. +/// /// Wrap a query that returns multiple rows in the following format: /// /// ```sql @@ -507,6 +523,26 @@ pub fn select_rows_as_json( select } +/// Turn each row of a query result into a json object. +/// +/// ```sql +/// SELECT row_to_json() AS +/// FROM as +/// ``` +pub fn select_row_as_json( + row_select: Select, + column_alias: ColumnAlias, + table_alias: TableAlias, +) -> Select { + let expression = Expression::RowToJson(TableReference::AliasedTable(table_alias.clone())); + let mut select = simple_select(vec![(column_alias, expression)]); + select.from = Some(From::Select { + select: Box::new(row_select), + alias: table_alias, + }); + select +} + /// Wrap a query that returns multiple rows in the following format: /// /// ```sql diff --git a/crates/query-engine/sql/src/sql/rewrites/constant_folding.rs b/crates/query-engine/sql/src/sql/rewrites/constant_folding.rs index 7358aad58..ca86bad83 100644 --- a/crates/query-engine/sql/src/sql/rewrites/constant_folding.rs +++ b/crates/query-engine/sql/src/sql/rewrites/constant_folding.rs @@ -16,6 +16,9 @@ pub fn normalize_select(mut select: Select) -> Select { // select list select.select_list = match select.select_list { SelectList::SelectStar => SelectList::SelectStar, + SelectList::SelectStarComposite(exp) => { + SelectList::SelectStarComposite(normalize_expr(exp)) + } SelectList::SelectList(vec) => SelectList::SelectList( vec.into_iter() .map(|(alias, expr)| (alias, normalize_expr(expr))) diff --git a/crates/query-engine/translation/src/translation/error.rs b/crates/query-engine/translation/src/translation/error.rs index 6791c90dd..5e494011f 100644 --- a/crates/query-engine/translation/src/translation/error.rs +++ b/crates/query-engine/translation/src/translation/error.rs @@ -1,6 +1,6 @@ //! Errors for translation. -use query_engine_metadata::metadata::database; +use query_engine_metadata::metadata::{database, Type}; /// A type for translation errors. #[derive(Debug, Clone)] @@ -31,6 +31,10 @@ pub enum Error { NoProcedureResultFieldsRequested, UnexpectedStructure(String), InternalError(String), + NestedFieldNotOfCompositeType { + field_name: String, + actual_type: Type, + }, } /// Capabilities we don't currently support. @@ -132,6 +136,16 @@ impl std::fmt::Display for Error { Error::NonScalarTypeUsedInOperator { r#type } => { write!(f, "Non-scalar-type used in operator: {:?}", r#type) } + Error::NestedFieldNotOfCompositeType { + field_name, + actual_type, + } => { + write!( + f, + "Nested field '{}' not of composite type. Actual type: {:?}", + field_name, actual_type + ) + } } } } diff --git a/crates/query-engine/translation/src/translation/helpers.rs b/crates/query-engine/translation/src/translation/helpers.rs index 73784997d..bcd2cc5c7 100644 --- a/crates/query-engine/translation/src/translation/helpers.rs +++ b/crates/query-engine/translation/src/translation/helpers.rs @@ -88,6 +88,16 @@ pub enum CollectionInfo<'env> { }, } +#[derive(Debug)] +/// Metadata information about a specific collection. +pub enum CompositeTypeInfo<'env> { + CollectionInfo(CollectionInfo<'env>), + CompositeTypeInfo { + name: String, + info: metadata::CompositeType, + }, +} + impl<'request> Env<'request> { /// Create a new Env by supplying the metadata and relationships. pub fn new( @@ -105,6 +115,29 @@ impl<'request> Env<'request> { } /// Lookup a collection's information in the metadata. + + pub fn lookup_composite_type( + &self, + type_name: &'request str, + ) -> Result, Error> { + let it_is_a_collection = self.lookup_collection(type_name); + + match it_is_a_collection { + Ok(collection_info) => Ok(CompositeTypeInfo::CollectionInfo(collection_info)), + Err(Error::CollectionNotFound(_)) => { + let its_a_type = self.metadata.composite_types.0.get(type_name).map(|t| { + CompositeTypeInfo::CompositeTypeInfo { + name: t.name.clone(), + info: t.clone(), + } + }); + + its_a_type.ok_or(Error::CollectionNotFound(type_name.to_string())) + } + Err(err) => Err(err), + } + } + pub fn lookup_collection( &self, collection_name: &'request str, @@ -209,6 +242,28 @@ impl CollectionInfo<'_> { } } +impl CompositeTypeInfo<'_> { + /// Lookup a column in a collection. + pub fn lookup_column(&self, column_name: &str) -> Result { + match self { + CompositeTypeInfo::CollectionInfo(collection_info) => { + collection_info.lookup_column(column_name) + } + CompositeTypeInfo::CompositeTypeInfo { name, info } => info + .fields + .get(column_name) + .map(|field_info| ColumnInfo { + name: sql::ast::ColumnName(field_info.name.clone()), + r#type: field_info.r#type.clone(), + }) + .ok_or(Error::ColumnNotFoundInCollection( + column_name.to_string(), + name.clone(), + )), + } + } +} + impl Default for State { fn default() -> State { State { diff --git a/crates/query-engine/translation/src/translation/query/root.rs b/crates/query-engine/translation/src/translation/query/root.rs index 62b446cd4..9b0742982 100644 --- a/crates/query-engine/translation/src/translation/query/root.rs +++ b/crates/query-engine/translation/src/translation/query/root.rs @@ -12,8 +12,9 @@ use super::relationships; use super::sorting; use crate::translation::error::Error; use crate::translation::helpers::{ - CollectionInfo, Env, RootAndCurrentTables, State, TableNameAndReference, + CollectionInfo, ColumnInfo, Env, RootAndCurrentTables, State, TableNameAndReference, }; +use query_engine_metadata::metadata::Type; use query_engine_sql::sql; /// Translate aggregates query to sql ast. @@ -34,8 +35,15 @@ pub fn translate_aggregate_query( // create the select clause and the joins, order by, where clauses. // We don't add the limit afterwards. - let mut select = - translate_query_part(env, state, current_table, query, aggregate_columns, vec![])?; + let mut select = translate_query_part( + env, + state, + current_table, + query, + aggregate_columns, + vec![], + vec![], + )?; // we remove the order by part though because it is only relevant for group by clauses, // which we don't support at the moment. select.order_by = sql::helpers::empty_order_by(); @@ -53,44 +61,73 @@ pub enum ReturnsFields { NoFieldsWereRequested, } -/// Translate rows part of query to sql ast. -pub fn translate_rows_query( +/// This type collects the salient parts of joined-on subqueries that compute the result of a +/// nested field selection. +struct JoinNestedFieldInfo { + select: sql::ast::Select, + alias: sql::ast::TableAlias, +} + +/// Translate a list of nested field joins into lateral joins. +fn transalate_nested_field_joins(joins: Vec) -> Vec { + joins + .into_iter() + .map(|JoinNestedFieldInfo { select, alias }| { + sql::ast::Join::LeftOuterJoinLateral(sql::ast::LeftOuterJoinLateral { + select: Box::new(select), + alias, + }) + }) + .collect() +} + +/// Translate the field-selection of a query to SQL. +/// Because field selection may be nested this function is mutually recursive with +/// 'translate_nested_field'. +fn translate_fields( env: &Env, state: &mut State, + fields: IndexMap, current_table: &TableNameAndReference, - from_clause: &sql::ast::From, - query: &models::Query, -) -> Result<(ReturnsFields, sql::ast::Select), Error> { + join_nested_fields: &mut Vec, + join_relationship_fields: &mut Vec, +) -> Result, Error> { // find the table according to the metadata. - let collection_info = env.lookup_collection(¤t_table.name)?; - - // join aliases - let mut join_fields: Vec = vec![]; - - // translate fields to select list - let fields = query.fields.clone().unwrap_or_default(); + let type_info = env.lookup_composite_type(¤t_table.name)?; - // remember whether we fields were requested or not. - // The case were fields were not requested, and also no aggregates were requested, - // can be used for `__typename` queries. - let returns_fields = if IndexMap::is_empty(&fields) { - ReturnsFields::NoFieldsWereRequested - } else { - ReturnsFields::FieldsWereRequested - }; - - // translate fields to columns or relationships. let columns: Vec<(sql::ast::ColumnAlias, sql::ast::Expression)> = fields .into_iter() .map(|(alias, field)| match field { - models::Field::Column { column, .. } => { - let column_info = collection_info.lookup_column(&column)?; + models::Field::Column { + column, + fields: None, + } => { + let column_info = type_info.lookup_column(&column)?; Ok(sql::helpers::make_column( current_table.reference.clone(), column_info.name.clone(), sql::helpers::make_column_alias(alias), )) } + models::Field::Column { + column, + fields: Some(nested_field), + } => { + let column_info = type_info.lookup_column(&column)?; + let nested_column_reference = translate_nested_field( + env, + state, + current_table, + &column_info, + nested_field, + join_nested_fields, + join_relationship_fields, + )?; + Ok(( + sql::helpers::make_column_alias(alias), + sql::ast::Expression::ColumnReference(nested_column_reference), + )) + } models::Field::Relationship { query, relationship, @@ -102,7 +139,7 @@ pub fn translate_rows_query( table: sql::ast::TableReference::AliasedTable(table_alias.clone()), column: column_alias.clone(), }; - join_fields.push(relationships::JoinFieldInfo { + join_relationship_fields.push(relationships::JoinFieldInfo { table_alias, column_alias: column_alias.clone(), relationship_name: relationship, @@ -116,10 +153,182 @@ pub fn translate_rows_query( } }) .collect::, Error>>()?; + Ok(columns) +} + +/// Translate a nested field selection. +/// +/// Nested fields are different from relationships in that the value of a nested field is already +/// avaliable on the current table as a column of composite type. +/// +/// A nested field selection translates to SQL in the form of: +/// +/// SELECT +/// coalesce(json_agg(row_to_json("%6_rows")), '[]') AS "rows" +/// FROM +/// ( +/// SELECT +/// "%3_nested_fields_collect"."collected" AS "result" +/// FROM +/// AS "%0_" +/// LEFT OUTER JOIN LATERAL ( +/// SELECT +/// ("%0_"."").* +/// ) AS "%2_nested_field_bound" ON ('true') +/// LEFT OUTER JOIN LATERAL ( +/// SELECT +/// row_to_json("%4_nested_fields") AS "collected" +/// FROM +/// ( +/// SELECT +/// "%2_nested_field_bound"."" AS "" +/// ) AS "%4_nested_fields" +/// ) AS "%3_nested_fields_collect" ON ('true') +/// ) AS "%6_rows" +/// +/// # Arguments +/// +/// * `current_table` - the table reference the column lives on +/// * `current_column` - the column to extract nested fields from +fn translate_nested_field( + env: &Env, + state: &mut State, + current_table: &TableNameAndReference, + current_column: &ColumnInfo, + field: models::NestedField, + join_nested_fields: &mut Vec, + join_relationship_fields: &mut Vec, +) -> Result { + match field { + models::NestedField::Object(models::NestedObject { fields }) => { + // In order to bring the nested fields into scope for sub selections + // we need to unpack them as selected columns of a bound relation. + // + // This becomes the SQL + // ``` + // LEFT OUTER JOIN LATERAL ( + // SELECT + // ("%0_"."").* + // ) AS "%2_nested_field_bound" ON ('true') + // ``` + let nested_field_select = sql::helpers::select_composite( + sql::ast::Expression::ColumnReference(sql::ast::ColumnReference::AliasedColumn { + table: current_table.reference.clone(), + column: sql::ast::ColumnAlias { + name: current_column.name.0.clone(), + }, + }), + ); + let nested_field_select_alias = + state.make_table_alias("nested_field_bound".to_string()); + + // We add this unpacking select statement to the stack of joins so that it is in scope + // for field-selection translation at the next level of nesting. + join_nested_fields.push(JoinNestedFieldInfo { + select: nested_field_select, + alias: nested_field_select_alias.clone(), + }); + + // Now each field of the composite field is in scope, bound just like columns of a + // table collection at the top level. + // We are now ready to make the recursive call to translate the field selection one + // more level down.selection one more level down. + + let nested_field_type_name = match ¤t_column.r#type { + Type::CompositeType(type_name) => Ok(type_name.clone()), + t => Err(Error::NestedFieldNotOfCompositeType { + field_name: current_column.name.0.clone(), + actual_type: t.clone(), + }), + }?; + let nested_field_table_reference = TableNameAndReference { + name: nested_field_type_name, + reference: sql::ast::TableReference::AliasedTable(nested_field_select_alias), + }; + + let field_expressions = translate_fields( + env, + state, + fields, + &nested_field_table_reference, + join_nested_fields, + join_relationship_fields, + )?; + + // With the select-list of field expressions at this level of nesting in hand from the + // recursive call we can now collect these into a single json value for the composite + // field which we are currently processing. + + let fields_select = sql::helpers::simple_select(field_expressions); + let nested_field_table_collect_alias = + state.make_table_alias("nested_fields_collect".to_string()); + let nested_field_column_collect_alias = sql::ast::ColumnAlias { + name: "collected".to_string(), + }; + let nested_field_join = JoinNestedFieldInfo { + select: sql::helpers::select_row_as_json( + fields_select, + nested_field_column_collect_alias.clone(), + state.make_table_alias("nested_fields".to_string()), + ), + alias: nested_field_table_collect_alias.clone(), + }; + + join_nested_fields.push(nested_field_join); + Ok(sql::ast::ColumnReference::AliasedColumn { + table: sql::ast::TableReference::AliasedTable(nested_field_table_collect_alias), + column: nested_field_column_collect_alias, + }) + } + models::NestedField::Array(_) => todo!(), + } +} + +/// Translate rows part of query to sql ast. +pub fn translate_rows_query( + env: &Env, + state: &mut State, + current_table: &TableNameAndReference, + from_clause: &sql::ast::From, + query: &models::Query, +) -> Result<(ReturnsFields, sql::ast::Select), Error> { + // join aliases + let mut join_nested_fields: Vec = vec![]; + let mut join_relationship_fields: Vec = vec![]; + + // translate fields to select list + let fields = query.fields.clone().unwrap_or_default(); + + // remember whether we fields were requested or not. + // The case were fields were not requested, and also no aggregates were requested, + // can be used for `__typename` queries. + let returns_fields = if IndexMap::is_empty(&fields) { + ReturnsFields::NoFieldsWereRequested + } else { + ReturnsFields::FieldsWereRequested + }; + + // translate fields to columns or relationships. + let columns = translate_fields( + env, + state, + fields, + current_table, + &mut join_nested_fields, + &mut join_relationship_fields, + )?; // create the select clause and the joins, order by, where clauses. // We'll add the limit afterwards. - let mut select = translate_query_part(env, state, current_table, query, columns, join_fields)?; + let mut select = translate_query_part( + env, + state, + current_table, + query, + columns, + join_nested_fields, + join_relationship_fields, + )?; select.from = Some(from_clause.clone()); @@ -145,7 +354,8 @@ fn translate_query_part( current_table: &TableNameAndReference, query: &models::Query, columns: Vec<(sql::ast::ColumnAlias, sql::ast::Expression)>, - join_fields: Vec, + join_nested_fields: Vec, + join_relationship_fields: Vec, ) -> Result { let root_table = current_table.clone(); @@ -158,15 +368,25 @@ fn translate_query_part( // construct a simple select with the table name, alias, and selected columns. let mut select = sql::helpers::simple_select(columns); + let nested_field_joins = transalate_nested_field_joins(join_nested_fields); + + select.joins.extend(nested_field_joins); + // collect any joins for relationships - let mut relationship_joins = - relationships::translate_joins(env, state, &root_and_current_tables, join_fields)?; + let relationship_joins = relationships::translate_joins( + env, + state, + &root_and_current_tables, + join_relationship_fields, + )?; + + select.joins.extend(relationship_joins); // translate order_by let (order_by, order_by_joins) = sorting::translate_order_by(env, state, &root_and_current_tables, &query.order_by)?; - relationship_joins.extend(order_by_joins); + select.joins.extend(order_by_joins); // translate where let (filter, filter_joins) = match &query.predicate { @@ -178,9 +398,7 @@ fn translate_query_part( select.where_ = sql::ast::Where(filter); - relationship_joins.extend(filter_joins); - - select.joins = relationship_joins; + select.joins.extend(filter_joins); select.order_by = order_by; diff --git a/crates/query-engine/translation/tests/goldenfiles/select_nested_column_simple/request.json b/crates/query-engine/translation/tests/goldenfiles/select_nested_column_simple/request.json new file mode 100644 index 000000000..8d7fa2869 --- /dev/null +++ b/crates/query-engine/translation/tests/goldenfiles/select_nested_column_simple/request.json @@ -0,0 +1,31 @@ +{ + "collection": "address_identity_function", + "query": { + "fields": { + "result": { + "type": "column", + "column": "result", + "fields": { + "type": "object", + "fields": { + "the_first_line_of_the_address": { + "type": "column", + "column": "address_line_1" + } + } + }, + "arguments": {} + } + } + }, + "arguments": { + "address": { + "type": "literal", + "value": { + "address_line_1": "Somstreet 159", + "address_line_2": "Second door to the right" + } + } + }, + "collection_relationships": {} +} diff --git a/crates/query-engine/translation/tests/goldenfiles/select_nested_column_simple/tables.json b/crates/query-engine/translation/tests/goldenfiles/select_nested_column_simple/tables.json new file mode 100644 index 000000000..75969cc11 --- /dev/null +++ b/crates/query-engine/translation/tests/goldenfiles/select_nested_column_simple/tables.json @@ -0,0 +1,47 @@ +{ + "compositeTypes": { + "person_address": { + "name": "person_address", + "fields": { + "address_line_1": { + "name": "address_line_1", + "type": { + "scalarType": "text" + } + }, + "address_line_2": { + "name": "address_line_2", + "type": { + "scalarType": "text" + } + } + } + } + }, + "nativeQueries": { + "address_identity_function": { + "sql": "SELECT {{address}} as result", + "columns": { + "result": { + "name": "result", + "type": { + "compositeType": "person_address" + }, + "nullable": "nullable", + "description": null + } + }, + "arguments": { + "address": { + "name": "address", + "type": { + "compositeType": "person_address" + }, + "nullable": "nullable", + "description": null + } + }, + "description": "A native query used to test support for composite types" + } + } +} diff --git a/crates/query-engine/translation/tests/snapshots/tests__select_nested_column_simple.snap b/crates/query-engine/translation/tests/snapshots/tests__select_nested_column_simple.snap new file mode 100644 index 000000000..36c43ed73 --- /dev/null +++ b/crates/query-engine/translation/tests/snapshots/tests__select_nested_column_simple.snap @@ -0,0 +1,54 @@ +--- +source: crates/query-engine/translation/tests/tests.rs +expression: result +--- +BEGIN +ISOLATION LEVEL READ COMMITTED READ ONLY; + +WITH "%1_NATIVE_QUERY_address_identity_function" AS ( + SELECT + jsonb_populate_record(cast(null as person_address), $1) as result +) +SELECT + coalesce(json_agg(row_to_json("%5_universe")), '[]') AS "universe" +FROM + ( + SELECT + * + FROM + ( + SELECT + coalesce(json_agg(row_to_json("%6_rows")), '[]') AS "rows" + FROM + ( + SELECT + "%3_nested_fields_collect"."collected" AS "result" + FROM + "%1_NATIVE_QUERY_address_identity_function" AS "%0_address_identity_function" + LEFT OUTER JOIN LATERAL ( + SELECT + ("%0_address_identity_function"."result").* + ) AS "%2_nested_field_bound" ON ('true') + LEFT OUTER JOIN LATERAL ( + SELECT + row_to_json("%4_nested_fields") AS "collected" + FROM + ( + SELECT + "%2_nested_field_bound"."address_line_1" AS "the_first_line_of_the_address" + ) AS "%4_nested_fields" + ) AS "%3_nested_fields_collect" ON ('true') + ) AS "%6_rows" + ) AS "%6_rows" + ) AS "%5_universe"; + +COMMIT; + +{ + 1: Value( + Object { + "address_line_1": String("Somstreet 159"), + "address_line_2": String("Second door to the right"), + }, + ), +} diff --git a/crates/query-engine/translation/tests/tests.rs b/crates/query-engine/translation/tests/tests.rs index 5db1739ba..90cd8e142 100644 --- a/crates/query-engine/translation/tests/tests.rs +++ b/crates/query-engine/translation/tests/tests.rs @@ -54,6 +54,12 @@ fn select_composite_variable_complex() { insta::assert_snapshot!(result); } +#[test] +fn select_nested_column_simple() { + let result = common::test_translation("select_nested_column_simple").unwrap(); + insta::assert_snapshot!(result); +} + #[test] fn select_array_column_reverse() { let result = common::test_translation("select_array_column_reverse").unwrap(); diff --git a/crates/tests/databases-tests/src/postgres/query_tests.rs b/crates/tests/databases-tests/src/postgres/query_tests.rs index 5ef9b45fe..89f36107b 100644 --- a/crates/tests/databases-tests/src/postgres/query_tests.rs +++ b/crates/tests/databases-tests/src/postgres/query_tests.rs @@ -86,6 +86,12 @@ mod basic { let result = run_query(create_router().await, "select_composite_variable_complex").await; insta::assert_json_snapshot!(result); } + + #[tokio::test] + async fn select_nested_column_simple() { + let result = run_query(create_router().await, "select_nested_column_simple").await; + insta::assert_json_snapshot!(result); + } } #[cfg(test)] diff --git a/crates/tests/databases-tests/src/postgres/snapshots/databases_tests__postgres__query_tests__basic__select_nested_column_simple.snap b/crates/tests/databases-tests/src/postgres/snapshots/databases_tests__postgres__query_tests__basic__select_nested_column_simple.snap new file mode 100644 index 000000000..81b726cb4 --- /dev/null +++ b/crates/tests/databases-tests/src/postgres/snapshots/databases_tests__postgres__query_tests__basic__select_nested_column_simple.snap @@ -0,0 +1,15 @@ +--- +source: crates/tests/databases-tests/src/postgres/query_tests.rs +expression: result +--- +[ + { + "rows": [ + { + "result": { + "the_first_line_of_the_address": "Somstreet 159" + } + } + ] + } +] diff --git a/crates/tests/tests-common/goldenfiles/select_nested_column_simple.json b/crates/tests/tests-common/goldenfiles/select_nested_column_simple.json new file mode 100644 index 000000000..8d7fa2869 --- /dev/null +++ b/crates/tests/tests-common/goldenfiles/select_nested_column_simple.json @@ -0,0 +1,31 @@ +{ + "collection": "address_identity_function", + "query": { + "fields": { + "result": { + "type": "column", + "column": "result", + "fields": { + "type": "object", + "fields": { + "the_first_line_of_the_address": { + "type": "column", + "column": "address_line_1" + } + } + }, + "arguments": {} + } + } + }, + "arguments": { + "address": { + "type": "literal", + "value": { + "address_line_1": "Somstreet 159", + "address_line_2": "Second door to the right" + } + } + }, + "collection_relationships": {} +}