hibernate/hibernate-orm · error · IllegalArgumentException
Illegal CTE name [%s]. Names must start with an alphabetic c
Error message
Illegal CTE name [%s]. Names must start with an alphabetic character!
What it means
Same validator (validateCteName) rejects a non-blank CTE label whose first character fails Character.isAlphabetic. The label of a CTE registered on a criteria DML statement must start with a letter; digits, underscores, '$' or other symbols as the first character throw IllegalArgumentException. The rest of the name is not validated here.
Source
Thrown at hibernate-core/src/main/java/org/hibernate/query/sqm/tree/spi/AbstractSqmDmlStatement.java:159
@Nonnull Function<JpaCteCriteria<X>, AbstractQuery<X>> recursiveCriteriaProducer) {
return withInternal( validateCteName( name ), baseCriteria, false, recursiveCriteriaProducer );
}
@Nonnull
@Override
public <X> JpaCteCriteria<X> withRecursiveUnionDistinct(
@Nonnull String name,
@Nonnull AbstractQuery<X> baseCriteria,
@Nonnull Function<JpaCteCriteria<X>, AbstractQuery<X>> recursiveCriteriaProducer) {
return withInternal( validateCteName( name ), baseCriteria, true, recursiveCriteriaProducer );
}
private String validateCteName(String name) {
if ( name == null || name.isBlank() ) {
throw new IllegalArgumentException( "Illegal empty CTE name" );
}
if ( !isAlphabetic( name.charAt( 0 ) ) ) {
throw new IllegalArgumentException(
String.format(
"Illegal CTE name [%s]. Names must start with an alphabetic character!",
name
)
);
}
return name;
}
private <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" );View on GitHub (pinned to fad1729dce)
Solutions
- Start the label with an ASCII letter, e.g. "cte_ids" instead of "_ids".
- Validate dynamic labels with a regex like ^\p{IsAlphabetic} before passing them.
- Use the unnamed with()/withRecursiveUnionAll() overloads to let Hibernate generate a legal alias.
Example fix
// before delete.withRecursiveUnionAll( "1_levels", base, producer ); // starts with digit // after delete.withRecursiveUnionAll( "levels", base, producer );
Defensive patterns
Strategy: validation
Validate before calling
private static final Pattern LEGAL_CTE = Pattern.compile( "^\\p{IsAlphabetic}" );
static boolean isLegalCteName(String name) {
return name != null && !name.isBlank() && LEGAL_CTE.matcher( name ).find();
} Prevention
- Start generated CTE labels with a fixed letter prefix such as "cte_".
- Do not copy DB identifier conventions (leading underscore, digit-first) into criteria CTE labels.
- Cover label format in unit tests for query-builder helpers.
When it happens
Trigger: with("_ids", cteQuery), with("1st_stage", cteQuery) or withRecursiveUnionDistinct("$foo", base, producer) on a criteria insert/update/delete statement. Any label whose charAt(0) is not alphabetic (leading '_', digit, '$', '-', etc.).
Common situations: Reusing database naming conventions (leading underscore snake_case), generated numeric-prefixed labels, or names prefixed with '$'/'#' tokens copied from HQL aliases or table aliases.
Related errors
- Illegal empty CTE name
- Illegal empty CTE name
- Illegal CTE name [%s]. Names must start with an alphabetic c
- A CTE with the label %s already exists
- Expecting DML target entity type [%s] but got [%s]
AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22).
Data as JSON: /api/errors/8b9254b6569f65dc.
Report an issue: GitHub.