hibernate/hibernate-orm · error · IllegalArgumentException
Different CTE with same name [%s] found in different set ope
Error message
Different CTE with same name [%s] found in different set operands!
What it means
While flattening the operands of a criteria set operation, collectQueryPartsAndCtes merges each operand's CTE statements into one map keyed by CTE name. If the same CTE name was already contributed by a previous operand and the two SqmCteStatement objects are not the identical instance, Hibernate throws IllegalArgumentException, because the merged query could not decide which definition of the CTE to render.
Source
Thrown at hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java:820
return new SqmSubQuery<>(
parent,
new SqmQueryGroup<>( this, operator, queryParts ),
resultType,
cteStatements,
this
);
}
private <T> void collectQueryPartsAndCtes(
SqmSelectQuery<T> query,
List<SqmQueryPart<T>> queryParts,
Map<String, SqmCteStatement<?>> cteStatements) {
queryParts.add( query.getQueryPart() );
for ( var cteStatement : query.getCteStatements() ) {
final String name = cteStatement.getCteTable().getCteName();
final var old = cteStatements.put( name, cteStatement );
if ( old != null && old != cteStatement ) {
throw new IllegalArgumentException(
String.format( "Different CTE with same name [%s] found in different set operands!", name )
);
}
}
}
@Override
public <X, T> SqmExpression<X> cast(JpaExpression<T> expression, Class<X> castTargetJavaType) {
return cast( expression, castTarget( castTargetJavaType ) );
}
@Override
public <X, T> SqmExpression<X> cast(JpaExpression<T> expression, JpaCastTarget<X> castTarget) {
final var sqmCastTarget = (SqmCastTarget<X>) castTarget;
return getFunctionDescriptor( "cast" ).generateSqmExpression(
asList( (SqmTypedNode<?>) expression, sqmCastTarget ),
sqmCastTarget.getType(),
queryEngineView on GitHub (pinned to fad1729dce)
Solutions
- Define the shared CTE once, on the outer/first operand only, and let later operands reference its name without redefining it.
- If each operand needs its own CTE, give them distinct names (idsA, idsB).
- Restructure so the CTE wraps the set operation instead of being duplicated inside it (a single outer query carrying the with-clause whose query part is the union group).
Example fix
// before
JpaCriteriaQuery<Order> q1 = ...; q1.with("ids", c -> c.select(orderRoot.get("id")).where(...));
JpaCriteriaQuery<Order> q2 = ...; q2.with("ids", c -> c.select(otherRoot.get("id")).where(...));
cb.union(q1, q2); // Different CTE with same name [ids] found in different set operands!
// after
q2.with("ids2", c -> c.select(otherRoot.get("id")).where(...)); // unique name per operand
cb.union(q1, q2); Defensive patterns
Strategy: validation
Validate before calling
// Hibernate API: JpaCriteriaQuery exposes its CTEs
Set<String> used = new HashSet<>();
for (CriteriaQuery<?> q : List.of(q1, q2)) {
if (q instanceof JpaCriteriaQuery<?> jq) {
// collect names via with-clause bookkeeping in your builder; JpaCteCriteria names must be unique across operands
}
}
// simplest: track names yourself when attaching CTEs
if (!used.add(cteName)) throw new IllegalArgumentException("CTE name reused across operands: " + cteName); Try / catch
try {
cb.union(q1, q2);
} catch (IllegalArgumentException e) {
if (e.getMessage().contains("Different CTE with same name")) { /* rename the second operand's CTE and retry */ }
else throw e;
} Prevention
- Attach each shared CTE to exactly one operand (or to the outer query) instead of duplicating it everywhere.
- Generate CTE names programmatically (prefix + counter) to avoid collisions when operands are built by loops.
- Keep per-operand builder helpers from stamping standard-named CTEs onto every query.
When it happens
Trigger: cb.union(q1, q2) where both q1 and q2 define their own 'with ids as (...)' CTE via the JpaCriteriaQuery with()/withRecursive() API; identical-looking CTE definitions created separately in each operand (equal text but different SqmCteStatement instances still fails, since the check is old != cteStatement).
Common situations: Per-operand query builders (one per shard/table) that each attach the same-named CTE for filtering; migrating an HQL 'with ... union' query to the criteria API and copying the with-clause into every operand; helper factories that stamp a standard CTE (e.g. 'filtered') onto every query they build.
Related errors
- Result type of all operands must match
- Subquery parent of all operands must match
- Couldn't generate CTE name for base name [%s] after %d tries
- Informix does not support binary literals
- Given subclass table number is outside expected range [%s] a
AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22).
Data as JSON: /api/errors/3a8b23017dd6828d.
Report an issue: GitHub.