hibernate/hibernate-orm · error · IllegalArgumentException

Illegal empty CTE name

Error message

Illegal empty CTE name

What it means

Hibernate throws this IllegalArgumentException from AbstractSqmDmlStatement.validateCteName when a criteria DML statement (SqmInsertStatement/SqmUpdateStatement/SqmDeleteStatement) registers a named CTE with a null or blank label. It applies to the JpaCteContainer methods with(String, criteria), withRecursiveUnionAll(string, ...) and withRecursiveUnionDistinct(string, ...) on the DML statement. It is a fail-fast query-construction error; nothing is executed against the database.

Source

Thrown at hibernate-core/src/main/java/org/hibernate/query/sqm/tree/spi/AbstractSqmDmlStatement.java:156

	public <X> JpaCteCriteria<X> withRecursiveUnionAll(
			@Nonnull String name,
			@Nonnull AbstractQuery<X> baseCriteria,
			@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()

View on GitHub (pinned to fad1729dce)

Solutions

  1. Pass a non-empty label that starts with an alphabetic character, e.g. with("ids_to_delete", cteQuery).
  2. If the name is dynamic, validate it (non-null, non-blank) before calling with().
  3. If you do not care about the label, use the unnamed overloads with(criteria) / withRecursiveUnionAll(criteria, producer) which auto-generate a unique alias.
  4. Keep CTE labels in shared constants so they are never built ad hoc.

Example fix

// before
delete.with( cteName, idsCte ); // cteName is "" or null -> "Illegal empty CTE name"
// after
delete.with( "ids_to_delete", idsCte );
Defensive patterns

Strategy: validation

Validate before calling

static String cteName(String raw) {
    if ( raw == null || raw.isBlank() ) {
        throw new IllegalArgumentException( "CTE label must be non-blank, got: " + raw );
    }
    return raw;
}
// use: delete.with( cteName( label ), cte );

Prevention

When it happens

Trigger: Calling criteriaUpdate.with(null, cteQuery), .with(" ", cteQuery) or .withRecursiveUnionAll("", base, producer) on a criteria insert/update/delete statement. Any named with* overload whose name argument is null, empty or whitespace-only hits this branch (name == null || name.isBlank()).

Common situations: CTE label built from user input, a config key or a Map key that turns out empty; refactors that renamed or dropped the label constant; translating an HQL 'WITH x AS (...)' to criteria and forgetting to carry the name over.

Related errors


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