fix: reject non-Value params in Connection.execute instead of crashing the JVM - #12
Conversation
…ert boxed Java types Reject non-Value parameters in Connection.execute instead of crashing the JVM. Option A: Accept boxed Java values (String, Long, UUID, etc.) by reusing the type-conversion ladder from lbugValueCreateValue in bindJavaParamsToPreparedStatement. Option B: Guard unrecognized types with IllegalArgumentException instead of reinterpret_cast crash. - Moves the JNI type-conversion ladder into a reusable javaObjectToValue helper - Adds throwIllegalArgumentException and getClassName JNI helpers - Widens Connection.execute signature from Map<String, Value> to Map<String, ?> - Adds early-return guard in lbugConnectionExecute after param binding - Adds 7 new tests: raw String/Long/Double/Boolean, mixed raw+Value, unsupported type throws, Value-wrapped regression Fixes: #11
Move the boxed-Java-type-to-Value coercion from the JNI binding into Connection.execute on the Java side. The new coerceParams helper builds a LinkedHashMap<String, Value> from the user-supplied Map<String, ?>, preserving order and dispatching each entry through a single pattern-matching switch (JEP 441, GA in Java 21). Already-wrapped Value instances pass through; boxed types are auto-converted via Value(T); unsupported types and null surface as IllegalArgumentException with clear messages. This lets the native binding stay strictly typed (no @SuppressWarnings("unchecked") or raw-type cast in Connection.execute) and removes ~135 lines of dead code from lbug_java.cpp: - javaObjectToValue (the conversion ladder) - getClassName (only used to format the dead-branch error) - throwIllegalArgumentException (only called from the dead branch) - the else branch in bindJavaParamsToPreparedStatement bindJavaParamsToPreparedStatement collapses to a direct lbug_value_clone with a defensive IsInstanceOf check as a contract guard against any future API bypass. Bumps Java compile target from 1.8/11 to 21 in CMakeLists.txt, build.gradle, and the published POM. Adds kotlinOptions.jvmTarget = 21 so the Kotlin plugin does not refuse the build with "inconsistent JVM target" between compileTestJava and compileTestKotlin. CI matrix is already on 21 (java-workflow.yml, build-and-deploy.yml) so no CI churn. Downstream consumers will need Java 21+ at runtime. Adds two tests: - executeWithNullParamThrows: locks in the key-naming error - executeWithValueCreateNullParam: verifies the happy SQL-NULL path
|
@alvorithm could you please test? |
|
Tested. It fixes the crash, including the original Clojure caller, and I found nothing broken. Details below. Build: Before and after, same class, same source
Caller shapes from #11
That last one is the case I most wanted to check, since the The original callerThe report came from Clojure, where maps are
Worth noting the error message names the offending class, which is what makes this actionable from a non-Java caller: Your test suite
One thing worth a deliberate decisionThe PR raises the Java baseline from 8/11 to 21 ( The same coercion as an Not blocking on my side. Happy to re-test if you change anything. |
Reject non-Value parameters in
Connection.executeinstead of crashing the JVM.The problem
Connection.executeis declaredexecute(PreparedStatement, Map<String, Value>)and the JNI layer trusts that declaration absolutely. Any caller that reaches it through erasure — a raw or uncheckedMap, reflection, or a JVM language that does not enforce Java generics — hands ordinary Java objects to areinterpret_cast, and the process dies with SIGSEGV.The fix
Implements both options proposed in the issue:
Option A — Accept boxed Java values. The type-conversion ladder from
lbugValueCreateValue(which already handles Boolean, Byte, Short, Integer, Long, BigInteger, Float, Double, BigDecimal, String, InternalID, UUID, LocalDate, Instant, Duration) is reused inbindJavaParamsToPreparedStatementsoMap.of(\"p_id\", uuid)behaves the way a JVM caller expects.Option B — Guard and throw. If the value is neither a
Valuenor a known boxed type, anIllegalArgumentExceptionis thrown naming the parameter and the offending class, instead of crashing the JVM.Additional changes
Connection.executesignature fromMap<String, Value>toMap<String, ?>so non-erased callers can pass raw types directlylbugConnectionExecuteafter param binding to safely propagate the pending Java exceptionIllegalArgumentException, Value-wrapped regressionFixes: #11