{"record":{"id":"ed72f759f75b05a9","repo":"hibernate/hibernate-orm","slug":"expectation-outparameter-operates-exclusively-on-c","errorCode":null,"errorMessage":"Expectation.OutParameter operates exclusively on CallableStatements","messagePattern":"Expectation\\.OutParameter operates exclusively on CallableStatements","errorType":"exception","errorClass":"MappingException","httpStatus":null,"severity":"error","filePath":"hibernate-core/src/main/java/org/hibernate/jdbc/Expectation.java","lineNumber":240,"sourceCode":"\t\t\ttry {\n\t\t\t\tresult = toCallableStatement( statement ).getInt( parameterIndex() );\n\t\t\t}\n\t\t\tcatch ( SQLException sqle ) {\n\t\t\t\tsqlExceptionHelper.logExceptions( sqle, \"Could not extract row count from CallableStatement\" );\n\t\t\t\tthrow new GenericJDBCException( \"Could not extract row count from CallableStatement\", sqle );\n\t\t\t}\n\t\t\tif ( batchPosition < 0 ) {\n\t\t\t\tcheckNonBatched( expectedRowCount(), result, sql );\n\t\t\t}\n\t\t\telse {\n\t\t\t\tcheckBatched( expectedRowCount(), result, batchPosition, sql );\n\t\t\t}\n\t\t}\n\n\t\t@Override\n\t\tpublic void validate(boolean callable) throws MappingException {\n\t\t\tif ( !callable ) {\n\t\t\t\tthrow new MappingException( \"Expectation.OutParameter operates exclusively on CallableStatements\" );\n\t\t\t}\n\t\t}\n\n\t\t@Override\n\t\tpublic int getNumberOfParametersUsed() {\n\t\t\treturn 1;\n\t\t}\n\n\t\t@Override\n\t\tpublic int prepare(PreparedStatement statement) throws SQLException, HibernateException {\n\t\t\ttoCallableStatement( statement ).registerOutParameter( parameterIndex(), Types.NUMERIC );\n\t\t\treturn 1;\n\t\t}\n\n\t\t@Override\n\t\tpublic boolean canBeBatched() {\n\t\t\treturn false;\n\t\t}","sourceCodeStart":222,"sourceCodeEnd":258,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/jdbc/Expectation.java#L222-L258","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Set callable = true on the custom SQL annotation (@SQLInsert/@SQLUpdate/@SQLDelete)","Or, if the SQL really is a plain statement, switch the expectation to ExpectationType.COUNT (default row-count check) or NONE","Re-run bootstrap: this fails fast at mapping validation, so the fix is purely a mapping/annotation change"],"exampleFix":"// before\n@SQLDelete(sql = \"delete from Person where id = ?\", callable = false)\n@Expectation(type = ExpectationType.OUT_PARAM)\nclass Person { }\n\n// after\n@SQLDelete(sql = \"{ ? = call delete_person(?) }\", callable = true)\n@Expectation(type = ExpectationType.OUT_PARAM)\nclass Person { }","handlingStrategy":"validation","validationCode":"// Fail fast on inconsistent mapping before first use (e.g. in a startup check)\nfor (Class<?> entity : scannedEntities) {\n    for (var a : entity.getAnnotations()) {\n        boolean callable = switch (a.annotationType().getSimpleName()) {\n            case \"SQLInsert\" -> ((org.hibernate.annotations.SQLInsert) a).callable();\n            case \"SQLUpdate\" -> ((org.hibernate.annotations.SQLUpdate) a).callable();\n            case \"SQLDelete\" -> ((org.hibernate.annotations.SQLDelete) a).callable();\n            default -> true;\n        };\n        org.hibernate.annotations.Expectation exp = entity.getAnnotation(org.hibernate.annotations.Expectation.class);\n        if (exp != null && exp.type() == org.hibernate.annotations.ExpectationType.OUT_PARAM && !callable) {\n            throw new IllegalStateException(entity + \" uses OUT_PARAM expectation on non-callable SQL\");\n        }\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Pair ExpectationType.OUT_PARAM only with callable = true custom SQL","Review expectation annotations whenever custom SQL changes from plain to stored procedures","Let bootstrap validation fail early in CI rather than at first flush"],"tags":["custom-sql","expectation","mapping","stored-procedures"],"backgroundTag":"callable-statement-required","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}