Skip to content

Commit b1ce3aa

Browse files
authored
fix(qc): fix missing nested take/limit (#5573)
[ORM-1096](https://linear.app/prisma-company/issue/ORM-1096/add-missing-limit-in-nested-pagination) This is basically an optimization to do the pagination using the database when we can instead of doing it in-memory.
1 parent 1c4c6a0 commit b1ce3aa

5 files changed

Lines changed: 125 additions & 7 deletions

File tree

query-compiler/query-compiler/src/translate/query/read.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ pub(crate) fn translate_read_query(query: ReadQuery, builder: &dyn QueryBuilder)
101101
}
102102

103103
ReadQuery::RelatedRecordsQuery(rrq) => {
104-
let (expr, join) = build_read_related_records(rrq, vec![], builder)?;
104+
let (expr, join) = build_read_related_records(rrq, vec![], false, builder)?;
105105
if join.is_relation_unique {
106106
Expression::Unique(Box::new(expr))
107107
} else {
@@ -167,8 +167,10 @@ pub(super) fn add_inmemory_join(
167167
_ => None,
168168
})
169169
.map(|rrq| -> TranslateResult<JoinExpression> {
170+
let has_unique_parent = !parent.r#type().is_list();
170171
let prefixed_parent_field_name = binding::nested_relation_field(&rrq.parent_field);
171172
let left_scalars = rrq.parent_field.left_scalars();
173+
172174
let links = left_scalars
173175
.iter()
174176
.zip(rrq.parent_field.related_field().left_scalars())
@@ -177,15 +179,15 @@ pub(super) fn add_inmemory_join(
177179
binding::join_parent_field(parent_scalar),
178180
parent_scalar.type_info().to_prisma_type(),
179181
);
180-
let condition = if parent.r#type().is_list() {
181-
ScalarCondition::InTemplate(ConditionValue::value(placeholder))
182-
} else {
182+
let condition = if has_unique_parent {
183183
ScalarCondition::Equals(ConditionValue::value(placeholder))
184+
} else {
185+
ScalarCondition::InTemplate(ConditionValue::value(placeholder))
184186
};
185187
ConditionalLink::new(child_scalar.clone(), vec![condition])
186188
})
187189
.collect();
188-
let (child, join) = build_read_related_records(rrq, links, builder)?;
190+
let (child, join) = build_read_related_records(rrq, links, has_unique_parent, builder)?;
189191

190192
Ok(JoinExpression {
191193
child,
@@ -220,6 +222,7 @@ pub(super) fn add_inmemory_join(
220222
fn build_read_related_records(
221223
mut rrq: RelatedRecordsQuery,
222224
links: Vec<ConditionalLink>,
225+
has_unique_parent: bool,
223226
builder: &dyn QueryBuilder,
224227
) -> TranslateResult<(Expression, JoinMetadata)> {
225228
// Skip the query entirely if the take is 0.
@@ -265,9 +268,15 @@ fn build_read_related_records(
265268
let selected_fields = rrq.selected_fields.without_relations().into_virtuals_last();
266269
let needs_reversed_order = rrq.args.needs_reversed_order();
267270

268-
let pagination = (rrq.args.take.is_some() || rrq.args.skip.is_some() || rrq.args.cursor.is_some())
271+
// We are forced to use in-memory processing when we have potentially more than one parent
272+
// record (!has_unique_parent). Otherwise our skip/limit on the database level would apply to
273+
// children of multiple parents, which would produce incorrect results.
274+
let needs_pagination = rrq.args.take.is_some() || rrq.args.skip.is_some() || rrq.args.cursor.is_some();
275+
let pagination = (needs_pagination && (!has_unique_parent || rrq.args.requires_inmemory_processing()))
269276
.then(|| extract_pagination(&mut rrq.args));
270-
let distinct_by = (rrq.args.distinct.is_some()).then(|| extract_distinct_by(&mut rrq.args));
277+
let needs_distinct = rrq.args.distinct.is_some();
278+
let distinct_by = (needs_distinct && (!has_unique_parent || rrq.args.requires_inmemory_distinct()))
279+
.then(|| extract_distinct_by(&mut rrq.args));
271280

272281
let (mut child_query, join) = if rrq.parent_field.relation().is_many_to_many() {
273282
build_read_m2m_query(linkage, rrq.args, &selected_fields, builder)?
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"modelName": "User",
3+
"action": "findMany",
4+
"query": {
5+
"arguments": {
6+
"relationLoadStrategy": "query",
7+
"where": {
8+
"id": 1
9+
}
10+
},
11+
"selection": {
12+
"$scalars": true,
13+
"posts": {
14+
"arguments": {
15+
"skip": 10,
16+
"take": 10
17+
},
18+
"selection": { "id": true }
19+
}
20+
}
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"modelName": "User",
3+
"action": "findUnique",
4+
"query": {
5+
"arguments": {
6+
"relationLoadStrategy": "query",
7+
"where": {
8+
"id": 1
9+
}
10+
},
11+
"selection": {
12+
"$scalars": true,
13+
"posts": {
14+
"arguments": {
15+
"skip": 10,
16+
"take": 10
17+
},
18+
"selection": { "id": true }
19+
}
20+
}
21+
}
22+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
source: query-compiler/query-compiler/tests/queries.rs
3+
expression: pretty
4+
input_file: query-compiler/query-compiler/tests/data/query-non-unique-one2m-pagination.json
5+
---
6+
dataMap {
7+
id: Int (id)
8+
email: String (email)
9+
role: Enum<Role> (role)
10+
posts (from @nested$posts): {
11+
id: Int (id)
12+
}
13+
}
14+
enums {
15+
Role: {
16+
admin: ADMIN
17+
user: USER
18+
}
19+
}
20+
let @parent = query «SELECT "public"."User"."id", "public"."User"."email",
21+
"public"."User"."role"::text FROM "public"."User" WHERE
22+
"public"."User"."id" = $1 OFFSET $2»
23+
params [const(BigInt(1)), const(BigInt(0))]
24+
in let @parent$id = mapField id (get @parent)
25+
in join (get @parent)
26+
with (skip 10
27+
take 10
28+
query «SELECT "public"."Post"."id", "public"."Post"."userId" FROM
29+
"public"."Post" WHERE "public"."Post"."userId" IN [$1] ORDER
30+
BY "public"."Post"."id" ASC OFFSET $2»
31+
params [var(@parent$id as Int),
32+
const(BigInt(0))]) on left.(id) = right.(userId) as @nested$posts
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
source: query-compiler/query-compiler/tests/queries.rs
3+
expression: pretty
4+
input_file: query-compiler/query-compiler/tests/data/query-unique-one2m-pagination.json
5+
---
6+
dataMap {
7+
id: Int (id)
8+
email: String (email)
9+
role: Enum<Role> (role)
10+
posts (from @nested$posts): {
11+
id: Int (id)
12+
}
13+
}
14+
enums {
15+
Role: {
16+
admin: ADMIN
17+
user: USER
18+
}
19+
}
20+
let @parent = unique (query «SELECT "public"."User"."id",
21+
"public"."User"."email",
22+
"public"."User"."role"::text FROM "public"."User"
23+
WHERE ("public"."User"."id" = $1 AND 1=1) LIMIT $2
24+
OFFSET $3»
25+
params [const(BigInt(1)), const(BigInt(1)),
26+
const(BigInt(0))])
27+
in let @parent$id = mapField id (get @parent)
28+
in join (get @parent)
29+
with (query «SELECT "public"."Post"."id", "public"."Post"."userId" FROM
30+
"public"."Post" WHERE "public"."Post"."userId" = $1 ORDER BY
31+
"public"."Post"."id" ASC LIMIT $2 OFFSET $3»
32+
params [var(@parent$id as Int), const(BigInt(10)),
33+
const(BigInt(10))]) on left.(id) = right.(userId) as @nested$posts

0 commit comments

Comments
 (0)