Skip to content

Commit c0aafc0

Browse files
authored
fix: make sure columns are fully qualified in aggregates (#5629)
Restores the original behavior where group by aggregations would have column names with explicit table names. This behavior cannot be used with non-group-by aggregates, because they sometimes rely on referring to columns from a subquery. [TML-1483](https://linear.app/prisma-company/issue/TML-1483/fix-groupby-regression) Fixes prisma/prisma#28084
1 parent 53cb2fa commit c0aafc0

7 files changed

Lines changed: 148 additions & 49 deletions

File tree

query-compiler/query-compiler/src/data_mapper.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ fn get_result_node_for_aggregation(
298298
sel.identifiers().map(move |ident| {
299299
let db_alias = ident.db_alias();
300300
let type_info = FieldTypeInformation::new(ident.typ, ident.arity, None);
301-
(ident.name, sel.aggregation_name(), db_alias, type_info)
301+
(ident.field.name(), sel.aggregation_name(), db_alias, type_info)
302302
})
303303
})
304304
.sorted_by_key(|(name, prefix, _, _)| ordered_set.get_index_of(&(*prefix, *name)))

query-compiler/query-compiler/tests/snapshots/queries__queries@group-by.json.snap

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ dataMap {
1111
_all: Int (_count$_all)
1212
}
1313
}
14-
query «SELECT COUNT("id") AS "_count$id", COUNT("title") AS "_count$title",
15-
COUNT("userId") AS "_count$userId", COUNT(*) AS "_count$_all" FROM
16-
"public"."Post" WHERE "public"."Post"."title"::text LIKE $1 GROUP BY
17-
"public"."Post"."title" OFFSET $2»
14+
query «SELECT COUNT("public"."Post"."id") AS "_count$id",
15+
COUNT("public"."Post"."title") AS "_count$title",
16+
COUNT("public"."Post"."userId") AS "_count$userId", COUNT(*) AS
17+
"_count$_all" FROM "public"."Post" WHERE "public"."Post"."title"::text
18+
LIKE $1 GROUP BY "public"."Post"."title" OFFSET $2»
1819
params [const(String("%something%")), const(BigInt(0))]

query-engine/connector-test-kit-rs/query-engine-tests/tests/queries/aggregation/group_by.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,48 @@ mod aggregation_group_by {
577577
Ok(())
578578
}
579579

580+
fn schema_28084() -> String {
581+
let schema = indoc! {
582+
r#"model Teacher {
583+
#id(id, String, @id)
584+
name String
585+
age Int
586+
assistant Assistant?
587+
}
588+
589+
model Assistant {
590+
#id(id, String, @id)
591+
name String
592+
age Int?
593+
teacher Teacher @relation(fields: [teacherId], references: [id])
594+
teacherId String @unique
595+
}
596+
"#
597+
};
598+
599+
schema.to_owned()
600+
}
601+
602+
// regression test for https://github.com/prisma/prisma/issues/28084
603+
#[connector_test(schema(schema_28084))]
604+
async fn regression_28084(runner: Runner) -> TestResult<()> {
605+
run_query!(
606+
&runner,
607+
r#"mutation { createOneTeacher(data: {
608+
id: "a1",
609+
name: "Alice",
610+
age: 30,
611+
assistant: { create: { id: "j1", name: "John" } } }
612+
) { id } }"#
613+
);
614+
615+
insta::assert_snapshot!(
616+
run_query!(&runner, r#"{ groupByTeacher(by: [name], where: { assistant: { name: { contains: "John" } } }) { _sum { age } } }"#),
617+
@r###"{"data":{"groupByTeacher":[{"_sum":{"age":30}}]}}"###
618+
);
619+
620+
Ok(())
621+
}
580622
/// Error cases
581623
582624
#[connector_test]

query-engine/connectors/mongodb-query-connector/src/output_meta.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ pub fn from_aggregation_selection(selection: &AggregationSelection) -> OutputMet
118118

119119
for ident in selection.identifiers() {
120120
map.insert(
121-
ident.field_db_name.into(),
121+
ident.field.db_name().into(),
122122
OutputMeta::Scalar(ScalarOutputMeta {
123123
ident: ident.typ.id,
124124
default: None,

query-engine/query-builders/sql-query-builder/src/model_extensions/column.rs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ impl AsColumns for ModelProjection {
5050

5151
pub trait AsColumn {
5252
fn as_column(&self, ctx: &Context<'_>) -> Column<'static>;
53+
fn as_column_with_style(&self, ctx: &Context<'_>, style: ColumnStyle) -> Column<'static>;
5354
}
5455

5556
impl AsColumns for Field {
@@ -90,15 +91,33 @@ where
9091

9192
impl AsColumn for ScalarField {
9293
fn as_column(&self, ctx: &Context<'_>) -> Column<'static> {
93-
// Unwrap is safe: SQL connectors do not anything other than models as field containers.
94-
let full_table_name = super::table::db_name_with_schema(&self.container().as_model().unwrap(), ctx);
95-
let col = self.db_name().to_string();
94+
self.as_column_with_style(ctx, ColumnStyle::ExplicitTable)
95+
}
96+
97+
fn as_column_with_style(&self, ctx: &Context<'_>, style: ColumnStyle) -> Column<'static> {
98+
let col = match style {
99+
ColumnStyle::ExplicitTable => {
100+
// Unwrap is safe: SQL connectors do not anything other than models as field containers.
101+
let full_table_name = super::table::db_name_with_schema(&self.container().as_model().unwrap(), ctx);
102+
let col = self.db_name().to_string();
103+
Column::from((full_table_name, col))
104+
}
105+
ColumnStyle::ImplicitTable => Column::new(self.db_name().to_owned()),
106+
};
96107

97-
Column::from((full_table_name, col))
98-
.type_family(self.type_family())
108+
col.type_family(self.type_family())
99109
.native_column_type(self.native_type().map(|nt| NativeColumnType::from(nt.name())))
100110
.set_is_enum(self.type_identifier().is_enum())
101111
.set_is_list(self.is_list())
102112
.default(quaint::ast::DefaultValue::Generated)
103113
}
104114
}
115+
116+
/// The style of column rendering.
117+
#[derive(Debug, Clone, Copy)]
118+
pub enum ColumnStyle {
119+
/// The column is rendered with an explicit table name, e.g. `User.id`.
120+
ExplicitTable,
121+
/// The column is rendered without a table name, e.g. `id`.
122+
ImplicitTable,
123+
}

query-engine/query-builders/sql-query-builder/src/read.rs

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::{
66
context::Context,
77
cursor_condition,
88
filter::FilterBuilder,
9-
model_extensions::{AsColumn, AsColumns, AsTable},
9+
model_extensions::{AsColumn, AsColumns, AsTable, ColumnStyle},
1010
nested_aggregations,
1111
ordering::OrderByBuilder,
1212
sql_trace::SqlTraceComment,
@@ -184,7 +184,7 @@ pub fn aggregate(
184184
let sub_table = Table::from(sub_query).alias("sub");
185185
selections.iter().fold(
186186
Select::from_table(sub_table).add_traceparent(ctx.traceparent),
187-
|acc, sel| apply_aggregate_selections(acc, sel, ctx),
187+
|acc, sel| apply_aggregate_selections(acc, sel, ctx, ColumnStyle::ImplicitTable),
188188
)
189189
}
190190

@@ -197,9 +197,9 @@ pub fn group_by_aggregate(
197197
ctx: &Context<'_>,
198198
) -> Select<'static> {
199199
let (base_query, _) = args.into_select(model, &[], ctx);
200-
let select_query = selections
201-
.iter()
202-
.fold(base_query, |acc, sel| apply_aggregate_selections(acc, sel, ctx));
200+
let select_query = selections.iter().fold(base_query, |acc, sel| {
201+
apply_aggregate_selections(acc, sel, ctx, ColumnStyle::ExplicitTable)
202+
});
203203

204204
let grouped = group_by
205205
.into_iter()
@@ -221,32 +221,50 @@ fn apply_aggregate_selections(
221221
select: Select<'static>,
222222
selection: &AggregationSelection,
223223
ctx: &Context,
224+
col_style: ColumnStyle,
224225
) -> Select<'static> {
225226
match selection {
226-
AggregationSelection::Field(field) => select.column(field.as_column(ctx).set_is_selected(true)),
227+
AggregationSelection::Field(field) => {
228+
select.column(field.as_column_with_style(ctx, col_style).set_is_selected(true))
229+
}
227230

228-
AggregationSelection::Count { all, .. } => selection.identifiers().fold(select, |select, next_field| {
229-
let expr = if all.is_some() && next_field.name == "_all" {
230-
asterisk()
231-
} else {
232-
Column::from(next_field.field_db_name.to_owned()).into()
231+
AggregationSelection::Count { .. } => selection.identifiers().fold(select, |select, next_field| {
232+
let expr = match next_field.field {
233+
SelectionField::All => asterisk(),
234+
SelectionField::Scalar(field) => field.as_column_with_style(ctx, col_style).into(),
233235
};
234236
select.value(count(expr).alias(next_field.db_alias().into_owned()))
235237
}),
236238

237239
AggregationSelection::Average(_) => selection.identifiers().fold(select, |select, next_field| {
238-
select
239-
.value(avg(Column::from(next_field.field_db_name.to_owned())).alias(next_field.db_alias().into_owned()))
240+
select.value(
241+
avg(next_field
242+
.field
243+
.as_scalar()
244+
.unwrap()
245+
.as_column_with_style(ctx, col_style))
246+
.alias(next_field.db_alias().into_owned()),
247+
)
240248
}),
241249

242250
AggregationSelection::Sum(_) => selection.identifiers().fold(select, |select, next_field| {
243-
select
244-
.value(sum(Column::from(next_field.field_db_name.to_owned())).alias(next_field.db_alias().into_owned()))
251+
select.value(
252+
sum(next_field
253+
.field
254+
.as_scalar()
255+
.unwrap()
256+
.as_column_with_style(ctx, col_style))
257+
.alias(next_field.db_alias().into_owned()),
258+
)
245259
}),
246260

247261
AggregationSelection::Min(_) => selection.identifiers().fold(select, |select, next_field| {
248262
select.value(
249-
min(Column::from(next_field.field_db_name.to_owned())
263+
min(next_field
264+
.field
265+
.as_scalar()
266+
.unwrap()
267+
.as_column_with_style(ctx, col_style)
250268
.set_is_enum(next_field.typ.id.is_enum())
251269
.set_is_selected(true))
252270
.alias(next_field.db_alias().into_owned()),
@@ -255,7 +273,11 @@ fn apply_aggregate_selections(
255273

256274
AggregationSelection::Max(_) => selection.identifiers().fold(select, |select, next_field| {
257275
select.value(
258-
max(Column::from(next_field.field_db_name.to_owned())
276+
max(next_field
277+
.field
278+
.as_scalar()
279+
.unwrap()
280+
.as_column_with_style(ctx, col_style)
259281
.set_is_enum(next_field.typ.id.is_enum())
260282
.set_is_selected(true))
261283
.alias(next_field.db_alias().into_owned()),

query-engine/query-structure/src/aggregate_selection.rs

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,8 @@ impl AggregationSelection {
6262
AggregationSelection::Count { all, fields } => Either::Right(
6363
self.map_field_types(fields, |_| TypeIdentifier::Int, |_| FieldArity::Required)
6464
.chain(all.iter().map(|all| SelectionIdentifier {
65-
name: all.name(),
65+
field: SelectionField::All,
6666
aggregation_name: self.aggregation_name(),
67-
field_db_name: all.db_name(),
6867
typ: all.r#type(),
6968
arity: all.arity(),
7069
})),
@@ -90,12 +89,11 @@ impl AggregationSelection {
9089
arity_mapper: fn(FieldArity) -> FieldArity,
9190
) -> impl Iterator<Item = SelectionIdentifier<'a>> {
9291
let aggregation_name = self.aggregation_name();
93-
fields.iter().map(move |f| SelectionIdentifier {
94-
name: f.name(),
92+
fields.iter().map(move |field| SelectionIdentifier {
93+
field: SelectionField::Scalar(field),
9594
aggregation_name,
96-
field_db_name: f.db_name(),
97-
typ: f.dm.clone().zip(type_mapper(f.type_identifier())),
98-
arity: arity_mapper(f.arity()),
95+
typ: field.dm.clone().zip(type_mapper(field.type_identifier())),
96+
arity: arity_mapper(field.arity()),
9997
})
10098
}
10199
}
@@ -110,16 +108,6 @@ impl CountAllAggregationSelection {
110108
CountAllAggregationSelection { dm }
111109
}
112110

113-
#[inline]
114-
pub fn name(&self) -> &str {
115-
"_all"
116-
}
117-
118-
#[inline]
119-
pub fn db_name(&self) -> &str {
120-
"_all"
121-
}
122-
123111
#[inline]
124112
pub fn type_identifier(&self) -> TypeIdentifier {
125113
TypeIdentifier::Int
@@ -142,8 +130,7 @@ impl<I> From<&'_ Zipper<I>> for CountAllAggregationSelection {
142130
}
143131

144132
pub struct SelectionIdentifier<'a> {
145-
pub name: &'a str,
146-
pub field_db_name: &'a str,
133+
pub field: SelectionField<'a>,
147134
pub aggregation_name: Option<&'static str>,
148135
pub typ: Type,
149136
pub arity: FieldArity,
@@ -153,8 +140,36 @@ impl<'a> SelectionIdentifier<'a> {
153140
pub fn db_alias(&self) -> Cow<'a, str> {
154141
const FIELD_SEPARATOR: &str = "$";
155142
self.aggregation_name
156-
.map_or(Cow::Borrowed(self.field_db_name), |aggregation| {
157-
Cow::Owned(format!("{aggregation}{FIELD_SEPARATOR}{}", self.field_db_name))
143+
.map_or(Cow::Borrowed(self.field.db_name()), |aggregation| {
144+
Cow::Owned(format!("{aggregation}{FIELD_SEPARATOR}{}", self.field.db_name()))
158145
})
159146
}
160147
}
148+
149+
pub enum SelectionField<'a> {
150+
Scalar(&'a ScalarFieldRef),
151+
All,
152+
}
153+
154+
impl<'a> SelectionField<'a> {
155+
pub fn name(&self) -> &'a str {
156+
match self {
157+
Self::Scalar(f) => f.name(),
158+
Self::All => "_all",
159+
}
160+
}
161+
162+
pub fn db_name(&self) -> &'a str {
163+
match self {
164+
Self::Scalar(f) => f.db_name(),
165+
Self::All => "_all",
166+
}
167+
}
168+
169+
pub fn as_scalar(&self) -> Option<&'a ScalarFieldRef> {
170+
match self {
171+
Self::Scalar(f) => Some(f),
172+
Self::All => None,
173+
}
174+
}
175+
}

0 commit comments

Comments
 (0)