hibernate/hibernate-orm · error · IllegalMutationQueryException

Query string is not a mutation

Error message

Query string is not a mutation

What it means

createMutationQuery(String hql) (and named-query paths returning MutationQuery) require an INSERT/UPDATE/DELETE statement. After interpreting the HQL, buildHqlMutationQuery checks that the SQM tree is an SqmDmlStatement; a SELECT produces IllegalMutationQueryException('Query string is not a mutation'). The typed MutationQuery API exists so executeUpdate() semantics are guaranteed at creation time.

Source

Thrown at hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java:1765

		}
		selectionQuery.setComment( hql );
		applyQuerySettingsAndHints( selectionQuery );

		return selectionQuery;
	}

	@Override
	public MutationQuery createMutationQuery(String hql) {
		checksBeforeQueryCreation();
		return buildHqlMutationQuery( hql, interpretHql( hql ) );
	}

	private <T> MutationQueryImplementor<T> buildHqlMutationQuery(String hql, HqlInterpretation<T> interpretation) {
		if ( interpretation.getSqmStatement() instanceof SqmDmlStatement<T> mutationAst ) {
			return buildHqlMutationQuery( hql, interpretation, mutationAst.getTarget().getJavaType() );
		}
		else {
			throw new IllegalMutationQueryException( "Query string is not a mutation", hql );
		}
	}

	private <T> MutationQueryImplementor<T> buildHqlMutationQuery(String hql, HqlInterpretation<T> interpretation, Class<T> targetType) {
		final var mutationQuery = new MutationQueryImpl<>( hql, interpretation, targetType, this );
		mutationQuery.setComment( hql );
		applyQuerySettingsAndHints( mutationQuery );
		return mutationQuery;
	}

	@Override
	@Nonnull
	public MutationQuery createStatement(@Nonnull String hqlString) {
		// JPA form
		try {
			return createMutationQuery( hqlString );
		}
		catch (IllegalMutationQueryException e) {

View on GitHub (pinned to fad1729dce)

Solutions

  1. Use createSelectionQuery/createQuery for SELECT statements; createMutationQuery only for INSERT/UPDATE/DELETE.
  2. If the string is config- or user-supplied, route by the first HQL keyword (see typeGuard) before choosing the factory method.
  3. Fix the statement itself when a select was unintentional (e.g., truncated HQL).

Example fix

// before
MutationQuery q = session.createMutationQuery("select p from Person p"); // throws
// after
SelectionQuery<Person> q = session.createSelectionQuery("select p from Person p", Person.class);
Defensive patterns

Strategy: type-guard

Type guard

static boolean isMutationHql(String hql) {
    String head = hql.stripLeading().toLowerCase(Locale.ROOT);
    return head.startsWith("update ") || head.startsWith("delete ") || head.startsWith("insert ");
}

// usage: route dynamic HQL to the right API
if (isMutationHql(hql)) {
    session.createMutationQuery(hql).executeUpdate();
} else {
    session.createSelectionQuery(hql).list();
}

Try / catch

try {
    return session.createMutationQuery(hql).executeUpdate();
} catch (IllegalMutationQueryException e) {
    throw new IllegalArgumentException("Expected DML but got: " + hql, e); // surface config error, do not auto-rerun
}

Prevention

When it happens

Trigger: session.createMutationQuery("select p from Person p"); createMutationQuery on a string whose first token is SELECT; routing every query string through createMutationQuery in a generic helper; createNamedQuery variants that must be mutations but resolve to a SELECT.

Common situations: Refactoring createQuery(hql).executeUpdate() to the createMutationQuery API for a select statement; generic DAO/query-router funnels; upgrading to Hibernate 6.3+/7 where the mutation/selection split is enforced strictly.

Related errors


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