hibernate/hibernate-orm · error · IllegalArgumentException
A CTE with the label %s already exists
Error message
A CTE with the label %s already exists
What it means
AbstractSqmSelectQuery.withInternal throws IllegalArgumentException when registering a non-recursive CTE whose name already exists in the same query — a `cteStatements.putIfAbsent(name, ...)` returning the previous entry means two CTEs would share one label, which is ambiguous SQL. Criteria CTEs are referenced by name later (query.from(cte)), so duplicates are rejected at registration rather than at SQL rendering.
Source
Thrown at hibernate-core/src/main/java/org/hibernate/query/sqm/tree/spi/select/AbstractSqmSelectQuery.java:206
throw new IllegalArgumentException(
String.format(
"Illegal CTE name [%s]. Names must start with an alphabetic character!",
name
)
);
}
return name;
}
protected <X> JpaCteCriteria<X> withInternal(String name, AbstractQuery<X> criteria) {
final var cteStatement = new SqmCteStatement<>(
name,
(SqmSelectQuery<X>) criteria,
this,
nodeBuilder()
);
if ( cteStatements.putIfAbsent( name, cteStatement ) != null ) {
throw new IllegalArgumentException( "A CTE with the label " + cteStatement.getCteTable().getCteName() + " already exists" );
}
return cteStatement;
}
protected <X> JpaCteCriteria<X> withInternal(
String name,
AbstractQuery<X> baseCriteria,
boolean unionDistinct,
Function<JpaCteCriteria<X>, AbstractQuery<X>> recursiveCriteriaProducer) {
final var cteStatement = new SqmCteStatement<>(
name,
(SqmSelectQuery<X>) baseCriteria,
unionDistinct,
recursiveCriteriaProducer,
this,
nodeBuilder()
);
if ( cteStatements.putIfAbsent( name, cteStatement ) != null ) {View on GitHub (pinned to fad1729dce)
Solutions
- Check first: `if (query.getCteCriteria(name) == null) query.with(name, sub); else reuse the existing JpaCteCriteria`
- Give merged fragments unique CTE names (prefix per fragment: "a_data", "b_data")
- Centralize CTE registration in one helper that deduplicates by name
- For the recursive variants, remember the base registration already owns the name — never re-register it in the recursive producer
Example fix
// before
query.with( "totals", cb.createQuery(Double.class)... );
// ... later, another fragment:
query.with( "totals", cb.createQuery(Double.class)... ); // IllegalArgumentException
// after
if ( query.getCteCriteria( "totals" ) == null ) {
query.with( "totals", cb.createQuery(Double.class)... );
} // else: reuse query.getCteCriteria("totals") Defensive patterns
Strategy: validation
Validate before calling
if (query.getCteCriteria(name) == null) {
query.with(name, sub);
} else {
return query.getCteCriteria(name); // reuse existing
} Prevention
- Centralize CTE registration behind a deduplicating helper
- Namespace CTE names per module/fragment
- Never re-register a name; fetch it via getCteCriteria instead
When it happens
Trigger: Criteria: calling `query.with( "totals", sub1 )` and later `query.with( "totals", sub2 )` on the same CriteriaQuery/CriteriaDefinition; loops that register CTEs from a list/map containing a repeated key; composing query fragments where each fragment adds its own "data" CTE into one target query.
Common situations: Query-composition frameworks that merge criteria fragments and naively merge their CTEs; refactoring one big CTE into two without renaming; copy-paste of a helper that always registers a CTE with the same fixed name; recursive refactoring that moves a shared CTE into the parent while leaving the original registration.
Related errors
- Illegal CTE name [%s]. Names must start with an alphabetic c
- Illegal empty CTE name
- Illegal CTE name [%s]. Names must start with an alphabetic c
- Illegal empty CTE name
- Illegal search order attribute '{}' passed, which is not par
AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22).
Data as JSON: /api/errors/9404b1b41c64ee68.
Report an issue: GitHub.