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
- 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).
- If operands naturally return different shapes, project them into one shared DTO via a ConstructorExpression (cb.construct(ResultDto.class, ...)) in every operand.
- 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
- Standardize every operand of a set operation on one result class from the start (e.g. always Long for counts).
- Write shared builder helpers that take the common Class<T> and stamp it on every operand.
- In classloader-heavy runtimes, always obtain the CriteriaBuilder from the same SessionFactory that loaded the result DTO.
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
- Subquery parent of all operands must match
- Different CTE with same name [%s] found in different set ope
- Informix does not support binary literals
- SingleStore doesn't support UNION/UNION ALL with limit claus
- Locking with set operators is not supported!
AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22).
Data as JSON: /api/errors/253f2e0d56da85a9.
Report an issue: GitHub.