hibernate/hibernate-orm · error · IllegalArgumentException

Result type of all operands must match

Error message

Result type of all operands must match

What it means

Thrown by HibernateCriteriaBuilder.union/unionAll/intersect/intersectAll/except/exceptAll(CriteriaQuery, CriteriaQuery...) when the operands' result types differ. The check uses reference equality on the Class returned by getResultType(), so both a genuinely different type (Long vs Integer) and the same class loaded by two different classloaders fail. Set operations in SQL require all operands to project union-compatible columns.

Source

Thrown at hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java:770

	@Override
	public <T> JpaSubQuery<T> except(boolean all, Subquery<? extends T> query1, Subquery<?>... queries) {
		return setOperation( all ? SetOperator.EXCEPT_ALL : SetOperator.EXCEPT, query1, queries );
	}

	@SuppressWarnings("unchecked")
	private <T> JpaCriteriaQuery<T> setOperation(
			SetOperator operator,
			CriteriaQuery<? extends T> criteriaQuery,
			CriteriaQuery<?>... queries) {
		final var resultType = (Class<T>) criteriaQuery.getResultType();
		final List<SqmQueryPart<T>> queryParts = new ArrayList<>( queries.length + 1 );
		final Map<String, SqmCteStatement<?>> cteStatements = new LinkedHashMap<>();
		final var selectStatement = (SqmSelectStatement<T>) criteriaQuery;
		collectQueryPartsAndCtes( selectStatement, queryParts, cteStatements );
		for ( var query : queries ) {
			if ( query.getResultType() != resultType ) {
				throw new IllegalArgumentException( "Result type of all operands must match" );
			}
			collectQueryPartsAndCtes( (SqmSelectQuery<T>) query, queryParts, cteStatements );
		}
		return new SqmSelectStatement<>(
				new SqmQueryGroup<>( this, operator, queryParts ),
				resultType,
				cteStatements,
				selectStatement.getQuerySource(),
				this
		);
	}

	@SuppressWarnings("unchecked")
	private <T> JpaSubQuery<T> setOperation(
			SetOperator operator,
			Subquery<? extends T> subquery,
			Subquery<?>... queries) {
		final var resultType = (Class<T>) subquery.getResultType();

View on GitHub (pinned to fad1729dce)

Solutions

  1. Make every operand produce exactly the same result type: rebuild each query with matching q.select(...) / tuple(...) target class (e.g. cast numeric results with cb.sum(...).as(Long.class) so all operands are Long).
  2. If operands naturally return different shapes, project them into one shared DTO via a ConstructorExpression (cb.construct(ResultDto.class, ...)) in every operand.
  3. In classloader-sensitive environments, ensure the criteria builder and the DTO class come from the same classloader (build queries with the SessionFactory's own CriteriaBuilder).

Example fix

// before
CriteriaQuery<Integer> q1 = cb.createQuery(Integer.class); q1.select(cb.count(root1).as(Integer.class));
CriteriaQuery<Long> q2 = cb.createQuery(Long.class); q2.select(cb.count(root2));
cb.union(q1, q2); // IllegalArgumentException: Result type of all operands must match

// after
CriteriaQuery<Long> q1 = cb.createQuery(Long.class); q1.select(cb.count(root1));
CriteriaQuery<Long> q2 = cb.createQuery(Long.class); q2.select(cb.count(root2));
cb.union(q1, q2);
Defensive patterns

Strategy: validation

Validate before calling

boolean sameResultType(CriteriaQuery<?> first, CriteriaQuery<?>... rest) {
    Class<?> t = first.getResultType();
    for (CriteriaQuery<?> q : rest) if (q.getResultType() != t) return false;
    return true;
}
// guard:
if (!sameResultType(q1, q2, q3)) throw new IllegalArgumentException("operands must share one result type");

Type guard

static boolean isUnionCompatible(CriteriaQuery<?> a, CriteriaQuery<?> b) {
    return a.getResultType() == b.getResultType(); // reference equality mirrors Hibernate's check
}

Try / catch

try {
    cb.union(q1, q2);
} catch (IllegalArgumentException e) {
    if (e.getMessage().contains("Result type of all operands")) { /* retype operands to one Class and retry */ }
    else throw e;
}

Prevention

When it happens

Trigger: cb.union(queryA, queryB) where queryA was built with select(Long.class) or multiselect to Long and queryB selects Integer.class; queries where one operand selects an entity (Order.class) and another selects a DTO class; same-named DTO classes loaded from different classloaders in app-server/reloading environments.

Common situations: Building 'search across two tables' queries where each sub-query maps to a different result DTO; count/aggregate operands that mix Long and Integer boxing; dev-reload or OSGi setups where the DTO class is loaded twice.

Related errors


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