Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions query-compiler/query-compiler/src/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,23 @@ impl Expression {
}
Expression::Transaction(expr) => {
expr.simplify();
// A plan that executes at most one statement is atomic on its own and doesn't
// need a transaction. Skipping it avoids BEGIN/COMMIT round-trips and keeps
// single-statement operations like `updateMany` and `createMany` working on
// driver adapters without transaction support (e.g. Neon over HTTP).
// A chunkable statement may still be split into multiple queries by the client
// if its parameters exceed the bind limit; such chunks then run without a
// transaction, consistent with how `deleteMany` and read queries, which are
// never wrapped in a transaction, are chunked today. Chunkability cannot
// refine this decision at compile time: nearly every single-statement write is
// marked chunkable (inserts unconditionally, filters unless negated), so
// keeping the wrapper for chunkable statements would keep it for essentially
// all of them, and whether a statement actually splits depends on the
// adapter-specific bind limit that is only known to the client at runtime.
// See https://github.com/prisma/prisma/issues/29748.
if expr.max_statement_count() <= 1 {
*self = std::mem::replace(expr, Expression::Unit);
}
}
Expression::DataMap { expr, .. } => {
expr.simplify();
Expand Down Expand Up @@ -236,6 +253,55 @@ impl Expression {
}
}
}

/// Returns an upper bound on the number of database statements (`Query` or `Execute`
/// nodes) that evaluating this expression can execute, assuming each statement node
/// renders to a single query.
///
/// Nested `Transaction` expressions are treated as unbounded since they manage their own
/// lifecycle.
fn max_statement_count(&self) -> usize {
match self {
Expression::Value(_) | Expression::Get { .. } | Expression::GetFirstNonEmpty { .. } | Expression::Unit => 0,

Expression::Query(_) | Expression::Execute(_) => 1,

Expression::Seq(exprs) | Expression::Concat(exprs) | Expression::Sum(exprs) => exprs
.iter()
.map(Self::max_statement_count)
.fold(0, usize::saturating_add),
Comment thread
aqrln marked this conversation as resolved.

Expression::Let { bindings, expr } => bindings
.iter()
.map(|binding| binding.expr.max_statement_count())
.fold(expr.max_statement_count(), usize::saturating_add),

Expression::Unique(expr)
| Expression::Required(expr)
| Expression::MapField { records: expr, .. }
| Expression::DataMap { expr, .. }
| Expression::Validate { expr, .. }
| Expression::InitializeRecord { expr, .. }
| Expression::MapRecord { expr, .. }
| Expression::Process { expr, .. } => expr.max_statement_count(),

Expression::Join { parent, children, .. } => children
.iter()
.map(|child| child.child.max_statement_count())
.fold(parent.max_statement_count(), usize::saturating_add),

// Only one of the branches is evaluated, so the bound is the larger of the two.
Expression::If {
value, then, r#else, ..
} => value
.max_statement_count()
.saturating_add(then.max_statement_count().max(r#else.max_statement_count())),

Expression::Diff { from, to, .. } => from.max_statement_count().saturating_add(to.max_statement_count()),

Expression::Transaction(_) => usize::MAX,
}
}
}

#[derive(Debug, Serialize)]
Expand Down
11 changes: 7 additions & 4 deletions query-compiler/query-compiler/src/translate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ pub fn translate(mut graph: QueryGraph, builder: &dyn QueryBuilder) -> Translate
.collect::<TranslateResult<Vec<_>>>()
.map(Expression::Seq)?;

let mut root = if let Some(structure) = structure {
let root = if let Some(structure) = structure {
Expression::DataMap {
expr: Box::new(root),
structure,
Expand All @@ -57,10 +57,13 @@ pub fn translate(mut graph: QueryGraph, builder: &dyn QueryBuilder) -> Translate
root
};

let mut root = if graph.needs_transaction() {
Transaction(Box::new(root))
} else {
root
};

root.simplify();
if graph.needs_transaction() {
return Ok(Transaction(Box::new(root)));
}
Ok(root)
}

Expand Down
17 changes: 17 additions & 0 deletions query-compiler/query-compiler/tests/data/update-many.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"modelName": "User",
"action": "updateMany",
"query": {
"arguments": {
"where": {
"email": "user@prisma.io"
},
"data": {
"role": "ADMIN"
}
},
"selection": {
"$scalars": true
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,20 @@ source: query-compiler/query-compiler/tests/queries.rs
expression: pretty
input_file: query-compiler/query-compiler/tests/data/create-many-and-return.json
---
transaction
dataMap {
id: Int (id)
email: String (email)
role: Enum<Role> (role)
}
enums {
Role: {
admin: ADMIN
user: USER
}
}
query «INSERT INTO "public"."User" ("email","role") VALUES ($1,CAST($2::text
AS "public"."Role")), ($3,CAST($4::text AS "public"."Role")) RETURNING
"public"."User"."id", "public"."User"."email",
"public"."User"."role"::text»
params [const(String("user.1737556028164@prisma.io")), const(String("user")),
const(String("user.1737556028165@prisma.io")), const(String("user"))]
dataMap {
id: Int (id)
email: String (email)
role: Enum<Role> (role)
}
enums {
Role: {
admin: ADMIN
user: USER
}
}
query «INSERT INTO "public"."User" ("email","role") VALUES ($1,CAST($2::text AS
"public"."Role")), ($3,CAST($4::text AS "public"."Role")) RETURNING
"public"."User"."id", "public"."User"."email",
"public"."User"."role"::text»
params [const(String("user.1737556028164@prisma.io")), const(String("user")),
const(String("user.1737556028165@prisma.io")), const(String("user"))]
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ source: query-compiler/query-compiler/tests/queries.rs
expression: pretty
input_file: query-compiler/query-compiler/tests/data/create-many.json
---
transaction
dataMap affectedRows
execute «INSERT INTO "public"."User" ("email","role") VALUES
($1,CAST($2::text AS "public"."Role")), ($3,CAST($4::text AS
"public"."Role"))»
params [const(String("user.1737556028164@prisma.io")), const(String("user")),
const(String("user.1737556028165@prisma.io")), const(String("user"))]
dataMap affectedRows
execute «INSERT INTO "public"."User" ("email","role") VALUES ($1,CAST($2::text
AS "public"."Role")), ($3,CAST($4::text AS "public"."Role"))»
params [const(String("user.1737556028164@prisma.io")), const(String("user")),
const(String("user.1737556028165@prisma.io")), const(String("user"))]
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
source: query-compiler/query-compiler/tests/queries.rs
expression: pretty
input_file: query-compiler/query-compiler/tests/data/update-many.json
---
dataMap affectedRows
execute «UPDATE "public"."User" SET "role" = CAST($1::text AS "public"."Role")
WHERE "public"."User"."email" = $2»
params [const(String("admin")), const(String("user@prisma.io"))]
Loading