Skip to Content
🔧 AdvancedSplitting Build And Execute

Last Updated: 3/20/2026


Splitting Build and Execute

Kysely is fundamentally a query builder that can also execute queries. In some architectures, you may want to separate these concerns: build queries in one part of your application and execute them elsewhere. This pattern is useful for serverless functions, microservices, or when you want to inspect and log queries before execution.

Cold Kysely Instances with DummyDriver

To use Kysely purely as a query builder without database connectivity, instantiate it with the DummyDriver class. This driver implements the Driver interface but performs no actual database operations.

import { DummyDriver, Kysely, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler, } from 'kysely' interface Database { person: { id: number first_name: string last_name: string | null } } const db = new Kysely<Database>({ dialect: { createAdapter: () => new PostgresAdapter(), createDriver: () => new DummyDriver(), createIntrospector: (db) => new PostgresIntrospector(db), createQueryCompiler: () => new PostgresQueryCompiler(), }, })

This “cold” instance compiles queries to PostgreSQL syntax but cannot execute them. The DummyDriver class (defined in src/driver/dummy-driver.ts) returns empty results from its executeQuery method and does nothing in transaction methods like beginTransaction or commitTransaction.

You can create cold instances for any SQL dialect by mixing the appropriate adapter, introspector, and query compiler. For MySQL, use MysqlAdapter, MysqlIntrospector, and MysqlQueryCompiler. For SQLite, use the corresponding SQLite components.

Compiling Queries to SQL

Call .compile() on any query builder to get a CompiledQuery object. This object contains the SQL string, parameters, and the internal query node tree.

const compiledQuery = db .selectFrom('person') .select(['first_name', 'last_name']) .where('id', '=', 42) .compile() console.log(compiledQuery.sql) // select "first_name", "last_name" from "person" where "id" = $1 console.log(compiledQuery.parameters) // [42]

The CompiledQuery interface (defined in src/query-compiler/compiled-query.ts) includes:

  • sql: The SQL string with placeholders
  • parameters: An array of parameter values in the order they appear in the SQL
  • query: The root operation node representing the query structure
  • queryId: A unique identifier for this query instance

You can also compile raw SQL queries:

import { sql } from 'kysely' const compiledQuery = sql<{ first_name: string }>` select first_name from person where id = ${42} `.compile(db) console.log(compiledQuery.sql) // select first_name from person where id = $1

The .compile() method requires a Kysely instance (even a cold one) because compilation depends on the dialect’s query compiler.

Type Inference for Compiled Queries

Kysely provides the InferResult utility type to extract the result type from a query builder or compiled query. This allows you to maintain type safety even when query building and execution are separated.

import { InferResult } from 'kysely' const query = db .selectFrom('person') .select(['first_name', 'last_name']) .where('id', '=', 42) type QueryResult = InferResult<typeof query> // { first_name: string; last_name: string | null }[] const compiledQuery = query.compile() type CompiledQueryResult = InferResult<typeof compiledQuery> // { first_name: string; last_name: string | null }[]

The InferResult type (defined in src/util/infer-result.ts) works with both Compilable objects (query builders) and CompiledQuery objects. It resolves the output type by examining the type parameter of the query builder or compiled query.

For insert, update, delete, and merge queries, InferResult returns an array of result objects (e.g., InsertResult[], UpdateResult[]). For select queries, it returns an array of the selected row type.

Executing Compiled Queries

A “hot” Kysely instance (one with a real driver) can execute compiled queries using the executeQuery method:

// Build the query with a cold instance const coldDb = new Kysely<Database>({ dialect: { createAdapter: () => new PostgresAdapter(), createDriver: () => new DummyDriver(), createIntrospector: (db) => new PostgresIntrospector(db), createQueryCompiler: () => new PostgresQueryCompiler(), }, }) const compiledQuery = coldDb .selectFrom('person') .select(['first_name', 'last_name']) .where('id', '=', 42) .compile() // Execute with a hot instance import { PostgresDialect } from 'kysely' import { Pool } from 'pg' const hotDb = new Kysely<Database>({ dialect: new PostgresDialect({ pool: new Pool({ connectionString: process.env.DATABASE_URL, }), }), }) const result = await hotDb.executeQuery(compiledQuery) console.log(result.rows)

The executeQuery method is defined on the Kysely class and delegates to the QueryExecutor interface (see src/query-executor/query-executor.ts). It returns a QueryResult object with:

  • rows: The result rows
  • numAffectedRows: For insert/update/delete queries
  • insertId: For insert queries on dialects that support it

This pattern is useful when you want to build queries in a stateless function (like an AWS Lambda) and execute them in a different environment, or when you want to serialize compiled queries for caching or logging.

Use Cases

Splitting build and execute is valuable in several scenarios:

Query inspection and logging: Compile queries to inspect the SQL before execution. This is useful for debugging, query performance analysis, or audit logging.

Serverless architectures: Build queries in edge functions (which may not have database access) and execute them in a backend service.

Query caching: Serialize compiled queries and cache them. When the same query is needed again, skip the compilation step.

Testing: Build queries in tests without needing a database connection. Verify that the generated SQL matches expectations.

Multi-dialect support: Build queries with one dialect’s compiler and execute them with a different driver, as long as the SQL syntax is compatible.

What’s Next

  • Dynamic Module: Learn how to build queries with runtime-determined table and column names using db.dynamic.
  • Extending Kysely: Create custom expressions and helpers using the Expression interface and sql template tag.
  • Streams: Execute queries that return large result sets using .stream() instead of .execute().