n8n-io/n8n · error · TypeORMError

cte.options.columnNames length (${cte.options.columnNames.le

Error message

cte.options.columnNames length (${cte.options.columnNames.length}) doesn't match subquery select list length ${cte.queryBuilder.expressionMap.selects.length} (CTE: ${cte.alias})

What it means

createCteExpression() throws when cte.options.columnNames length differs from cte.queryBuilder.expressionMap.selects length. A CTE with an explicit column list (WITH name (a, b) AS ...) must have exactly as many columns as the subquery's select list; the database would reject the mismatch. TypeORM checks this only for SelectQueryBuilder bodies with non-empty selects.

Source

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

				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
					) {
						throw new TypeORMError(
							`cte.options.columnNames length (${cte.options.columnNames.length}) doesn't match subquery select list length ${cte.queryBuilder.expressionMap.selects.length} (CTE: ${cte.alias})`,
						);
					}
				}
				cteHeader += `(${escapedColumnNames.join(', ')})`;
			}
			const recursiveClause =
				cte.options.recursive && databaseRequireRecusiveHint ? 'RECURSIVE' : '';
			let materializeClause = '';
			if (
				this.connection.driver.cteCapabilities.materializedHint &&
				cte.options.materialized !== undefined
			) {
				materializeClause = cte.options.materialized ? 'MATERIALIZED' : 'NOT MATERIALIZED';
			}

			return [recursiveClause, cteHeader, 'AS', materializeClause, `(${cteBodyExpression})`]
				.filter(Boolean)

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Align columnNames length with the body's select list; either drop columnNames (let the DB infer) or update both together.
  2. When using addSelect on the body, recount and update the columnNames array.
  3. Omit columnNames entirely if you don't need explicit CTE column aliases.
  4. Write a unit test asserting columnNames.length === body.expressionMap.selects.length for composed CTEs.

Example fix

// before
const body = dataSource.createQueryBuilder().select('a').addSelect('b').from(T, 't');
qb.addCommonTableExpression(body, 'cte', { columnNames: ['a', 'b', 'c'] }); // 3 != 2

// after
qb.addCommonTableExpression(body, 'cte', { columnNames: ['a', 'b'] });
Defensive patterns

Strategy: validation

Validate before calling

function cteColumnsMatch(body: { expressionMap: { selects: unknown[] } }, columnNames?: string[]): boolean {
  if (!columnNames) return true;
  if (body.expressionMap.selects.length === 0) return true;
  return columnNames.length === body.expressionMap.selects.length;
}
if (!cteColumnsMatch(bodyQb, opts.columnNames)) throw new Error('cte columnNames length must equal select list length');

Type guard

function cteColumnsAligned(body: { expressionMap: { selects: unknown[] } }, columnNames?: string[]): boolean {
  if (!columnNames || body.expressionMap.selects.length === 0) return true;
  return columnNames.length === body.expressionMap.selects.length;
}

Prevention

When it happens

Trigger: Calling .addCommonTableExpression(selectQb, 'cte', { columnNames: ['a', 'b', 'c'] }) when selectQb selects only two columns (or vice versa). Mismatch arises from adding/removing a select in the body without updating columnNames.

Common situations: Naming CTE columns explicitly and then editing the body's select list. Copying a CTE config from a query with a different projection. Forgetting that addSelect changes the select count.

Related errors


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