{"record":{"id":"a1dbbd67e8ff68dc","repo":"hibernate/hibernate-orm","slug":"expectation-outparameter-operates-exclusively-on-c-a1dbbd","errorCode":null,"errorMessage":"Expectation.OutParameter operates exclusively on CallableStatements: ${statement.getClass()}","messagePattern":"Expectation\\.OutParameter operates exclusively on CallableStatements: (.+?)","errorType":"exception","errorClass":"HibernateException","httpStatus":null,"severity":"error","filePath":"hibernate-core/src/main/java/org/hibernate/jdbc/Expectations.java","lineNumber":60,"sourceCode":"\t}\n\n\tprivate static Expectation instantiate(Supplier<? extends Expectation> supplier, boolean callable) {\n\t\tif ( supplier == null ) {\n\t\t\treturn callable\n\t\t\t\t\t? new Expectation.OutParameter()\n\t\t\t\t\t: new Expectation.RowCount();\n\t\t}\n\t\telse {\n\t\t\treturn supplier.get();\n\t\t}\n\t}\n\n\tstatic CallableStatement toCallableStatement(PreparedStatement statement) {\n\t\tif ( statement instanceof CallableStatement callableStatement ) {\n\t\t\treturn callableStatement;\n\t\t}\n\t\telse {\n\t\t\tthrow new HibernateException( \"Expectation.OutParameter operates exclusively on CallableStatements: \"\n\t\t\t\t\t+ statement.getClass() );\n\t\t}\n\t}\n\n\tstatic void checkBatched(int expectedRowCount, int rowCount, int batchPosition, String sql) {\n\t\tswitch (rowCount) {\n\t\t\tcase EXECUTE_FAILED:\n\t\t\t\tthrow new BatchFailedException( \"Batch update failed: \" + batchPosition );\n\t\t\tcase SUCCESS_NO_INFO:\n\t\t\t\tBATCH_MESSAGE_LOGGER.batchSuccessUnknown( batchPosition );\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tif ( expectedRowCount > rowCount ) {\n\t\t\t\t\tthrow new StaleStateException(\n\t\t\t\t\t\t\t\"Batch update returned unexpected row count from update \" + batchPosition\n\t\t\t\t\t\t\t\t\t+ actualVsExpected( expectedRowCount, rowCount )\n\t\t\t\t\t\t\t\t\t+ \" [\" + sql + \"]\"\n\t\t\t\t\t);","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/jdbc/Expectations.java#L42-L78","documentation":"Expectations.toCallableStatement narrows the PreparedStatement handed to verifyOutcome/prepare to a CallableStatement. When the statement executed at flush time is not a CallableStatement (custom SQL not actually callable, or a programmatic call path), this HibernateException is thrown and names the statement's concrete class so you can see what the driver really produced.","triggerScenarios":"An OutParameter expectation is active but the executed statement is a plain PreparedStatement - e.g. callable flag lost on a native/programmatic path, or the driver/connection pool not wrapping {call ...} SQL in a CallableStatement.","commonSituations":"Same misconfiguration as the validate() MappingException but surfacing at flush instead of bootstrap (programmatic expectations, statements built outside annotation validation); unusual driver behavior wrapping callable syntax.","solutions":["Ensure the custom SQL for the entity is declared callable = true and really is a {call ...} statement","Confirm the driver returns a CallableStatement for the SQL (the exception prints the actual class)","If the statement genuinely is not callable, use a COUNT or NONE expectation instead of OUT_PARAM"],"exampleFix":"// before\n@SQLUpdate(sql = \"update Person set name = ? where id = ?\", callable = false)\n@Expectation(type = ExpectationType.OUT_PARAM)\nclass Person { } // flush-time HibernateException: statement is not a CallableStatement\n\n// after\n@SQLUpdate(sql = \"{ ? = call update_person(?, ?) }\", callable = true)\n@Expectation(type = ExpectationType.OUT_PARAM)\nclass Person { }","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n    session.flush();\n} catch (org.hibernate.HibernateException e) {\n    if (e.getMessage() != null && e.getMessage().contains(\"exclusively on CallableStatements\")) {\n        // the executed statement was: e.getMessage().substring(e.getMessage().lastIndexOf(':') + 1)\n        // fix: mark the custom SQL callable or use a COUNT/NONE expectation\n    }\n    throw e;\n}","preventionTips":["Never attach OUT_PARAM expectations to plain PreparedStatement custom SQL","Verify the driver wraps {call ...} syntax in a CallableStatement before relying on out parameters","Keep entity custom-SQL and expectations under one review unit so they change together"],"tags":["custom-sql","expectation","jdbc","stored-procedures"],"backgroundTag":"callable-statement-required","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}