Skip to content

Commit b99ebaa

Browse files
committed
perf(query-compiler): skip nested-only upsert update nodes
Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
1 parent 23d89d5 commit b99ebaa

3 files changed

Lines changed: 276 additions & 41 deletions

File tree

query-compiler/core/src/query_graph_builder/write/nested/upsert_nested.rs

Lines changed: 75 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use super::*;
2-
use crate::inputs::{IfInput, UpdateManyRecordsSelectorsInput, UpdateOrCreateArgsInput, UpdateRecordSelectorsInput};
2+
use crate::inputs::{
3+
IfInput, ReturnInput, UpdateManyRecordsSelectorsInput, UpdateOrCreateArgsInput, UpdateRecordSelectorsInput,
4+
};
35
use crate::query_graph_builder::write::utils::coerce_values;
6+
use crate::query_graph_builder::write::write_args_parser::WriteArgsParser;
47
use crate::{DataExpectation, RowSink};
58
use crate::{
69
ParsedInputMap, ParsedInputValue,
@@ -138,14 +141,9 @@ pub fn nested_upsert(
138141
let if_node = graph.create_node(Flow::if_non_empty());
139142
let create_node =
140143
create::create_record_node(graph, query_schema, child_model.clone(), create_input.try_into()?)?;
141-
let update_node = update::update_record_node(
142-
graph,
143-
query_schema,
144-
filter.clone(),
145-
child_model.clone(),
146-
update_input.try_into()?,
147-
None,
148-
)?;
144+
let update_map: ParsedInputMap<'_> = update_input.try_into()?;
145+
let update_args = WriteArgsParser::from(&child_model, update_map)?;
146+
let is_nested_only_update = update_args.args.is_empty() && !update_args.nested.is_empty();
149147

150148
graph.create_edge(
151149
&read_children_node,
@@ -157,41 +155,77 @@ pub fn nested_upsert(
157155
),
158156
)?;
159157

160-
graph.create_edge(
161-
&read_children_node,
162-
&update_node,
163-
QueryGraphDependency::ProjectedDataDependency(
164-
child_model_identifier.clone(),
165-
RowSink::ExactlyOne(&UpdateRecordSelectorsInput),
166-
Some(DataExpectation::non_empty_rows(
167-
MissingRelatedRecord::builder()
168-
.model(&child_model)
169-
.relation(&parent_relation_field.relation())
170-
.needed_for(DependentOperation::nested_update())
171-
.operation(DataOperation::NestedUpsert)
172-
.build(),
173-
)),
174-
),
175-
)?;
158+
let then_node = if is_nested_only_update {
159+
let return_node = graph.create_node(Flow::Return(Vec::new()));
176160

177-
// In case the connector doesn't support referential integrity, we add a subtree to the graph that emulates the ON_UPDATE referential action.
178-
// When that's the case, we create an intermediary node to which we connect all the nodes reponsible for emulating the referential action
179-
// Then, we connect the if node to that intermediary emulation node. This enables performing the emulation only in case the graph traverses
180-
// the update path (if the children already exists and goes to the THEN node).
181-
// It's only after we've executed the emulation that it'll traverse the update node, hence the ExecutionOrder between
182-
// the emulation node and the update node.
183-
let then_node = if let Some(emulation_node) = utils::insert_emulated_on_update_with_intermediary_node(
184-
graph,
185-
query_schema,
186-
&child_model,
187-
&read_children_node,
188-
&update_node,
189-
)? {
190-
graph.create_edge(&emulation_node, &update_node, QueryGraphDependency::ExecutionOrder)?;
161+
graph.create_edge(
162+
&read_children_node,
163+
&return_node,
164+
QueryGraphDependency::ProjectedDataDependency(
165+
child_model_identifier.clone(),
166+
RowSink::All(&ReturnInput),
167+
Some(DataExpectation::non_empty_rows(
168+
MissingRelatedRecord::builder()
169+
.model(&child_model)
170+
.relation(&parent_relation_field.relation())
171+
.needed_for(DependentOperation::nested_update())
172+
.operation(DataOperation::NestedUpsert)
173+
.build(),
174+
)),
175+
),
176+
)?;
191177

192-
emulation_node
178+
for (relation_field, data_map) in update_args.nested {
179+
connect_nested_query(graph, query_schema, return_node, relation_field, data_map)?;
180+
}
181+
182+
return_node
193183
} else {
194-
update_node
184+
let update_node = update::update_record_node_from_args(
185+
graph,
186+
query_schema,
187+
filter.clone(),
188+
child_model.clone(),
189+
update_args,
190+
None,
191+
)?;
192+
193+
graph.create_edge(
194+
&read_children_node,
195+
&update_node,
196+
QueryGraphDependency::ProjectedDataDependency(
197+
child_model_identifier.clone(),
198+
RowSink::ExactlyOne(&UpdateRecordSelectorsInput),
199+
Some(DataExpectation::non_empty_rows(
200+
MissingRelatedRecord::builder()
201+
.model(&child_model)
202+
.relation(&parent_relation_field.relation())
203+
.needed_for(DependentOperation::nested_update())
204+
.operation(DataOperation::NestedUpsert)
205+
.build(),
206+
)),
207+
),
208+
)?;
209+
210+
// In case the connector doesn't support referential integrity, we add a subtree to the graph that emulates the ON_UPDATE referential action.
211+
// When that's the case, we create an intermediary node to which we connect all the nodes reponsible for emulating the referential action
212+
// Then, we connect the if node to that intermediary emulation node. This enables performing the emulation only in case the graph traverses
213+
// the update path (if the children already exists and goes to the THEN node).
214+
// It's only after we've executed the emulation that it'll traverse the update node, hence the ExecutionOrder between
215+
// the emulation node and the update node.
216+
if let Some(emulation_node) = utils::insert_emulated_on_update_with_intermediary_node(
217+
graph,
218+
query_schema,
219+
&child_model,
220+
&read_children_node,
221+
&update_node,
222+
)? {
223+
graph.create_edge(&emulation_node, &update_node, QueryGraphDependency::ExecutionOrder)?;
224+
225+
emulation_node
226+
} else {
227+
update_node
228+
}
195229
};
196230

197231
graph.create_edge(&if_node, &then_node, QueryGraphDependency::Then)?;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"modelName": "User",
3+
"action": "updateOne",
4+
"query": {
5+
"arguments": {
6+
"relationLoadStrategy": "query",
7+
"where": { "email": "user.1737556028164@prisma.io" },
8+
"data": {
9+
"posts": {
10+
"upsert": {
11+
"where": { "id": 11 },
12+
"update": {
13+
"categories": {
14+
"connect": { "id": 10 }
15+
}
16+
},
17+
"create": {
18+
"title": "Nested upsert created post",
19+
"categories": {
20+
"connect": { "id": 10 }
21+
}
22+
}
23+
}
24+
}
25+
}
26+
},
27+
"selection": {
28+
"$composites": true,
29+
"$scalars": true,
30+
"posts": {
31+
"arguments": {},
32+
"selection": {
33+
"$composites": true,
34+
"$scalars": true,
35+
"categories": {
36+
"arguments": {},
37+
"selection": { "$composites": true, "$scalars": true }
38+
}
39+
}
40+
}
41+
}
42+
}
43+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
---
2+
source: query-compiler/query-compiler/tests/queries.rs
3+
expression: pretty
4+
input_file: query-compiler/query-compiler/tests/data/nested-upsert-nested-only.json
5+
snapshot_kind: text
6+
---
7+
transaction
8+
dataMap {
9+
id: Int (id)
10+
email: String (email)
11+
role: Enum<Role> (role)
12+
posts (from @nested$posts): {
13+
id: Int (id)
14+
title: String (title)
15+
userId: Int (userId)
16+
categories (from @nested$categories): {
17+
id: Int (id)
18+
name: String (name)
19+
}
20+
}
21+
}
22+
enums {
23+
Role: {
24+
admin: ADMIN
25+
user: USER
26+
}
27+
}
28+
let 0 = unique (query «SELECT "public"."User"."id", "public"."User"."email",
29+
"public"."User"."role"::text FROM "public"."User"
30+
WHERE "public"."User"."email" = $1 LIMIT $2 OFFSET $3»
31+
params [const(String("user.1737556028164@prisma.io")),
32+
const(BigInt(1)), const(BigInt(0))])
33+
in let 0$id = mapField id (get 0)
34+
in let 1 = query «SELECT "public"."Post"."id", "public"."Post"."userId"
35+
FROM "public"."Post" WHERE ("public"."Post"."id" = $1
36+
AND "public"."Post"."userId" IN [$2,*]) OFFSET $3»
37+
params [const(BigInt(11)), var(0$id as Int), const(BigInt(0))]
38+
in if (rowCountNeq 0 (get 1))
39+
then let 1 = validate (get 1)
40+
[ rowCountNeq 0
41+
] orRaise "MISSING_RELATED_RECORD"
42+
in let 6 = get 1
43+
in let 7 = query «SELECT "public"."Category"."id" FROM
44+
"public"."Category" WHERE
45+
"public"."Category"."id" = $1 OFFSET $2»
46+
params [const(BigInt(10)), const(BigInt(0))]
47+
in let 6 = unique (validate (get 6)
48+
[ rowCountNeq 0
49+
] orRaise "MISSING_RELATED_RECORD");
50+
6$id = mapField id (get 6);
51+
7 = validate (get 7)
52+
[ rowCountEq 1
53+
] orRaise "INCOMPLETE_CONNECT_INPUT";
54+
7$id = mapField id (get 7)
55+
in execute «INSERT INTO "public"."_CategoryToPost"
56+
("B","A") VALUES [($1),*],* ON CONFLICT DO
57+
NOTHING»
58+
params [product(var(6$id as Int[]),
59+
var(7$id as Int[]))]
60+
else let 0 = unique (validate (get 0)
61+
[ rowCountNeq 0
62+
] orRaise "MISSING_RELATED_RECORD");
63+
0$id = mapField id (get 0)
64+
in let 3 = unique (query «INSERT INTO "public"."Post"
65+
("title","userId") VALUES ($1,$2)
66+
RETURNING "public"."Post"."id"»
67+
params [const(String("Nested upsert created post")),
68+
var(0$id as Int)])
69+
in let 4 = query «SELECT "public"."Category"."id" FROM
70+
"public"."Category" WHERE
71+
"public"."Category"."id" = $1 OFFSET $2»
72+
params [const(BigInt(10)), const(BigInt(0))]
73+
in let 3 = unique (validate (get 3)
74+
[ rowCountNeq 0
75+
] orRaise "MISSING_RELATED_RECORD");
76+
3$id = mapField id (get 3);
77+
4 = validate (get 4)
78+
[ rowCountEq 1
79+
] orRaise "INCOMPLETE_CONNECT_INPUT";
80+
4$id = mapField id (get 4)
81+
in execute «INSERT INTO "public"."_CategoryToPost"
82+
("B","A") VALUES [($1),*],* ON CONFLICT DO
83+
NOTHING»
84+
params [product(var(3$id as Int[]),
85+
var(4$id as Int[]))];
86+
let 0 = unique (validate (get 0)
87+
[ rowCountNeq 0
88+
] orRaise "MISSING_RECORD");
89+
0$id = mapField id (get 0)
90+
in rawNestedReadUnique (query «SELECT "public"."User"."id",
91+
"public"."User"."email",
92+
"public"."User"."role"::text FROM
93+
"public"."User" WHERE "public"."User"."id"
94+
= $1 LIMIT $2 OFFSET $3»
95+
params [var(0$id as Int), const(BigInt(1)),
96+
const(BigInt(0))]
97+
fields {
98+
id: 0
99+
email: 1
100+
role: 2
101+
}
102+
relations [posts on left.0 = right.2 many (query
103+
«SELECT
104+
"public"."Post"."id",
105+
"public"."Post"."title",
106+
"public"."Post"."userId"
107+
FROM
108+
"public"."Post"
109+
WHERE
110+
"public"."Post"."userId"
111+
= $1
112+
OFFSET
113+
$2»
114+
params [var(@parent$id as Int),
115+
const(BigInt(0))]
116+
fields {
117+
id: 0
118+
title: 1
119+
userId: 2
120+
}
121+
relations [categories on left.0 = right.2 many (query
122+
«SELECT
123+
"public"."Category"."id",
124+
"public"."Category"."name",
125+
"t0"."B"
126+
AS
127+
"CategoryToPost@Post"
128+
FROM
129+
"public"."Category"
130+
INNER
131+
JOIN
132+
"public"."_CategoryToPost"
133+
AS
134+
"t0"
135+
ON
136+
"t0"."A"
137+
=
138+
"public"."Category"."id"
139+
WHERE
140+
(1=1
141+
AND
142+
"t0"."B"
143+
IN
144+
[$1,*])
145+
OFFSET
146+
$2»
147+
params [var(@parent$id as Int),
148+
const(BigInt(0))]
149+
fields {
150+
id: 0
151+
name: 1
152+
})])])
153+
enums {
154+
Role: {
155+
admin: ADMIN
156+
user: USER
157+
}
158+
}

0 commit comments

Comments
 (0)