Skip to content

Commit 7eee51e

Browse files
committed
fix: guard against non-Value params in Connection.execute + auto-convert 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
1 parent a0e7728 commit 7eee51e

3 files changed

Lines changed: 271 additions & 8 deletions

File tree

‎src/jni/lbug_java.cpp‎

Lines changed: 143 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,14 @@ static void throwJNIException(JNIEnv* env, const char* message) {
160160
env->ThrowNew(exClass, message);
161161
}
162162

163+
static void throwIllegalArgumentException(JNIEnv* env, const char* message) {
164+
jclass exClass = env->FindClass("java/lang/IllegalArgumentException");
165+
if (exClass == nullptr) {
166+
return;
167+
}
168+
env->ThrowNew(exClass, message);
169+
}
170+
163171
template<typename... Args>
164172
static jobject callObjectMethodChecked(JNIEnv* env, jobject object, jmethodID method,
165173
Args... args) {
@@ -504,6 +512,111 @@ std::string dataTypeToString(lbug_data_type_id dataType) {
504512
}
505513
}
506514

515+
// Convert a Java boxed primitive/String/UUID/etc. to an lbug_value*.
516+
// Returns nullptr if the type is not recognised.
517+
static lbug_value* javaObjectToValue(JNIEnv* env, jobject val) {
518+
if (env->IsInstanceOf(val, J_C_Boolean)) {
519+
jboolean value = callBooleanMethodChecked(env, val, J_C_Boolean_M_booleanValue);
520+
return lbug_value_create_bool(static_cast<bool>(value));
521+
} else if (env->IsInstanceOf(val, J_C_Byte)) {
522+
jbyte value = callByteMethodChecked(env, val, J_C_Byte_M_byteValue);
523+
return lbug_value_create_int8(static_cast<int8_t>(value));
524+
} else if (env->IsInstanceOf(val, J_C_Short)) {
525+
jshort value = callShortMethodChecked(env, val, J_C_Short_M_shortValue);
526+
return lbug_value_create_int16(static_cast<int16_t>(value));
527+
} else if (env->IsInstanceOf(val, J_C_Integer)) {
528+
jint value = callIntMethodChecked(env, val, J_C_Integer_M_intValue);
529+
return lbug_value_create_int32(static_cast<int32_t>(value));
530+
} else if (env->IsInstanceOf(val, J_C_Long)) {
531+
jlong value = callLongMethodChecked(env, val, J_C_Long_M_longValue);
532+
return lbug_value_create_int64(static_cast<int64_t>(value));
533+
} else if (env->IsInstanceOf(val, J_C_BigInteger)) {
534+
int64_t lower =
535+
static_cast<int64_t>(callLongMethodChecked(env, val, J_C_BigInteger_M_longValue));
536+
jobject shifted = callObjectMethodChecked(env, val, J_C_BigInteger_M_shiftRight, 64);
537+
int64_t upper = static_cast<int64_t>(
538+
callLongMethodChecked(env, shifted, J_C_BigInteger_M_longValue));
539+
return lbug_value_create_int128({.low = static_cast<uint64_t>(lower), .high = upper});
540+
} else if (env->IsInstanceOf(val, J_C_Float)) {
541+
jfloat value = callFloatMethodChecked(env, val, J_C_Float_M_floatValue);
542+
return lbug_value_create_float(static_cast<float>(value));
543+
} else if (env->IsInstanceOf(val, J_C_Double)) {
544+
jdouble value = callDoubleMethodChecked(env, val, J_C_Double_M_doubleValue);
545+
return lbug_value_create_double(static_cast<double>(value));
546+
} else if (env->IsInstanceOf(val, J_C_BigDecimal)) {
547+
jobject normalized =
548+
callObjectMethodChecked(env, val, J_C_BigDecimal_M_stripTrailingZeros);
549+
jstring value = static_cast<jstring>(
550+
callObjectMethodChecked(env, normalized, J_C_BigDecimal_M_toString));
551+
std::string str = jstringToUtf8String(env, value);
552+
auto precision = static_cast<int32_t>(
553+
callIntMethodChecked(env, normalized, J_C_BigDecimal_M_precision));
554+
auto scale =
555+
static_cast<int32_t>(callIntMethodChecked(env, normalized, J_C_BigDecimal_M_scale));
556+
if (precision > JAVA_DECIMAL_PRECISION_LIMIT) {
557+
throw NotImplementedException(
558+
std::format("Decimal precision cannot be greater than {}"
559+
"Note: positive exponents contribute to precision",
560+
JAVA_DECIMAL_PRECISION_LIMIT));
561+
}
562+
return lbug_value_create_decimal(
563+
str.c_str(), static_cast<uint32_t>(precision), static_cast<uint32_t>(scale));
564+
} else if (env->IsInstanceOf(val, J_C_String)) {
565+
jstring value = static_cast<jstring>(val);
566+
std::string str = jstringToUtf8String(env, value);
567+
return lbug_value_create_string(str.c_str());
568+
} else if (env->IsInstanceOf(val, J_C_InternalID)) {
569+
return lbug_value_create_internal_id(getInternalID(env, val));
570+
} else if (env->IsInstanceOf(val, J_C_UUID)) {
571+
jstring uuid =
572+
static_cast<jstring>(callObjectMethodChecked(env, val, J_C_UUID_M_toString));
573+
std::string uuidString = jstringToUtf8String(env, uuid);
574+
return lbug_value_create_uuid(uuidString.c_str());
575+
} else if (env->IsInstanceOf(val, J_C_LocalDate)) {
576+
int64_t days =
577+
static_cast<int64_t>(callLongMethodChecked(env, val, J_C_LocalDate_M_toEpochDay));
578+
return lbug_value_create_date({.days = static_cast<int32_t>(days)});
579+
} else if (env->IsInstanceOf(val, J_C_Instant)) {
580+
// TODO: Need to review this for overflow
581+
int64_t seconds = static_cast<int64_t>(
582+
callLongMethodChecked(env, val, J_C_LocalDate_M_getEpochSecond));
583+
int64_t nano =
584+
static_cast<int64_t>(callLongMethodChecked(env, val, J_C_LocalDate_M_getNano));
585+
int64_t micro = (seconds * 1000000L) + (nano / 1000L);
586+
return lbug_value_create_timestamp({.value = micro});
587+
} else if (env->IsInstanceOf(val, J_C_Duration)) {
588+
auto milis = callLongMethodChecked(env, val, J_C_Duration_M_toMillis);
589+
return lbug_value_create_interval({.months = 0, .days = 0, .micros = milis * 1000L});
590+
}
591+
return nullptr;
592+
}
593+
594+
// Get the fully-qualified class name of a Java object as a std::string.
595+
// Returns "<unknown>" on error.
596+
static std::string getClassName(JNIEnv* env, jobject obj) {
597+
jclass clazz = env->GetObjectClass(obj);
598+
if (clazz == nullptr) return "<unknown>";
599+
jclass classClass = env->FindClass("java/lang/Class");
600+
if (classClass == nullptr) {
601+
env->DeleteLocalRef(clazz);
602+
return "<unknown>";
603+
}
604+
jmethodID getNameMethod =
605+
env->GetMethodID(classClass, "getName", "()Ljava/lang/String;");
606+
if (getNameMethod == nullptr) {
607+
env->DeleteLocalRef(classClass);
608+
env->DeleteLocalRef(clazz);
609+
return "<unknown>";
610+
}
611+
jstring classNameJStr =
612+
static_cast<jstring>(env->CallObjectMethod(clazz, getNameMethod));
613+
std::string result = jstringToUtf8String(env, classNameJStr);
614+
env->DeleteLocalRef(classNameJStr);
615+
env->DeleteLocalRef(classClass);
616+
env->DeleteLocalRef(clazz);
617+
return result;
618+
}
619+
507620
void bindJavaParamsToPreparedStatement(JNIEnv* env, lbug_prepared_statement* preparedStatement,
508621
jobject javaMap) {
509622
jobject set = callObjectMethodChecked(env, javaMap, J_C_Map_M_entrySet);
@@ -513,7 +626,33 @@ void bindJavaParamsToPreparedStatement(JNIEnv* env, lbug_prepared_statement* pre
513626
jstring key = (jstring)callObjectMethodChecked(env, entry, J_C_Map$Entry_M_getKey);
514627
jobject value = callObjectMethodChecked(env, entry, J_C_Map$Entry_M_getValue);
515628
std::string keyStr = jstringToUtf8String(env, key);
516-
auto* clonedValue = lbug_value_clone(getValue(env, value));
629+
630+
lbug_value* clonedValue = nullptr;
631+
632+
if (env->IsInstanceOf(value, J_C_Value)) {
633+
// Already a Value — use the direct JNI path (Option A, existing behaviour)
634+
clonedValue = lbug_value_clone(getValue(env, value));
635+
} else {
636+
// Try conversion from a boxed Java type via the same ladder as
637+
// Java_com_ladybugdb_Native_lbugValueCreateValue (Option A)
638+
clonedValue = javaObjectToValue(env, value);
639+
if (clonedValue == nullptr) {
640+
// Not a recognised type — throw IllegalArgumentException instead
641+
// of crashing (Option B)
642+
std::string typeName = getClassName(env, value);
643+
env->DeleteLocalRef(entry);
644+
env->DeleteLocalRef(key);
645+
env->DeleteLocalRef(value);
646+
throwIllegalArgumentException(env,
647+
("Parameter '" + keyStr + "' has unsupported type " + typeName
648+
+ ". Accepted types: Value, Boolean, Byte, Short, Integer, "
649+
"Long, BigInteger, Float, Double, BigDecimal, String, "
650+
"InternalID, UUID, LocalDate, Instant, Duration")
651+
.c_str());
652+
return;
653+
}
654+
}
655+
517656
auto state =
518657
lbug_prepared_statement_bind_value(preparedStatement, keyStr.c_str(), clonedValue);
519658
lbug_value_destroy(clonedValue);
@@ -769,6 +908,9 @@ JNIEXPORT jobject JNICALL Java_com_ladybugdb_Native_lbugConnectionExecute(JNIEnv
769908
auto* conn = getConnection(env, thisConn);
770909
auto* ps = getPreparedStatement(env, preStm);
771910
bindJavaParamsToPreparedStatement(env, ps, paramMap);
911+
if (env->ExceptionCheck()) {
912+
return jobject();
913+
}
772914
auto* queryResult = new lbug_query_result();
773915
lbug_state state = lbug_connection_execute(conn, ps, queryResult);
774916
if (state != LbugSuccess && queryResult->_query_result == nullptr) {

‎src/main/java/com/lbugdb/Connection.java‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,24 @@ public PreparedStatement prepare(String queryStr) {
107107
/**
108108
* Executes the given prepared statement with args and returns the result.
109109
*
110-
* @param ps: The prepared statement to execute.
111-
* @param m: The parameter pack where each arg is a std::pair with the first element being parameter name and second
112-
* element being parameter value
110+
* <p>Values that are not already {@link Value} instances are automatically converted from
111+
* their boxed Java type (e.g. {@link String}, {@link Long}, {@link java.util.UUID}, etc.).
112+
* If a value's type is not supported, an {@link IllegalArgumentException} is thrown instead
113+
* of crashing the JVM.
114+
*
115+
* @param ps The prepared statement to execute.
116+
* @param params The parameter map. Each value must be a {@link Value} or one of the
117+
* supported boxed types: Boolean, Byte, Short, Integer, Long, BigInteger,
118+
* Float, Double, BigDecimal, String, InternalID, UUID, LocalDate, Instant,
119+
* Duration.
113120
* @return The result of the query.
114-
* @throws RuntimeException If the connection has been destroyed.
121+
* @throws RuntimeException If the connection has been destroyed.
122+
* @throws IllegalArgumentException If a parameter value has an unsupported type.
115123
*/
116-
public QueryResult execute(PreparedStatement ps, Map<String, Value> m) {
124+
@SuppressWarnings("unchecked")
125+
public QueryResult execute(PreparedStatement ps, Map<String, ?> params) {
117126
checkNotDestroyed();
118-
return Native.lbugConnectionExecute(this, ps, m);
127+
return Native.lbugConnectionExecute(this, ps, (Map<String, Value>) (Map<?, ?>) params);
119128
}
120129

121130
/**

‎src/test/java/com/lbugdb/PreparedStatementTest.java‎

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
import static org.junit.jupiter.api.Assertions.*;
66

7-
import java.util.Map;
7+
import java.util.*;
8+
import java.util.stream.Collectors;
89

910
public class PreparedStatementTest extends TestBase {
1011

@@ -61,4 +62,115 @@ void PrepStmtSpecialString() {
6162
}
6263
}
6364

65+
@Test
66+
void executeWithRawBoxedString() {
67+
// Option A: raw String value auto-converted by C++ layer
68+
String query = "MATCH (n:person) WHERE n.fName = $1 RETURN n.fName";
69+
try (PreparedStatement ps = conn.prepare(query)) {
70+
Map<String, Object> raw = new HashMap<>();
71+
raw.put("1", "Alice");
72+
@SuppressWarnings("unchecked")
73+
Map<String, ?> params = (Map<String, ?>) (Map<?, ?>) raw;
74+
QueryResult result = conn.execute(ps, params);
75+
assertTrue(result.isSuccess());
76+
assertTrue(result.hasNext());
77+
String got = result.getNext().getValue(0).getValue();
78+
assertEquals("Alice", got);
79+
}
80+
}
81+
82+
@Test
83+
void executeWithRawBoxedLong() {
84+
// Option A: raw Long value auto-converted by C++ layer
85+
String query = "MATCH (n:person) WHERE n.ID = $1 RETURN n.fName";
86+
try (PreparedStatement ps = conn.prepare(query)) {
87+
Map<String, Object> raw = new HashMap<>();
88+
raw.put("1", 0L);
89+
@SuppressWarnings("unchecked")
90+
Map<String, ?> params = (Map<String, ?>) (Map<?, ?>) raw;
91+
QueryResult result = conn.execute(ps, params);
92+
assertTrue(result.isSuccess());
93+
assertTrue(result.hasNext());
94+
String got = result.getNext().getValue(0).getValue();
95+
assertEquals("Alice", got);
96+
}
97+
}
98+
99+
@Test
100+
void executeWithRawBoxedDouble() {
101+
// Option A: raw Double value auto-converted
102+
String query = "MATCH (n:person) WHERE n.eyeSight = $1 RETURN n.fName";
103+
try (PreparedStatement ps = conn.prepare(query)) {
104+
Map<String, Object> raw = new HashMap<>();
105+
raw.put("1", 5.0);
106+
@SuppressWarnings("unchecked")
107+
Map<String, ?> params = (Map<String, ?>) (Map<?, ?>) raw;
108+
QueryResult result = conn.execute(ps, params);
109+
assertTrue(result.isSuccess());
110+
assertTrue(result.hasNext());
111+
String got = result.getNext().getValue(0).getValue();
112+
assertEquals("Alice", got);
113+
}
114+
}
115+
116+
@Test
117+
void executeWithRawBoxedBoolean() {
118+
// Option A: raw Boolean value auto-converted
119+
String query = "MATCH (n:person) WHERE n.isStudent = $1 RETURN n.fName";
120+
try (PreparedStatement ps = conn.prepare(query)) {
121+
Map<String, Object> raw = new HashMap<>();
122+
raw.put("1", true);
123+
@SuppressWarnings("unchecked")
124+
Map<String, ?> params = (Map<String, ?>) (Map<?, ?>) raw;
125+
QueryResult result = conn.execute(ps, params);
126+
assertTrue(result.isSuccess());
127+
assertTrue(result.hasNext());
128+
}
129+
}
130+
131+
@Test
132+
void executeWithUnsupportedTypeThrows() {
133+
// Option B: unsupported type throws IllegalArgumentException instead of crashing
134+
String query = "MATCH (n:person) WHERE n.fName = $1 RETURN n.fName";
135+
try (PreparedStatement ps = conn.prepare(query)) {
136+
Map<String, Object> raw = new HashMap<>();
137+
raw.put("1", new ArrayList<>(List.of("not", "supported")));
138+
@SuppressWarnings("unchecked")
139+
Map<String, ?> params = (Map<String, ?>) (Map<?, ?>) raw;
140+
assertThrows(IllegalArgumentException.class, () -> {
141+
conn.execute(ps, params);
142+
});
143+
}
144+
}
145+
146+
@Test
147+
void executeWithValueWrappedParamsRegression() {
148+
// Regression: Value-wrapped path is unchanged
149+
String query = "MATCH (n:person) WHERE n.fName = $1 RETURN n.fName";
150+
try (PreparedStatement ps = conn.prepare(query)) {
151+
Map<String, Value> params = Map.of("1", new Value("Alice"));
152+
QueryResult result = conn.execute(ps, params);
153+
assertTrue(result.isSuccess());
154+
assertTrue(result.hasNext());
155+
String got = result.getNext().getValue(0).getValue();
156+
assertEquals("Alice", got);
157+
}
158+
}
159+
160+
@Test
161+
void executeWithMixedRawAndValueParams() {
162+
// Option A: mixed raw and Value-wrapped params
163+
String query = "MATCH (n:person) WHERE n.fName = $1 AND n.age = $2 RETURN n.fName";
164+
try (PreparedStatement ps = conn.prepare(query)) {
165+
Map<String, Object> raw = new HashMap<>();
166+
raw.put("1", new Value("Alice"));
167+
raw.put("2", 35L);
168+
@SuppressWarnings("unchecked")
169+
Map<String, ?> params = (Map<String, ?>) (Map<?, ?>) raw;
170+
QueryResult result = conn.execute(ps, params);
171+
assertTrue(result.isSuccess());
172+
assertTrue(result.hasNext());
173+
}
174+
}
175+
64176
}

0 commit comments

Comments
 (0)