Skip to content

Commit 346d2db

Browse files
author
Gil Mizrahi
authored
fix nested fields relationships (#564)
### What Selecting nested field relationships had a bug where it would try to look up a composite type field column in the parent collection. For example: I have a collection `institution` with a field `staff` that is a composite type that contains the field `favorite_artist_id`. Trying to have a relationship from this field to the `Artist` table would attempt to look-up this field in `institution`, instead of in `staff`. This PR fixes this. ### How Before, we had a type called `TableNameAndReference` that describe the metadata name of a table and held a (relationship alias) reference to it. - We modify this type to contain a `Source` instead of a `Name`, which can be a collection name or a nested field name. This is done so it is clearer whether we are tracking a collection or a type. - Instead of looking up a `Collection` everywhere, we lookup /anything/ that can have fields by replacing `lookup_collection` with `lookup_fields_info`. - When translating nested fields, instead of passing the `joins` accumulator for the parent table, we create another on the fly, and translate the joins using the table created for the nested field instead of the parent table. We also: - Create nicer aliases for nested fields so they are easier to track - Add a test that is based on sql tables from the `v3-engine`, so we add a new `sql` file and update the configurations accordingly.
1 parent 91a6a39 commit 346d2db

32 files changed

+5926
-183
lines changed

changelog.md

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
- Allow Native Operations that end with a semicolon when it's easy to remove them.
2626
[#566](https://github.com/hasura/ndc-postgres/pull/566)
27+
- Fix nested field relationships.
28+
[#564](https://github.com/hasura/ndc-postgres/pull/564)
2729

2830
## [v1.0.1]
2931

crates/query-engine/sql/src/sql/ast.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ pub enum From {
139139
Unnest {
140140
expression: Expression,
141141
alias: TableAlias,
142-
column: ColumnAlias,
142+
columns: Vec<ColumnAlias>,
143143
},
144144
GenerateSeries {
145145
from: usize,
@@ -306,7 +306,7 @@ pub enum Expression {
306306
}
307307

308308
/// Represents the name of a field in a nested object.
309-
#[derive(Debug, Clone, PartialEq, Eq)]
309+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
310310
pub struct NestedField(pub String);
311311

312312
/// An unary operator
@@ -409,6 +409,11 @@ pub enum TableReference {
409409
},
410410
/// refers to an alias we created
411411
AliasedTable(TableAlias),
412+
/// refers to a nested field
413+
NestedField {
414+
source: Box<TableReference>,
415+
field: NestedField,
416+
},
412417
}
413418

414419
/// A database table's column name

crates/query-engine/sql/src/sql/convert.rs

+20-4
Original file line numberDiff line numberDiff line change
@@ -315,17 +315,26 @@ impl From {
315315
From::Unnest {
316316
expression,
317317
alias,
318-
column,
318+
columns,
319319
} => {
320320
sql.append_syntax("UNNEST");
321321
sql.append_syntax("(");
322322
expression.to_sql(sql);
323323
sql.append_syntax(")");
324324
sql.append_syntax(" AS ");
325325
alias.to_sql(sql);
326-
sql.append_syntax("(");
327-
column.to_sql(sql);
328-
sql.append_syntax(")");
326+
if !columns.is_empty() {
327+
sql.append_syntax("(");
328+
329+
for (index, column) in columns.iter().enumerate() {
330+
column.to_sql(sql);
331+
if index < (columns.len() - 1) {
332+
sql.append_syntax(", ");
333+
}
334+
}
335+
336+
sql.append_syntax(")");
337+
}
329338
}
330339
From::GenerateSeries { from, to } => {
331340
sql.append_syntax("generate_series");
@@ -701,6 +710,13 @@ impl TableReference {
701710
table.to_sql(sql);
702711
}
703712
TableReference::AliasedTable(alias) => alias.to_sql(sql),
713+
TableReference::NestedField { source, field } => {
714+
sql.append_syntax("(");
715+
source.to_sql(sql);
716+
sql.append_syntax(")");
717+
sql.append_syntax(".");
718+
field.to_sql(sql);
719+
}
704720
};
705721
}
706722
}

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

+97-52
Original file line numberDiff line numberDiff line change
@@ -53,22 +53,50 @@ pub struct NativeQueryInfo {
5353
#[derive(Debug, Clone, PartialEq, Eq)]
5454
pub struct RootAndCurrentTables {
5555
/// The root (top-most) table in the query.
56-
pub root_table: TableNameAndReference,
56+
pub root_table: TableSourceAndReference,
5757
/// The current table we are processing.
58-
pub current_table: TableNameAndReference,
58+
pub current_table: TableSourceAndReference,
5959
}
6060

6161
/// For a table in the query, We'd like to track what is its reference in the query
62-
/// (the name we can use to address them, an alias we generate), and what is their name in the
62+
/// (the name we can use to address them, an alias we generate), and what is their source in the
6363
/// metadata (so we can get their information such as which columns are available for that table).
6464
#[derive(Debug, Clone, PartialEq, Eq)]
65-
pub struct TableNameAndReference {
65+
pub struct TableSourceAndReference {
6666
/// Table name for column lookup
67-
pub name: models::CollectionName,
67+
pub source: TableSource,
6868
/// Table alias to query from
6969
pub reference: sql::ast::TableReference,
7070
}
7171

72+
/// How to find the relevant information about a table.
73+
#[derive(Debug, Clone, PartialEq, Eq)]
74+
pub enum TableSource {
75+
/// Using the collection name.
76+
Collection(models::CollectionName),
77+
/// Using the nested field path.
78+
NestedField {
79+
type_name: models::TypeName,
80+
// These are used to create a nice table alias.
81+
collection_name: models::CollectionName,
82+
field_path: FieldPath,
83+
},
84+
}
85+
86+
impl TableSource {
87+
/// Generate a nice name that can be used to give a table alias for this source.
88+
pub fn name_for_alias(&self) -> String {
89+
match self {
90+
TableSource::Collection(collection_name) => collection_name.to_string(),
91+
TableSource::NestedField {
92+
collection_name,
93+
field_path,
94+
type_name: _,
95+
} => format!("{collection_name}.{}", field_path.0.join(".")),
96+
}
97+
}
98+
}
99+
72100
#[derive(Debug)]
73101
/// Information about columns
74102
pub struct ColumnInfo {
@@ -108,11 +136,11 @@ pub enum CompositeTypeInfo<'env> {
108136
/// Metadata information about any object that can have fields
109137
pub enum FieldsInfo<'env> {
110138
Table {
111-
name: &'env models::CollectionName,
139+
name: models::CollectionName,
112140
info: &'env metadata::TableInfo,
113141
},
114142
NativeQuery {
115-
name: &'env models::CollectionName,
143+
name: models::CollectionName,
116144
info: &'env metadata::NativeQueryInfo,
117145
},
118146
CompositeType {
@@ -124,7 +152,10 @@ pub enum FieldsInfo<'env> {
124152
impl<'a> From<&'a CompositeTypeInfo<'a>> for FieldsInfo<'a> {
125153
fn from(value: &'a CompositeTypeInfo<'a>) -> Self {
126154
match value {
127-
CompositeTypeInfo::Table { name, info } => FieldsInfo::Table { name, info },
155+
CompositeTypeInfo::Table { name, info } => FieldsInfo::Table {
156+
name: name.clone(),
157+
info,
158+
},
128159
CompositeTypeInfo::CompositeType { name, info } => FieldsInfo::CompositeType {
129160
name: name.clone(),
130161
info,
@@ -136,8 +167,14 @@ impl<'a> From<&'a CompositeTypeInfo<'a>> for FieldsInfo<'a> {
136167
impl<'a> From<&'a CollectionInfo<'a>> for FieldsInfo<'a> {
137168
fn from(value: &'a CollectionInfo<'a>) -> Self {
138169
match value {
139-
CollectionInfo::Table { name, info } => FieldsInfo::Table { name, info },
140-
CollectionInfo::NativeQuery { name, info } => FieldsInfo::NativeQuery { name, info },
170+
CollectionInfo::Table { name, info } => FieldsInfo::Table {
171+
name: (*name).clone(),
172+
info,
173+
},
174+
CollectionInfo::NativeQuery { name, info } => FieldsInfo::NativeQuery {
175+
name: (*name).clone(),
176+
info,
177+
},
141178
}
142179
}
143180
}
@@ -182,55 +219,63 @@ impl<'request> Env<'request> {
182219
/// Queries, and Composite Types.
183220
///
184221
/// This is used to translate field selection, where any of these may occur.
185-
pub fn lookup_fields_info(
186-
&self,
187-
type_name: &'request models::CollectionName,
188-
) -> Result<FieldsInfo<'request>, Error> {
189-
// Lookup the fields of a type name in a specific order:
190-
// tables, then composite types, then native queries.
191-
let info = self
192-
.metadata
193-
.tables
194-
.0
195-
.get(type_name)
196-
.map(|t| FieldsInfo::Table {
197-
name: type_name,
198-
info: t,
199-
})
200-
.or_else(|| {
201-
self.metadata
222+
pub fn lookup_fields_info(&self, source: &TableSource) -> Result<FieldsInfo<'request>, Error> {
223+
match source {
224+
TableSource::NestedField {
225+
collection_name: _,
226+
type_name,
227+
field_path: _,
228+
} => {
229+
let info = self
230+
.metadata
202231
.composite_types
203232
.0
204233
.get(type_name.as_str())
205234
.map(|t| FieldsInfo::CompositeType {
206235
name: t.type_name.clone().into(),
207236
info: t,
208-
})
209-
})
210-
.or_else(|| {
211-
self.metadata
212-
.native_operations
213-
.queries
237+
});
238+
239+
info.ok_or(Error::ScalarTypeNotFound(type_name.as_str().into()))
240+
}
241+
TableSource::Collection(collection_name) => {
242+
// Lookup the fields of a type name in a specific order:
243+
// tables, then composite types, then native queries.
244+
let info = self
245+
.metadata
246+
.tables
214247
.0
215-
.get(type_name)
216-
.map(|nq| FieldsInfo::NativeQuery {
217-
name: type_name,
218-
info: nq,
248+
.get(collection_name)
249+
.map(|t| FieldsInfo::Table {
250+
name: collection_name.clone(),
251+
info: t,
219252
})
220-
})
221-
.or_else(|| {
222-
self.metadata
223-
.native_operations
224-
.mutations
225-
.0
226-
.get(type_name.as_str())
227-
.map(|nq| FieldsInfo::NativeQuery {
228-
name: type_name,
229-
info: nq,
253+
.or_else(|| {
254+
self.metadata
255+
.native_operations
256+
.queries
257+
.0
258+
.get(collection_name)
259+
.map(|nq| FieldsInfo::NativeQuery {
260+
name: collection_name.clone(),
261+
info: nq,
262+
})
230263
})
231-
});
232-
233-
info.ok_or(Error::CollectionNotFound(type_name.as_str().into()))
264+
.or_else(|| {
265+
self.metadata
266+
.native_operations
267+
.mutations
268+
.0
269+
.get(collection_name.as_str())
270+
.map(|nq| FieldsInfo::NativeQuery {
271+
name: collection_name.clone(),
272+
info: nq,
273+
})
274+
});
275+
276+
info.ok_or(Error::CollectionNotFound(collection_name.as_str().into()))
277+
}
278+
}
234279
}
235280

236281
/// Lookup a metadata object which can be described by a Composite Type. This can be any of
@@ -265,7 +310,7 @@ impl<'request> Env<'request> {
265310
})
266311
});
267312

268-
info.ok_or(Error::CollectionNotFound(type_name.as_str().into()))
313+
info.ok_or(Error::ScalarTypeNotFound(type_name.as_str().into()))
269314
}
270315

271316
/// Lookup a collection's information in the metadata.
@@ -591,7 +636,7 @@ impl NativeQueries {
591636
}
592637

593638
/// A newtype wrapper around an ndc-spec type which represents accessing a nested field.
594-
#[derive(Debug, Clone)]
639+
#[derive(Debug, Clone, PartialEq, Eq)]
595640
pub struct FieldPath(pub Vec<models::FieldName>);
596641

597642
impl From<&Option<Vec<models::FieldName>>> for FieldPath {

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Auto-generate delete mutations and translate them into sql ast.
22
33
use crate::translation::error::Error;
4-
use crate::translation::helpers::{self, TableNameAndReference};
4+
use crate::translation::helpers::{self, TableSourceAndReference};
55
use crate::translation::query::filtering;
66
use crate::translation::query::values;
77
use ndc_models as models;
@@ -104,8 +104,8 @@ pub fn translate(
104104

105105
let table_alias = state.make_table_alias(table_name.0.clone());
106106

107-
let table_name_and_reference = TableNameAndReference {
108-
name: collection_name.clone(),
107+
let table_name_and_reference = TableSourceAndReference {
108+
source: helpers::TableSource::Collection(collection_name.clone()),
109109
reference: sql::ast::TableReference::AliasedTable(table_alias.clone()),
110110
};
111111

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Auto-generate insert mutations and translate them into sql ast.
22
33
use crate::translation::error::Error;
4-
use crate::translation::helpers::{self, TableNameAndReference};
4+
use crate::translation::helpers::{self, TableSourceAndReference};
55
use crate::translation::mutation::check_columns;
66
use crate::translation::query::filtering;
77
use crate::translation::query::values;
@@ -215,8 +215,8 @@ pub fn translate(
215215

216216
let (columns, from) = translate_objects_to_columns_and_values(env, state, mutation, object)?;
217217

218-
let table_name_and_reference = TableNameAndReference {
219-
name: mutation.collection_name.clone(),
218+
let table_name_and_reference = TableSourceAndReference {
219+
source: helpers::TableSource::Collection(mutation.collection_name.clone()),
220220
reference: sql::ast::TableReference::DBTable {
221221
schema: mutation.schema_name.clone(),
222222
table: mutation.table_name.clone(),

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Auto-generate update mutations and translate them into sql ast.
22
33
use crate::translation::error::Error;
4-
use crate::translation::helpers::{self, TableNameAndReference};
4+
use crate::translation::helpers::{self, TableSourceAndReference};
55
use crate::translation::mutation::check_columns;
66
use crate::translation::query::filtering;
77
use crate::translation::query::values;
@@ -121,8 +121,8 @@ pub fn translate(
121121

122122
let set = parse_update_columns(env, state, mutation, object)?;
123123

124-
let table_name_and_reference = TableNameAndReference {
125-
name: mutation.collection_name.clone(),
124+
let table_name_and_reference = TableSourceAndReference {
125+
source: helpers::TableSource::Collection(mutation.collection_name.clone()),
126126
reference: sql::ast::TableReference::DBTable {
127127
schema: mutation.schema_name.clone(),
128128
table: mutation.table_name.clone(),

0 commit comments

Comments
 (0)