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

AbstractSqmSelectQuery.validateCteName throws IllegalArgumentException when the CTE name's first character is not alphabetic. SQL identifiers for common table expressions must start with a letter in Hibernate's criteria validation (digits, underscores, or symbols at position 0 are rejected), so names like "1cte", "_tmp", "@x", or names accidentally prefixed by a template marker fail here. The validation runs for every with/withRecursive* variant.

Source

Thrown at hibernate-core/src/main/java/org/hibernate/query/sqm/tree/spi/select/AbstractSqmSelectQuery.java:188

			@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;
	}

	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" );

View on GitHub (pinned to fad1729dce)

Solutions

  1. Rename the CTE to start with an ASCII letter, e.g. "cte1_results" instead of "1_results"
  2. Sanitize dynamic names: strip invalid prefixes and prepend a letter if needed before calling with()
  3. Keep a naming utility that enforces [A-Za-z][A-Za-z0-9_]* for all query identifiers
  4. Assert generated names in tests for every report/CTE source

Example fix

// before
query.with( "1_results", sub ); // IllegalArgumentException

// after
query.with( "results_1", sub );
Defensive patterns

Strategy: validation

Validate before calling

if (name == null || name.isEmpty() || !Character.isLetter(name.charAt(0))) {
    throw new IllegalArgumentException("CTE name must start with a letter: " + name);
}
query.with(name, sub);

Prevention

When it happens

Trigger: Criteria `query.with( "1_results", sub )` or `query.with( "_tmp", sub )`; names assembled as prefix+counter where the prefix begins with a digit; sanitizing user input by stripping characters until the name starts with a digit/symbol; HQL-to-criteria translation tools carrying over quoted or decorated identifiers.

Common situations: Auto-generated CTE names ("2024_report", "#1") from reporting layers; i18n or non-ASCII names beginning with a localized character; underscores-first temporary-table naming conventions brought over from SQL dialect habits.

Related errors


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