Skip to main content
doc0blade47/drizzle-ormdocumentation home
Sign in

Relational Query Engine

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

The Relational Query Engine serves as Drizzle's high-level declarative query layer, translating nested TypeScript query definitions into optimized, dialect-specific SQL execution plans across multiple database engines. It solves the complexity of querying graph-shaped relational data structures by bridging typed schema definitions with underlying driver sessions through dedicated query builder classes like PgRelationalQuery, MySqlRelationalQuery, GelRelationalQuery, SingleStoreRelationalQuery, and SQLiteRelationalQuery. Key architectural decisions include encapsulating query promises and execution lifecycles, supporting nested filtering, pagination, and sorting via findMany and findFirst, and normalizing graph relations across primary and non-primary key constraints. It interacts closely with underlying database dialects to construct lateral subqueries, manage table aliases, apply selection masks, and execute row-mapping transformations on fetched database records.

Sources: drizzle-orm/src/pg-core/query-builders/query.ts:19-110, drizzle-orm/src/mysql-core/query-builders/query.ts:23-74, drizzle-orm/src/gel-core/query-builders/query.ts:19-63, drizzle-orm/src/singlestore-core/query-builders/query.ts:22-70, drizzle-orm/src/sqlite-core/query-builders/query.ts:22-96

Relation Declarations and Metadata Modeling

Schema Definition and Helpers

The relational metadata layer begins with public schema definition helpers and typed relation descriptors. The relations function accepts a table and a callback yielding a record of Relation objects, automatically binding each entry to its field name via value.withFieldName(key). The helpers createOne and createMany construct One and Many relation descriptors respectively, where createOne evaluates nullability by inspecting whether all component fields have notNull: true.

export function relations<
	TTableName extends string,
	TRelations extends Record<string, Relation<any>>,
>(
	table: AnyTable<{ name: TTableName }>,
	relations: (helpers: TableRelationsHelpers<TTableName>) => TRelations,
): Relations<TTableName, TRelations> {
	return new Relations<TTableName, TRelations>(
		table,
		(helpers: TableRelationsHelpers<TTableName>) =>
			Object.fromEntries(
				Object.entries(relations(helpers)).map(([key, value]) => [
					key,
					value.withFieldName(key),
				]),
			) as TRelations,
	);
}

Sources: drizzle-orm/src/relations.ts:502-519

The engine extracts complete relational schemas using extractTablesRelationalConfig, which iterates over raw schema entries, normalizes single-default schema wrappers, and resolves forward references through relationsBuffer when relations are declared before their target tables.

Configuration FieldTypePurpose
tsNamestringTypeScript schema object key for the table
dbNamestringPhysical database table identifier name
schemastring | undefinedOptional database schema namespace
columnsRecord<string, AnyColumn>Mapped column definitions
relationsRecord<string, Relation>Registered table relations
primaryKeyAnyColumn[]Resolved primary key column array

Sources: drizzle-orm/src/relations.ts:416-500

Note

When schema entries contain a single 'default' property holding a nested record, extractTablesRelationalConfig automatically unwraps it before iterating over table and relation definitions. Sources: drizzle-orm/src/relations.ts

Graph Relation Normalization

Relation graph normalization resolves mapping fields between source and target tables when explicit join configurations are omitted. The normalizeRelation function inspects whether a relation is an instance of One containing an explicit RelationConfig. If explicit fields and references are present, they are returned directly. Otherwise, the engine looks up the referenced table configuration in the schema and searches for matching reverse relations.

export function normalizeRelation(
	schema: TablesRelationalConfig,
	tableNamesMap: Record<string, string>,
	relation: Relation,
): NormalizedRelation {
	if (is(relation, One) && relation.config) {
		return {
			fields: relation.config.fields,
			references: relation.config.references,
		};
	}
...

Sources: drizzle-orm/src/relations.ts:559-569

Reverse relation matching evaluates either matching explicit relationName properties or linking back via referencedTable === relation.sourceTable. If multiple reverse relations match without a disambiguating name, an error is thrown. When a single reverse One relation is found, its fields and references are inverted to establish the normalized connection path.

Warning

Omitting an explicit relationName when multiple bidirectional relations exist between the same two tables causes normalizeRelation to throw an ambiguous match error. Sources: drizzle-orm/src/relations.ts

Relational Query Builder Classes

Relational Query Builder Construction and Methods

Dialect-specific relational query builders coordinate schema definitions, table mappings, and session handlers to instantiate executable queries. Each dialect exports a RelationalQueryBuilder class that exposes findMany and findFirst methods accepting type-safe configuration objects restricted by KnownKeysOnly. The findFirst method wraps the query configuration by setting limit: 1 and marks the internal execution mode as 'first', whereas findMany defaults to 'many'.

Dialect ModuleBuilder Class NameQuery Runner Class NameSession Type
PostgreSQL (pg-core)RelationalQueryBuilderPgRelationalQueryPgSession
MySQL (mysql-core)RelationalQueryBuilderMySqlRelationalQueryMySqlSession
Gel (gel-core)RelationalQueryBuilderGelRelationalQueryGelSession
SingleStore (singlestore-core)RelationalQueryBuilderSingleStoreRelationalQuerySingleStoreSession
SQLite (sqlite-core)RelationalQueryBuilderSQLiteRelationalQuery / SQLiteSyncRelationalQuerySQLiteSession

Sources: drizzle-orm/src/pg-core/query-builders/query.ts:19-63, drizzle-orm/src/mysql-core/query-builders/query.ts:23-74, drizzle-orm/src/gel-core/query-builders/query.ts:19-63, drizzle-orm/src/singlestore-core/query-builders/query.ts:22-70, drizzle-orm/src/sqlite-core/query-builders/query.ts:22-96

findMany<TConfig extends DBQueryConfig<'many', true, TSchema, TFields>>(
	config?: KnownKeysOnly<TConfig, DBQueryConfig<'many', true, TSchema, TFields>>,
): PgRelationalQuery<BuildQueryResult<TSchema, TFields, TConfig>[]> {
	return new PgRelationalQuery(
		this.fullSchema,
		this.schema,
		this.tableNamesMap,
		this.table,
		this.tableConfig,
		this.dialect,
		this.session,
		config ? (config as DBQueryConfig<'many', true>) : {},
		'many',
	);
}
 
findFirst<TSelection extends Omit<DBQueryConfig<'many', true, TSchema, TFields>, 'limit'>>(
	config?: KnownKeysOnly<TSelection, Omit<DBQueryConfig<'many', true, TSchema, TFields>, 'limit'>>,
): PgRelationalQuery<BuildQueryResult<TSchema, TFields, TSelection> | undefined> {
	return new PgRelationalQuery(
		this.fullSchema,
		this.schema,
		this.tableNamesMap,
		this.table,
		this.tableConfig,
		this.dialect,
		this.session,
		config ? { ...(config as DBQueryConfig<'many', true> | undefined), limit: 1 } : { limit: 1 },
		'first',
	);
}

Sources: drizzle-orm/src/pg-core/query-builders/query.ts:32-62

Note

In MySQL and SingleStore builders, relational query classes are parameterized by higher-kinded types (PreparedQueryHKTBase) to allow engines to generate dialect-specific prepared query handles. Sources: drizzle-orm/src/mysql-core/query-builders/query.ts, drizzle-orm/src/singlestore-core/query-builders/query.ts

Execution Lifecycle and Query Compilation

Relational query runner classes inherit from QueryPromise and implement query compilation, statement preparation, and row mapping. When .execute() or .prepare() is invoked, the runner calls an internal compilation method such as _toSQL() or _getQuery() which delegates to the dialect to build the underlying SQL abstract syntax tree (AST). For instance, PostgreSQL and Gel invoke dialect.buildRelationalQueryWithoutPK, whereas MySQL checks the connection mode (planetscale) to determine whether to build queries without lateral subqueries.

private _getQuery() {
	const query = this.mode === 'planetscale'
		? this.dialect.buildRelationalQueryWithoutLateralSubqueries({
			fullSchema: this.fullSchema,
			schema: this.schema,
			tableNamesMap: this.tableNamesMap,
			table: this.table,
			tableConfig: this.tableConfig,
			queryConfig: this.config,
			tableAlias: this.tableConfig.tsName,
		})
		: this.dialect.buildRelationalQuery({
			fullSchema: this.fullSchema,
			schema: this.schema,
			tableNamesMap: this.tableNamesMap,
			table: this.table,
			tableConfig: this.tableConfig,
			queryConfig: this.config,
			tableAlias: this.tableConfig.tsName,
		});
	return query;
}

Sources: drizzle-orm/src/mysql-core/query-builders/query.ts:114-135

Once compiled into SQL, the query is passed to the session via prepareQuery (or prepareOneTimeQuery in SQLite). The session callback processes raw database rows by executing mapRelationalRow, passing the schema table configuration, row selection mask, and column mapping utility. If the query execution mode is set to 'first', the result mapper extracts and returns the first element (rows[0]), otherwise returning the full array.

	(rawRows, mapColumnValue) => {
		const rows = rawRows.map((row) =>
			mapRelationalRow(this.schema, this.tableConfig, row, query.selection, mapColumnValue)
		);
		if (this.mode === 'first') {
			return rows[0] as TResult;
		}
		return rows as TResult;
	},
);

Sources: drizzle-orm/src/pg-core/query-builders/query.ts:99-108

Warning

SQLite provides separate asynchronous (SQLiteRelationalQuery) and synchronous (SQLiteSyncRelationalQuery) classes where synchronous executions bypass promises and immediately invoke .get() or .all() through executeRaw(). Sources: drizzle-orm/src/sqlite-core/query-builders/query.ts

Lateral Joins and Subquery Compilation

Dialect-Specific Subquery Strategies

Different database engines impose distinct constraints on how relational joins and nested subqueries are compiled. Drizzle accommodates these engine-specific capabilities by delegating query construction to dialect methods tailored to each backend's support for lateral subqueries, correlated references, and table primary key requirements.

private _getQuery() {
	const query = this.mode === 'planetscale'
		? this.dialect.buildRelationalQueryWithoutLateralSubqueries({
			fullSchema: this.fullSchema,
			schema: this.schema,
			tableNamesMap: this.tableNamesMap,
			table: this.table,
			tableConfig: this.tableConfig,
			queryConfig: this.config,
			tableAlias: this.tableConfig.tsName,
		})
		: this.dialect.buildRelationalQuery({
			fullSchema: this.fullSchema,
			schema: this.schema,
			tableNamesMap: this.tableNamesMap,
			table: this.table,
			tableConfig: this.tableConfig,
			queryConfig: this.config,
			tableAlias: this.tableConfig.tsName,
		});
	return query;
}

Sources: drizzle-orm/src/mysql-core/query-builders/query.ts:114-135

As shown in the MySQL relational query builder, the dialect branch inspection explicitly checks whether the execution target is running under PlanetScale mode. When this.mode === 'planetscale', the query builder invokes buildRelationalQueryWithoutLateralSubqueries instead of standard buildRelationalQuery. This branching is required because serverless MySQL hosting environments often lack full lateral join support or enforce restrictions on correlated subquery constructs.

Keyless and Specialized Compilation Methods

Beyond MySQL's conditional dispatch, PostgreSQL and Gel employ buildRelationalQueryWithoutPK to handle relational queries where primary key assumptions or table structures require specialized AST generation.

private _getQuery() {
	return this.dialect.buildRelationalQueryWithoutPK({
		fullSchema: this.fullSchema,
		schema: this.schema,
		tableNamesMap: this.tableNamesMap,
		table: this.table,
		tableConfig: this.tableConfig,
		queryConfig: this.config,
		tableAlias: this.tableConfig.tsName,
	});
}

Sources: drizzle-orm/src/pg-core/query-builders/query.ts:116-126

Dialect / CoreTarget MethodBranch Condition / Purpose
PostgreSQL (pg-core)buildRelationalQueryWithoutPKCompiles relational queries without relying on explicit primary key column lookups in subquery correlations.
MySQL (mysql-core)buildRelationalQueryWithoutLateralSubqueriesExecuted when mode === 'planetscale' to avoid lateral join features unsupported by PlanetScale.
MySQL (mysql-core)buildRelationalQueryStandard MySQL path utilizing lateral subqueries and advanced join syntax.
SingleStore (singlestore-core)buildRelationalQueryStandard SingleStore path for relational query compilation.
SQLite (sqlite-core)buildRelationalQueryStandard SQLite path compiling relational selections into AST structures.

Sources: drizzle-orm/src/pg-core/query-builders/query.ts:116-126, drizzle-orm/src/mysql-core/query-builders/query.ts:114-135, drizzle-orm/src/gel-core/query-builders/query.ts:116-126, drizzle-orm/src/singlestore-core/query-builders/query.ts:109-119, drizzle-orm/src/sqlite-core/query-builders/query.ts:129-138

Caution

Attempting to execute standard lateral-join-based relational queries against a PlanetScale MySQL instance will fail at runtime unless the engine mode is explicitly configured to 'planetscale', which triggers buildRelationalQueryWithoutLateralSubqueries. Sources: drizzle-orm/src/mysql-core/query-builders/query.ts

Selection Masking and Dynamic Aliasing

Selection Proxy Wrapping and Proxy Handlers

Selection masking and dynamic aliasing in Drizzle are governed by SelectionProxyHandler, which implements the JavaScript ProxyHandler interface across subqueries, record sets, and database views. When properties are accessed on a selection proxy, the handler intercepts symbol and string lookups to manage _ metadata configurations, view configurations, and column selections.

get(subquery: T, prop: string | symbol): any {
	if (prop === '_') {
		return {
			...subquery['_' as keyof typeof subquery],
			selectedFields: new Proxy(
				(subquery as Subquery)._.selectedFields,
				this as ProxyHandler<Record<string, unknown>>,
			),
		};
	}
 
	if (prop === ViewBaseConfig) {
		return {
			...subquery[ViewBaseConfig as keyof typeof subquery],
			selectedFields: new Proxy(
				(subquery as View)[ViewBaseConfig].selectedFields,
				this as ProxyHandler<Record<string, unknown>>,
			),
		};
	}

Sources: drizzle-orm/src/selection-proxy.ts:47-66

The proxy handler evaluates accessed properties to determine whether they represent stored columns, raw SQL expressions, aliased SQL chunks, or nested objects. When a column is encountered and an alias is configured, the handler wraps the column and its underlying table within ColumnAliasProxyHandler and TableAliasProxyHandler respectively.

if (is(value, Column)) {
	if (this.config.alias) {
		return new Proxy(
			value,
			new ColumnAliasProxyHandler(
				new Proxy(
					value.table,
					new TableAliasProxyHandler(this.config.alias, this.config.replaceOriginalName ?? false),
				),
			),
		);
	}
	return value;
}

Sources: drizzle-orm/src/selection-proxy.ts:100-113

Column Projection Scoping and SQL Behavior

The configuration options accepted by SelectionProxyHandler control how raw SQL expressions and aliased fields behave during query building and view generation.

Configuration KeyTypeDefault / OptionsPurpose
aliasstringundefinedTable alias applied to intercepted columns during proxy wrapping.
sqlAliasedBehavior'sql' | 'alias'RequiredDetermines whether SQL.Aliased returns the underlying expression ('sql') or the field alias ('alias').
sqlBehavior'sql' | 'error'RequiredDetermines whether unaliased raw SQL expressions return the expression or throw a type/runtime error.
replaceOriginalNamebooleanfalseReplaces the original column name with its alias, primarily used when creating database views.

Sources: drizzle-orm/src/selection-proxy.ts:13-41

Warning

Accessing a raw SQL field from a subquery that lacks an explicit alias will throw an error at runtime and evaluate to a Drizzle type-level error when sqlBehavior is set to 'error'. Sources: drizzle-orm/src/selection-proxy.ts:90-98

Note

SQL.Aliased instances that originated inside a prior subquery carry isSelectionField: true, preventing the proxy handler from mistakenly returning raw underlying SQL expressions when sqlAliasedBehavior is set to 'sql'. Sources: drizzle-orm/src/selection-proxy.ts:79-88

Result Hydration and Row Mapping

Relational Row Transformation and Mapping Lifecycles

After database sessions execute compiled relational queries, raw tabular rows returned by drivers require structural reshaping into nested object graphs matching the user's schema and selection configuration. Across PostgreSQL, MySQL, SQLite, SingleStore, and Gel dialects, this post-query phase is driven by mapRelationalRow, which processes raw row data and column value mappers provided by underlying database drivers.

(rawRows, mapColumnValue) => {
	const rows = rawRows.map((row) =>
		mapRelationalRow(this.schema, this.tableConfig, row, query.selection, mapColumnValue)
	);
	if (this.mode === 'first') {
		return rows[0] as TResult;
	}
	return rows as TResult;
},

Sources: drizzle-orm/src/pg-core/query-builders/query.ts:99-107

For asynchronous and synchronous session configurations, the execution pipeline wraps the raw rows mapper inside prepared query definitions. Depending on whether the query mode is configured as 'many' or 'first', the result handler returns either an array of fully hydrated entities or extracts the initial index element.

(rawRows, mapColumnValue) => {
	const rows = rawRows.map((row) =>
		mapRelationalRow(this.schema, this.tableConfig, row, query.selection, mapColumnValue)
	);
	if (this.mode === 'first') {
		return rows[0] as TResult;
	}
	return rows as TResult;
},

Sources: drizzle-orm/src/sqlite-core/query-builders/query.ts:152-160

Execution and Preparation Modalities

Different query runner implementations manage row mapping through dialect-specific prepared statements and query promises.

Dialect Runner ClassMethod / HookMode ParameterReturn Processing
PgRelationalQuery_prepare'many' | 'first'Maps raw rows with mapRelationalRow, slicing index 0 for 'first'.
MySqlRelationalQueryprepare'many' | 'first'Iterates over rawRows via mapRelationalRow without column value mappers.
SQLiteRelationalQuery_prepare'many' | 'first'Dispatches to session prepareQuery or prepareOneTimeQuery with driver mappers.
GelRelationalQuery_prepare'many' | 'first'Executes via Gel session wrapper, applying active telemetry span tracing.
SingleStoreRelationalQueryprepare'many' | 'first'Processes result sets via SingleStore session query preparation layer.

Sources: drizzle-orm/src/pg-core/query-builders/query.ts:90-108, drizzle-orm/src/mysql-core/query-builders/query.ts:99-111, drizzle-orm/src/sqlite-core/query-builders/query.ts:142-161, drizzle-orm/src/gel-core/query-builders/query.ts:90-108, drizzle-orm/src/singlestore-core/query-builders/query.ts:94-106

Note

PostgreSQL, Gel, and SQLite sessions pass a secondary mapColumnValue callback into mapRelationalRow, enabling custom type decoding per column during row deserialization. Sources: drizzle-orm/src/pg-core/query-builders/query.ts:99-102, drizzle-orm/src/gel-core/query-builders/query.ts:99-102, drizzle-orm/src/sqlite-core/query-builders/query.ts:152-155

Warning

Selecting findFirst on any relational query builder forces the row transformation pipeline to truncate the hydrated array output, returning undefined if no records match the criteria. Sources: drizzle-orm/src/pg-core/query-builders/query.ts:48-62, drizzle-orm/src/sqlite-core/query-builders/query.ts:69-95

Was this page helpful?