hibernate/hibernate-orm · error · UnsupportedOperationException

DELETE query cannot be sub-query

Error message

DELETE query cannot be sub-query

What it means

SqmDeleteStatement overrides AbstractQuery.subquery(EntityType) to throw UnsupportedOperationException: in the criteria model a DELETE statement cannot host subqueries. Criteria delete statements only support a where predicate on the target root; subselects must be expressed differently (e.g. HQL mutation query or a two-step select-then-delete).

Source

Thrown at hibernate-core/src/main/java/org/hibernate/query/sqm/tree/spi/delete/SqmDeleteStatement.java:152

		return walker.visitDeleteStatement( this );
	}

	@Override
	public void appendHqlString(StringBuilder hql, SqmRenderContext context) {
		appendHqlCteString( hql, context );
		hql.append( "delete from " );
		final SqmRoot<T> root = getTarget();
		hql.append( root.getEntityName() );
		hql.append( ' ' ).append( root.resolveAlias( context ) );
		SqmFromClause.appendJoins( root, hql, context );
		SqmFromClause.appendTreatJoins( root, hql, context );
		super.appendHqlString( hql, context );
	}

	@Nonnull
	@Override
	public <U> Subquery<U> subquery(@Nonnull EntityType<U> type) {
		throw new UnsupportedOperationException( "DELETE query cannot be sub-query" );
	}
}

View on GitHub (pinned to fad1729dce)

Solutions

  1. Use an HQL mutation query with a subselect: em.createMutationQuery("delete from Order o where o.customer.id in (select c.id from Customer c where c.status = :s)").
  2. Run a select criteria query first to collect the affected IDs, then issue the delete restricted to those IDs.
  3. Guard generic code with a query-type check (only SqmSelectQuery supports subquery()).

Example fix

// before
CriteriaDelete<Order> delete = cb.createCriteriaDelete( Order.class );
Subquery<Long> sq = delete.subquery( Long.class ); // throws UnsupportedOperationException
// after
em.createMutationQuery(
    "delete from Order o where o.customer.id in (select c.id from Customer c where c.status = :st)"
).setParameter( "st", "INACTIVE" ).executeUpdate();
Defensive patterns

Strategy: type-guard

Validate before calling

// guard generic tree-processing before calling subquery():
if ( query instanceof SqmSelectQuery<?> ) {
    Subquery<U> sq = ( (AbstractQuery<?>) query ).subquery( type );
} else {
    // DML statement: use HQL mutation query or two-step select+delete instead
}

Type guard

static boolean canHostSubquery(jakarta.persistence.criteria.AbstractQuery<?> q) {
    return q instanceof SqmSelectQuery; // delete (and other DML) statements reject subquery()
}

Try / catch

try {
    sub = deleteStatement.subquery( type );
} catch (UnsupportedOperationException e) {
    // "DELETE query cannot be sub-query" -> fall back to HQL mutation query with subselect
    em.createMutationQuery( hqlDeleteWithSubselect ).executeUpdate();
}

Prevention

When it happens

Trigger: Subquery<Long> sq = cb.createCriteriaDelete(Order.class).subquery(Customer.class); directly, or generic code that walks any AbstractQuery/AbstractSelectionQuery and uniformly calls subquery(type) on it.

Common situations: Porting 'delete where x in (select ...)' logic written for SQL or for a select criteria query; reusable restriction builders that add exists()/in() predicates via query.subquery(...); framework code that processes criteria trees of any kind.

Related errors


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