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

  1. Start the label with an ASCII letter, e.g. "cte_ids" instead of "_ids".
  2. Validate dynamic labels with a regex like ^\p{IsAlphabetic} before passing them.
  3. 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

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


AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22). Data as JSON: /api/errors/8b9254b6569f65dc. Report an issue: GitHub.