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
- Align columnNames length with the body's select list; either drop columnNames (let the DB infer) or update both together.
- When using addSelect on the body, recount and update the columnNames array.
- Omit columnNames entirely if you don't need explicit CTE column aliases.
- 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
- Keep columnNames length in sync with the body select list; update both together.
- Omit columnNames if explicit CTE column aliases are unnecessary.
- Recount selects after addSelect changes.
- Unit-test composed CTEs to assert column alignment.
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
- Cannot perform insert query because values are not defined.
- Function parameter isn't supported in the parameters. Please
- QueryBuilder parameter keys may only contain numbers, letter
- Nested CTEs aren't supported (CTE: ${cte.alias})
- Only select queries are supported in CTEs in ${this.connecti
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/36e8b0d93578d15a.
Report an issue: GitHub.