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

  1. Check first: `if (query.getCteCriteria(name) == null) query.with(name, sub); else reuse the existing JpaCteCriteria`
  2. Give merged fragments unique CTE names (prefix per fragment: "a_data", "b_data")
  3. Centralize CTE registration in one helper that deduplicates by name
  4. 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

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


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