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

SQL Template Engine

Last updated Oct 12, 2026100% grounded
Relevant source files

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

Overview

The SQL template engine in Drizzle ORM provides a powerful programmatic foundation for constructing typesafe SQL queries, expressions, and database operations through tagged template literals and abstract syntax tree (AST) nodes. It solves the complexity of dynamic query composition by turning nested SQL chunks, parameters, tables, columns, and subqueries into structured trees that compile cleanly into driver-ready queries with proper parameter binding and escaping. Core classes like SQL, StringChunk, and Param manage query structures, while helper functions for conditions, aggregates, and vector operations enable expressive query building across dialects.

Sources: drizzle-orm/src/sql/sql.ts:1-73, drizzle-orm/src/sql/sql.ts:89-130, drizzle-orm/src/sql/expressions/conditions.ts:1-35, drizzle-orm/src/sql/functions/aggregate.ts:1-21, drizzle-orm/src/sql/functions/vector.ts:1-39

SQL Chunks and AST Structure

Chunk Union and AST Node Types

The core AST representation revolves around the Chunk union type and the SQL class, which hold fragments, metadata, and nested child nodes. Every part of a compiled query starts as a chunk or a wrapper implementing the SQLWrapper interface.

The Chunk type defines all valid nodes that can be assembled inside an SQL statement tree. These include raw string pieces, database tables, views, columns, raw name identifiers, parameters, placeholders, and nested SQL instances.

export type Chunk =
	| string
	| Table
	| View
	| AnyColumn
	| Name
	| Param
	| Placeholder
	| SQL;

Sources: drizzle-orm/src/sql/sql.ts:22-30

Any value or schema model implementing the getSQL() method satisfies the SQLWrapper interface, allowing custom builders and database constructs to integrate directly into query compilation trees.

export interface SQLWrapper {
	getSQL(): SQL;
	shouldOmitSQLParens?(): boolean;
}

Sources: drizzle-orm/src/sql/sql.ts:65-68

The table below lists the core AST classes and wrappers, their entity kinds, and their runtime roles during query generation.

Class / InterfaceEntity KindRole in AST
SQL'SQL'Root or nested container holding an array of SQLChunk instances, track-used tables, and driver encoders.
StringChunk'StringChunk'Holds raw literal SQL string arrays that require no escaping or parameterization.
SQLWrapperN/A (Interface)Protocol implemented by entities capable of returning an internal SQL AST via getSQL().
FakePrimitiveParam'FakePrimitiveParam'Type-level marker indicating a primitive parameter used in templates; never instantiated at runtime.

Sources: drizzle-orm/src/sql/sql.ts:18-20, drizzle-orm/src/sql/sql.ts:89-96, drizzle-orm/src/sql/sql.ts:103-110

Note

FakePrimitiveParam exists strictly at the TypeScript type level to represent primitive parameter values within sql template tags, remaining completely uninstantiated during runtime execution.

Sources: drizzle-orm/src/sql/sql.ts:13-17

SQL Container and Used Tables Tracking

The SQL class manages an array of query chunks (queryChunks: SQLChunk[]). Upon instantiation, the constructor inspects each chunk; if the chunk is a Table instance, it extracts schema prefixes and table names, registering them in the usedTables array for reference tracking.

	constructor(readonly queryChunks: SQLChunk[]) {
		for (const chunk of queryChunks) {
			if (is(chunk, Table)) {
				const schemaName = chunk[Table.Symbol.Schema];
 
				this.usedTables.push(
					schemaName === undefined
						? chunk[Table.Symbol.Name]
						: schemaName + '.' + chunk[Table.Symbol.Name],
				);
			}
		}
	}

Sources: drizzle-orm/src/sql/sql.ts:118-130

Important

When compiling subqueries or nested expressions, SQL instances propagate their shouldInlineParams flag down the tree during recursive query building to override default parameter placeholder behavior.

Sources: drizzle-orm/src/sql/sql.ts:188-193

Template Tag and Parameter Binding

Query Configuration and Typings Values

Query building configurations dictate how placeholders, parameters, escape sequences, and casing rules are processed during AST compilation. The BuildQueryConfig interface provides the required callbacks and state containers for name escaping, parameter formatting, string literals, casing caches, and index generation contexts.

export interface BuildQueryConfig {
	casing: CasingCache;
	escapeName(name: string): string;
	escapeParam(num: number, value: unknown): string;
	escapeString(str: string): string;
	prepareTyping?: (encoder: DriverValueEncoder<unknown, unknown>) => QueryTypingsValue;
	paramStartIndex?: { value: number };
	inlineParams?: boolean;
	invokeSource?: 'indexes' | undefined;
}

Sources: drizzle-orm/src/sql/sql.ts:32-41

Supported driver typing annotations determine how database parameter values are serialized or cast across driver boundaries. The table below outlines every allowed QueryTypingsValue constant recognized by the query compiler.

Query Typing ValueDescription / Driver Mapping
'json'Indicates JSON-encoded columns or payload structures.
'decimal'Represents arbitrary-precision numeric decimal types.
'time'Designates time-of-day temporal values.
'timestamp'Represents combined date and time stamps.
'uuid'Designates universally unique identifier types.
'date'Represents calendar date values.
'none'Default untyped driver parameter binding.

Sources: drizzle-orm/src/sql/sql.ts:43-43

Note

FakePrimitiveParam serves as a type-level marker representing primitive parameters inside template string literals without incurring runtime instantiation overhead.

Sources: drizzle-orm/src/sql/sql.ts:13-17

Parameter Binding and Inline Mapping

Parameter processing inspects chunks during query building to determine whether values should be bound via driver parameters or serialized directly into the SQL text string. When inlineParams is active, or when dealing with unmapped parameters, values are converted via mapInlineParam.

	private mapInlineParam(
		chunk: unknown,
		{ escapeString }: BuildQueryConfig,
	): string {
		if (chunk === null) {
			return 'null';
		}
		if (typeof chunk === 'number' || typeof chunk === 'boolean') {
			return chunk.toString();
		}
		if (typeof chunk === 'string') {
			return escapeString(chunk);
		}
		if (typeof chunk === 'object') {
			const mappedValueAsString = chunk.toString();
			if (mappedValueAsString === '[object Object]') {
				return escapeString(JSON.stringify(chunk));
			}
			return escapeString(mappedValueAsString);
		}
		throw new Error('Unexpected param value: ' + chunk);
	}

Sources: drizzle-orm/src/sql/sql.ts:302-323

Warning

Object parameters whose toString() representation evaluates to [object Object] are automatically serialized via JSON.stringify before string escaping when parameter inlining is enabled.

Sources: drizzle-orm/src/sql/sql.ts:315-321

Query Resolution and Placeholder Substitution

Query Building Control Flow

The buildQueryFromSourceParams method processes arrays of query chunks recursively, dispatching each node type through specialized serialization branches. Telemetry spans wrap query generation via tracer.startActiveSpan, emitting query text and parameters to tracing diagnostics.

	toQuery(config: BuildQueryConfig): QueryWithTypings {
		return tracer.startActiveSpan('drizzle.buildSQL', (span) => {
			const query = this.buildQueryFromSourceParams(this.queryChunks, config);
			span?.setAttributes({
				'drizzle.query.text': query.sql,
				'drizzle.query.params': JSON.stringify(query.params),
			});
			return query;
		});
	}

Sources: drizzle-orm/src/sql/sql.ts:137-146

Important

When query chunks contain arrays, the compiler automatically parenthesizes the items and joins them with commas, constructing an expanded chunk array before re-invoking buildQueryFromSourceParams.

Sources: drizzle-orm/src/sql/sql.ts:176-186

Chunk Serialization Handlers

Different AST chunk types trigger distinct formatting rules during query generation, handling identifiers, schemas, parameters, and wrapper structures.

Chunk TypeSerialization Behavior
StringChunkConcatenates literal string segments with empty parameter arrays.
NameEscapes identifier names using escapeName.
TableFormats schema-qualified table names or bare names when aliased.
ColumnResolves column casing via casing.getColumnCasing and prefixes table identifiers unless queried within indexes.
ViewFormats schema-qualified view identifiers or alias names.
ParamBinds parameters through escapeParam, tracking parameter indices and driver encoders.
PlaceholderGenerates parameter placeholders using the active paramStartIndex.
SubqueryFormats inline subqueries with parentheses and aliases, or referenced CTE names when isWith is set.

Sources: drizzle-orm/src/sql/sql.ts:164-282

Note

If a Param chunk contains a nested SQL instance after encoder mapping, the compiler unwraps and processes it recursively via buildQueryFromSourceParams.

Sources: drizzle-orm/src/sql/sql.ts:238-242

Comparison and Logical Expressions

Parameter Binding and Condition Operators

Condition tree constructors leverage template literals and helper functions defined in drizzle-orm/src/sql/expressions/conditions.ts to build SQL AST nodes. The bindIfParam function inspects values against column encoders, wrapping raw values into Param instances when appropriate while leaving columns, tables, views, placeholders, parameters, and SQL wrappers unhindered.

export function bindIfParam(value: unknown, column: SQLWrapper): SQLChunk {
	if (
		isDriverValueEncoder(column)
		&& !isSQLWrapper(value)
		&& !is(value, Param)
		&& !is(value, Placeholder)
		&& !is(value, Column)
		&& !is(value, Table)
		&& !is(value, View)
	) {
		return new Param(value, column);
	}
	return value as SQLChunk;
}

Sources: drizzle-orm/src/sql/expressions/conditions.ts:17-30

Binary operators construct equality, inequality, and directional comparisons using sql`` template tags combined with bindIfParam`.

Operator FunctionSQL Expression GeneratedDescription
eq${left} = ${bindIfParam(right, left)}Tests equality between two expressions.
ne${left} <> ${bindIfParam(right, left)}Tests inequality between two expressions.
gt${left} > ${bindIfParam(right, left)}Tests strictly greater-than.
gte${left} >= ${bindIfParam(right, left)}Tests greater-than or equal-to.
lt${left} < ${bindIfParam(right, left)}Tests strictly less-than.
lte${left} <= ${bindIfParam(right, left)}Tests less-than or equal-to.

Sources: drizzle-orm/src/sql/expressions/conditions.ts:62-86, drizzle-orm/src/sql/expressions/conditions.ts:195-253

Warning

The SQL standard specifies that two NULL values are not equal. To check for NULL values, developers should use isNull instead of eq(column, null).

Sources: drizzle-orm/src/sql/expressions/conditions.ts:46-51

Logical and Array Membership Combinators

Logical combinators filter out undefined conditions dynamically before assembling expression trees. If an and or or call receives an empty list of valid conditions, it returns undefined. If it receives a single valid condition, it returns that SQL instance directly without wrapping parentheses.

export function and(
	...unfilteredConditions: (SQLWrapper | undefined)[]
): SQL | undefined {
	const conditions = unfilteredConditions.filter(
		(c): c is Exclude<typeof c, undefined> => c !== undefined,
	);
 
	if (conditions.length === 0) {
		return undefined;
	}
 
	if (conditions.length === 1) {
		return new SQL(conditions);
	}
 
	return new SQL([
		new StringChunk('('),
		sql.join(conditions, new StringChunk(' and ')),
		new StringChunk(')'),
	]);
}

Sources: drizzle-orm/src/sql/expressions/conditions.ts:105-125

Negation, array membership, and null checks complete the condition toolkit. The not function prefixes any expression with the not keyword. Array checks handle empty arrays specially for notInArray, returning a literal true expression when no values are provided.

export function notInArray(
	column: SQLWrapper,
	values: (unknown | Placeholder)[] | SQLWrapper,
): SQL {
	if (Array.isArray(values)) {
		if (values.length === 0) {
			return sql`true`;
		}
		return sql`${column} not in ${values.map((v) => bindIfParam(v, column))}`;
	}
 
	return sql`${column} not in ${bindIfParam(values, column)}`;
}

Sources: drizzle-orm/src/sql/expressions/conditions.ts:322-334

Note

Passing an empty array to notInArray evaluates to true to preserve valid SQL syntax when filtering dynamically.

Sources: drizzle-orm/src/sql/expressions/conditions.ts:326-329

Dialect Expressions and String Functions

Dialect Expression Helpers and String Operations

Dialect-specific modules provide specialized string manipulation helpers that integrate driver parameter binding with database-native string syntax. Both the Gel and SingleStore core modules export custom concat and substring functions that accept column references or aliased fields along with string values, placeholders, or wrapped SQL fragments.

export function concat(column: GelColumn | SQL.Aliased, value: string | Placeholder | SQLWrapper): SQL {
	return sql`${column} || ${bindIfParam(value, column)}`;
}
 
export function substring(
	column: GelColumn | SQL.Aliased,
	{ from, for: _for }: { from?: number | Placeholder | SQLWrapper; for?: number | Placeholder | SQLWrapper },
): SQL {
	const chunks: SQLChunk[] = [sql`substring(`, column];
	if (from !== undefined) {
		chunks.push(sql` from `, bindIfParam(from, column));
	}
	if (_for !== undefined) {
		chunks.push(sql` for `, bindIfParam(_for, column));
	}
	chunks.push(sql`)`);
	return sql.join(chunks);
}

Sources: drizzle-orm/src/gel-core/expressions.ts:8-25, drizzle-orm/src/singlestore-core/expressions.ts:8-25

The concat helper utilizes the standard concatenation operator (||) and passes the right-hand value through bindIfParam to ensure proper encoder mapping and driver parameterization. The substring helper constructs dynamic chunk lists for extraction queries, conditionally appending from and for modifiers when defined.

Note

Both Gel and SingleStore implement identical signatures and AST assembly logic for concat and substring, utilizing sql.join to merge fragmented expression chunks into valid parameterized statements.

Sources: drizzle-orm/src/gel-core/expressions.ts:8-25, drizzle-orm/src/singlestore-core/expressions.ts:8-25

Aggregate and Vector Functions

Standard Aggregate Builders

Drizzle provides standard aggregate function wrappers constructed via the sql template literal. The count function accepts an optional expression and defaults to * via sql.raw('*') when omitted, mapping its resulting type to number. Distinct variants such as countDistinct, avgDistinct, and sumDistinct prepend the distinct keyword inside the expression block.

export function count(expression?: SQLWrapper): SQL<number> {
	return sql`count(${expression || sql.raw('*')})`.mapWith(Number);
}
 
export function countDistinct(expression: SQLWrapper): SQL<number> {
	return sql`count(distinct ${expression})`.mapWith(Number);
}

Sources: drizzle-orm/src/sql/functions/aggregate.ts:19-37

The following table summarizes the available aggregate functions, their SQL template structures, and their return type mappings:

FunctionTemplate ExpressionReturn TypeSource Lines
countcount(${expression || sql.raw('*')})numberaggregate.ts:19-21
countDistinctcount(distinct ${expression})numberaggregate.ts:35-37
avgavg(${expression})string | nullaggregate.ts:51-53
avgDistinctavg(distinct ${expression})string | nullaggregate.ts:67-69
sumsum(${expression})string | nullaggregate.ts:83-85
sumDistinctsum(distinct ${expression})string | nullaggregate.ts:99-101
maxmax(${expression})Column data type or string | nullaggregate.ts:113-115
minmin(${expression})Column data type or string | nullaggregate.ts:127-129

Note

Functions like max and min dynamically inspect whether the passed expression is an instance of Column. If so, they map the result to the column's native data type; otherwise, they fall back to String.

Sources: drizzle-orm/src/sql/functions/aggregate.ts:113-115, drizzle-orm/src/sql/functions/aggregate.ts:127-129

Vector Distance Operator Abstractions

Vector distance operations combine column references or SQL wrappers with distance operators and target values. The internal helper toSql serializes array inputs via JSON.stringify, while non-array inputs are embedded directly into the template string.

function toSql(value: number[] | string[]): string {
	return JSON.stringify(value);
}
 
export function l2Distance(
	column: SQLWrapper | AnyColumn,
	value: number[] | string[] | TypedQueryBuilder<any> | string,
): SQL {
	if (Array.isArray(value)) {
		return sql`${column} <-> ${toSql(value)}`;
	}
	return sql`${column} <-> ${value}`;
}

Sources: drizzle-orm/src/sql/functions/vector.ts:5-39

The vector distance helpers and their corresponding operators are mapped below:

Function NameOperatorDescriptionSource Lines
l2Distance<->Euclidean (L2) distancevector.ts:31-39
l1Distance<+>Taxicab (L1) distancevector.ts:61-69
innerProduct<#>Negative inner productvector.ts:93-101
cosineDistance<=>Cosine distancevector.ts:125-133
hammingDistance<~>Hamming distancevector.ts:150-158
jaccardDistance<%>Jaccard distancevector.ts:170-178

Warning

Passing raw JavaScript arrays to vector distance functions triggers JSON.stringify serialization via toSql, whereas passing query builders or string expressions embeds them directly into the generated AST without JSON wrapping.

Sources: drizzle-orm/src/sql/functions/vector.ts:5-39

Was this page helpful?