n8n-io/n8n · error · TypeORMError

Nested CTEs aren't supported (CTE: ${cte.alias})

Error message

Nested CTEs aren't supported (CTE: ${cte.alias})

What it means

createCteExpression() throws when a CTE's queryBuilder itself has common table expressions (cte.queryBuilder.hasCommonTableExpressions()). SQL does not allow a WITH clause inside the body of another WITH clause entry; each CTE must be a flat sibling. TypeORM enforces this at query build time rather than emitting invalid SQL.

Source

Thrown at packages/@n8n/typeorm/src/query-builder/QueryBuilder.ts:1013

				return '(' + condition.parameters.join(' OR ') + ')';
		}

		throw new TypeError(`Unsupported FindOperator ${FindOperator.constructor.name}`);
	}

	protected createCteExpression(): string {
		if (!this.hasCommonTableExpressions()) {
			return '';
		}
		const databaseRequireRecusiveHint =
			this.connection.driver.cteCapabilities.requiresRecursiveHint;

		const cteStrings = this.expressionMap.commonTableExpressions.map((cte) => {
			const cteBodyExpression =
				typeof cte.queryBuilder === 'string' ? cte.queryBuilder : cte.queryBuilder.getQuery();
			if (typeof cte.queryBuilder !== 'string') {
				if (cte.queryBuilder.hasCommonTableExpressions()) {
					throw new TypeORMError(`Nested CTEs aren't supported (CTE: ${cte.alias})`);
				}
				if (
					!this.connection.driver.cteCapabilities.writable &&
					!InstanceChecker.isSelectQueryBuilder(cte.queryBuilder)
				) {
					throw new TypeORMError(
						`Only select queries are supported in CTEs in ${this.connection.options.type} (CTE: ${cte.alias})`,
					);
				}
				this.setParameters(cte.queryBuilder.getParameters());
			}
			let cteHeader = this.escape(cte.alias);
			if (cte.options.columnNames) {
				const escapedColumnNames = cte.options.columnNames.map((column) => this.escape(column));
				if (InstanceChecker.isSelectQueryBuilder(cte.queryBuilder)) {
					if (
						cte.queryBuilder.expressionMap.selects.length &&
						cte.options.columnNames.length !== cte.queryBuilder.expressionMap.selects.length

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Flatten all CTEs into a single WITH list on the outermost QueryBuilder; do not chain CTE-bearing builders as CTE bodies.
  2. If a CTE body needs another CTE, hoist that inner CTE to the outer builder's addCommonTableExpression list.
  3. Use raw SQL strings for CTE bodies if composition is complex, ensuring no nested WITH.
  4. Refactor shared subqueries into separate top-level CTEs referenced by name.

Example fix

// before
const inner = dataSource.createQueryBuilder().select(...)
  .addCommonTableExpression(deeperQb, 'deep');
const outer = dataSource.createQueryBuilder().select(...)
  .addCommonTableExpression(inner, 'inner'); // throws: nested CTE

// after — flatten
const outer = dataSource.createQueryBuilder().select(...)
  .addCommonTableExpression(deeperQb, 'deep')
  .addCommonTableExpression(innerFlatQb, 'inner');
Defensive patterns

Strategy: validation

Validate before calling

function isFlatCteBody(qb: { hasCommonTableExpressions: () => boolean }): boolean {
  return !qb.hasCommonTableExpressions();
}
if (!isFlatCteBody(bodyQb)) throw new Error('CTE body must not itself contain CTEs; flatten them to the outer builder');

Type guard

function isFlatCteBody(qb: { hasCommonTableExpressions: () => boolean }): qb is { hasCommonTableExpressions: () => false } {
  return !qb.hasCommonTableExpressions();
}

Prevention

When it happens

Trigger: Calling .addCommonTableExpression(qbA, 'a') where qbA was itself built with .addCommonTableExpression(qbB, 'b'). The inner CTE nests inside the outer, which the database would reject.

Common situations: Composing reusable QueryBuilders that each add their own CTEs and then embedding one inside another's CTE list. Refactoring a flat CTE list into a nested builder by mistake.

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/592ce051b3bf9259. Report an issue: GitHub.