Skip to main content
The Query Compiler is a core component of Prisma’s new architecture that separates query planning (Rust) from query execution (TypeScript with driver adapters).

Architecture

The Query Compiler architecture consists of three main layers:
1

Query Graph Building

GraphQL-like queries are parsed and transformed into an internal query graph representation. This graph captures the logical structure of the query including relations, filters, and data dependencies.
2

Query Planning (Rust)

The query graph is translated into an optimized expression tree (query plan) by the query-compiler crate. This happens entirely in Rust and produces a serializable plan.
3

Query Execution (TypeScript)

The expression tree is interpreted and executed in Prisma Client using driver adapters. The interpreter has no knowledge of connection strings or database specifics.

Key Components

Query Graph

The query graph is an intermediate representation that captures:
  • Nodes: Queries, computations, control flow (if/else, return)
  • Edges: Data dependencies, execution order, conditional branches
  • Dependencies: Parent-child relationships and data flow
pub enum Node {
    Query(Query),
    Empty,
    Flow(Flow),
    Computation(Computation),
}

Expression Tree

The expression tree is the final output of compilation. It’s a serializable representation that can be executed by the TypeScript interpreter.
#[derive(Debug, Serialize)]
#[serde(tag = "type", content = "args", rename_all = "camelCase")]
pub enum Expression {
    /// Database query that returns data
    Query(DbQuery),
    
    /// Database query that returns affected rows
    Execute(DbQuery),
    
    /// Sequence of statements
    Seq(Vec<Expression>),
    
    /// Lexical scope with bindings
    Let {
        bindings: Vec<Binding>,
        expr: Box<Expression>,
    },
    
    /// Application-level join
    Join {
        parent: Box<Expression>,
        children: Vec<JoinExpression>,
        can_assume_strict_equality: bool,
    },
    
    /// Conditional execution
    If {
        value: Box<Expression>,
        rule: DataRule,
        then: Box<Expression>,
        r#else: Box<Expression>,
    },
    
    // ... and more
}

Compilation Pipeline

The compilation process follows these stages:
1

Parse Query Document

Parse the incoming GraphQL-like query into a QueryDocument structure.
let request = RequestBody::try_from_str(&request, protocol)?;
let QueryDocument::Single(op) = request.into_doc(&schema)?;
2

Build Query Graph

Transform the query document into a query graph that represents dependencies and execution order.
let graph = QueryGraphBuilder::new(query_schema).build(operation)?;
3

Translate to Expression Tree

Walk the query graph and generate an optimized expression tree.
let plan = translate(graph, &query_builder)?;
4

Simplify and Optimize

Apply simplification rules to reduce redundant expressions.
plan.simplify();
5

Serialize to JSON

Convert the expression tree to JSON for transmission to TypeScript.
plan.serialize(&RESPONSE_SERIALIZER)?

Main Entry Point

The primary compilation function is straightforward:
pub fn compile(
    query_schema: &QuerySchema,
    query: Operation,
    connection_info: &ConnectionInfo,
) -> Result<Expression, CompileError> {
    let ctx = Context::new(connection_info, None);
    let graph = QueryGraphBuilder::new(query_schema).build(query)?;

    let res = match connection_info.sql_family() {
        SqlFamily::Postgres => translate(graph, &SqlQueryBuilder::<Postgres>::new(ctx)),
        SqlFamily::Mysql => translate(graph, &SqlQueryBuilder::<Mysql>::new(ctx)),
        SqlFamily::Sqlite => translate(graph, &SqlQueryBuilder::<Sqlite>::new(ctx)),
        SqlFamily::Mssql => translate(graph, &SqlQueryBuilder::<Mssql>::new(ctx)),
    };

    res.map_err(CompileError::TranslateError)
}

Dependencies

Key crates used by the query compiler:
CratePurpose
pslPrisma Schema Language parser and validator
query-structureData structures for queries and schema
query-builderSQL query building abstractions
query-coreCore query graph and document types
sql-query-builderDatabase-specific SQL generation
quaintDatabase connectivity and query execution
From query-compiler/Cargo.toml:
[dependencies]
psl.workspace = true
query-structure.workspace = true
query-builder.workspace = true
query-core.workspace = true
sql-query-builder = { workspace = true, features = ["relation_joins"]}
quaint.workspace = true

Expression Simplification

The compiler applies several optimization passes:
  • Single-element sequences: Seq([expr]) becomes expr
  • Identity let bindings: let x = e in x becomes e
  • Single-element aggregates: Concat([expr]) becomes expr
  • Nested simplification: Recursively simplifies all sub-expressions

Error Handling

Compilation can fail at multiple stages:
#[derive(Debug, Error)]
pub enum CompileError {
    #[error("only a single query can be compiled at a time")]
    UnsupportedRequest,

    #[error("failed to build query graph: {0}")]
    GraphBuildError(#[from] QueryGraphBuilderError),

    #[error("{0}")]
    TranslateError(#[from] TranslateError),
}

Design Principles

Separation of Concerns

Planning happens in Rust for performance and type safety. Execution happens in TypeScript for flexibility and ecosystem integration.

Database Agnostic Planning

The expression tree is database-agnostic. Database-specific details are handled during execution via driver adapters.

Serializable Plans

Expression trees serialize to JSON, enabling cross-language execution and potential caching.

Composable Expressions

Expressions compose naturally, enabling complex queries through simple building blocks.

Next Steps

Driver Adapters

Learn about the driver adapter system and TypeScript integration

Query Planning

Deep dive into how query planning works

WASM Build

Build the WebAssembly module for browser/edge environments

Build docs developers (and LLMs) love