hibernate/hibernate-orm · error · MappingException

Expectation.OutParameter operates exclusively on CallableSta

Error message

Expectation.OutParameter operates exclusively on CallableStatements

What it means

At bootstrap Hibernate validates every custom-SQL expectation against its declaration. Expectation.OutParameter.validate throws this MappingException when an OUT-parameter expectation is attached to custom SQL that is not marked callable - the OutParameter verification path only works against CallableStatements, so a plain PreparedStatement declaration is a mapping error, not a runtime one.

Source

Thrown at hibernate-core/src/main/java/org/hibernate/jdbc/Expectation.java:240

			try {
				result = toCallableStatement( statement ).getInt( parameterIndex() );
			}
			catch ( SQLException sqle ) {
				sqlExceptionHelper.logExceptions( sqle, "Could not extract row count from CallableStatement" );
				throw new GenericJDBCException( "Could not extract row count from CallableStatement", sqle );
			}
			if ( batchPosition < 0 ) {
				checkNonBatched( expectedRowCount(), result, sql );
			}
			else {
				checkBatched( expectedRowCount(), result, batchPosition, sql );
			}
		}

		@Override
		public void validate(boolean callable) throws MappingException {
			if ( !callable ) {
				throw new MappingException( "Expectation.OutParameter operates exclusively on CallableStatements" );
			}
		}

		@Override
		public int getNumberOfParametersUsed() {
			return 1;
		}

		@Override
		public int prepare(PreparedStatement statement) throws SQLException, HibernateException {
			toCallableStatement( statement ).registerOutParameter( parameterIndex(), Types.NUMERIC );
			return 1;
		}

		@Override
		public boolean canBeBatched() {
			return false;
		}

View on GitHub (pinned to fad1729dce)

Solutions

  1. Set callable = true on the custom SQL annotation (@SQLInsert/@SQLUpdate/@SQLDelete)
  2. Or, if the SQL really is a plain statement, switch the expectation to ExpectationType.COUNT (default row-count check) or NONE
  3. Re-run bootstrap: this fails fast at mapping validation, so the fix is purely a mapping/annotation change

Example fix

// before
@SQLDelete(sql = "delete from Person where id = ?", callable = false)
@Expectation(type = ExpectationType.OUT_PARAM)
class Person { }

// after
@SQLDelete(sql = "{ ? = call delete_person(?) }", callable = true)
@Expectation(type = ExpectationType.OUT_PARAM)
class Person { }
Defensive patterns

Strategy: validation

Validate before calling

// Fail fast on inconsistent mapping before first use (e.g. in a startup check)
for (Class<?> entity : scannedEntities) {
    for (var a : entity.getAnnotations()) {
        boolean callable = switch (a.annotationType().getSimpleName()) {
            case "SQLInsert" -> ((org.hibernate.annotations.SQLInsert) a).callable();
            case "SQLUpdate" -> ((org.hibernate.annotations.SQLUpdate) a).callable();
            case "SQLDelete" -> ((org.hibernate.annotations.SQLDelete) a).callable();
            default -> true;
        };
        org.hibernate.annotations.Expectation exp = entity.getAnnotation(org.hibernate.annotations.Expectation.class);
        if (exp != null && exp.type() == org.hibernate.annotations.ExpectationType.OUT_PARAM && !callable) {
            throw new IllegalStateException(entity + " uses OUT_PARAM expectation on non-callable SQL");
        }
    }
}

Prevention

When it happens

Trigger: Combining an out-parameter expectation with non-callable custom SQL, e.g. @SQLDelete(sql = "delete from Person where id = ?", callable = false) together with @Expectation(type = ExpectationType.OUT_PARAM).

Common situations: Switching an entity's custom SQL from inline statements to stored procedures and forgetting the callable flag; copy-pasting @Expectation annotations between entities with different SQL styles.

Related errors


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