Skip to content

Commit 09593d6

Browse files
committed
fix: provide default getObject(Class) fallback in QueryJDBCAccessor
QueryJDBCAccessor.getObject(Class) threw "Operation not supported" no matter what. That's a problem: JDBC says ResultSet.getObject(int, Class<T>) is supposed to return the value as T when the conversion is trivial, but every accessor that didn't override it would throw, even on the identity case like getObject(col, String.class) against a VARCHAR. Callers had to either know which accessors implement typed conversion or wrap calls in catch-and-retry. The new default does the obvious thing: reject null type with SQLState 22023, fetch the raw object, return null if it's null, return it cast to T if type.isInstance(raw), otherwise throw a typed conversion error. Accessors with richer needs (timestamp accessors return Instant, OffsetDateTime, etc.) keep their overrides. TimeStampVectorAccessor and TimeStampTZVectorAccessor are the two that have them today. Tests in StreamingResultSetMethodTest cover: - getObjectWithClassUsesAccessorBaseFallback: identity case (VARCHAR -> String) returns the value via the inherited fallback. - getObjectWithNullClassThrows: null type parameter raises SQLException with the expected "must not be null" message. - getObjectWithIncompatibleClassThrows: requesting an unrelated type (String column, StringBuilder asked) raises the typed conversion error rather than returning null or the raw value. - getObjectWithClassReturnsNullForNullValue: a null column value short-circuits and returns null regardless of the requested type; wasNull() is true afterwards. The existing QueryJDBCAccessorTest still passes since it doesn't touch getObject(Class). Null-type rejection is also pinned by TimeStampVectorAccessorTest and TimeZoneIntegrationTest.testGetObjectWithNullTypeThrows.
1 parent 092ac4d commit 09593d6

2 files changed

Lines changed: 83 additions & 4 deletions

File tree

jdbc-core/src/main/java/com/salesforce/datacloud/jdbc/core/accessor/QueryJDBCAccessor.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,28 @@ public Reader getNCharacterStream() throws SQLException {
193193
throw getOperationNotSupported(this.getClass());
194194
}
195195

196-
@Override
197-
public <T> T getObject(Class<T> aClass) throws SQLException {
198-
throw getOperationNotSupported(aClass.getClass());
196+
/**
197+
* Default {@code getObject(Class)} implementation: take the raw object produced by
198+
* {@link #getObject()} and return it if {@code type.isInstance(raw)}, otherwise raise a
199+
* conversion error. Accessors that need richer conversion (e.g. an Arrow {@code TIMESTAMP}
200+
* vector returning {@link java.time.Instant} / {@link java.time.OffsetDateTime}) override
201+
* this; everyone else inherits this generic path so callers do not need to special-case
202+
* "accessor implements typed getObject" vs "accessor does not".
203+
*/
204+
@Override
205+
public <T> T getObject(Class<T> type) throws SQLException {
206+
if (type == null) {
207+
throw new SQLException("type parameter must not be null", "22023");
208+
}
209+
Object raw = getObject();
210+
if (raw == null) {
211+
return null;
212+
}
213+
if (type.isInstance(raw)) {
214+
return type.cast(raw);
215+
}
216+
throw new SQLException("Cannot convert column value to " + type.getName() + "; actual type is "
217+
+ raw.getClass().getName());
199218
}
200219

201220
private static SQLException getOperationNotSupported(final Class<?> type) {

jdbc-core/src/test/java/com/salesforce/datacloud/jdbc/core/StreamingResultSetMethodTest.java

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,24 @@ class StreamingResultSetMethodTest {
3838

3939
@SneakyThrows
4040
private StreamingResultSet createResultSet() {
41+
return createSingleVarCharResultSet(false);
42+
}
43+
44+
@SneakyThrows
45+
private StreamingResultSet createResultSetWithNullValue() {
46+
return createSingleVarCharResultSet(true);
47+
}
48+
49+
@SneakyThrows
50+
private StreamingResultSet createSingleVarCharResultSet(boolean nullValue) {
4151
val allocator = ext.getRootAllocator();
4252
val vector = new VarCharVector("col1", allocator);
4353
vector.allocateNew();
44-
vector.set(0, "hello".getBytes(StandardCharsets.UTF_8));
54+
if (nullValue) {
55+
vector.setNull(0);
56+
} else {
57+
vector.set(0, "hello".getBytes(StandardCharsets.UTF_8));
58+
}
4559
vector.setValueCount(1);
4660

4761
val root = new VectorSchemaRoot(Arrays.asList(vector.getField()), Arrays.asList(vector));
@@ -143,6 +157,52 @@ void methodsThrowAfterClose() throws Exception {
143157
.hasMessageContaining("closed");
144158
}
145159

160+
@Test
161+
void getObjectWithClassUsesAccessorBaseFallback() throws Exception {
162+
// VarCharVectorAccessor does not override getObject(Class); it inherits the default in
163+
// QueryJDBCAccessor that does raw + isInstance. Pin that this delivers a String for a
164+
// VARCHAR column — regressing the base-class fallback breaks every accessor that does
165+
// not implement typed conversion of its own.
166+
try (val rs = createResultSet()) {
167+
rs.next();
168+
assertThat(rs.getObject(1, String.class)).isEqualTo("hello");
169+
}
170+
}
171+
172+
@Test
173+
void getObjectWithNullClassThrows() throws Exception {
174+
try (val rs = createResultSet()) {
175+
rs.next();
176+
assertThatThrownBy(() -> rs.getObject(1, (Class<?>) null))
177+
.isInstanceOf(SQLException.class)
178+
.hasMessageContaining("must not be null");
179+
}
180+
}
181+
182+
@Test
183+
void getObjectWithIncompatibleClassThrows() throws Exception {
184+
// VarCharVectorAccessor returns a String. Asking for an unrelated type (StringBuilder
185+
// here) cannot be satisfied by isInstance, so the fallback should surface a typed
186+
// conversion error rather than silently returning null or the raw string.
187+
try (val rs = createResultSet()) {
188+
rs.next();
189+
assertThatThrownBy(() -> rs.getObject(1, StringBuilder.class))
190+
.isInstanceOf(SQLException.class)
191+
.hasMessageContaining("Cannot convert");
192+
}
193+
}
194+
195+
@Test
196+
void getObjectWithClassReturnsNullForNullValue() throws Exception {
197+
// A null column value should round-trip as null regardless of the requested type — the
198+
// fallback short-circuits before the isInstance check.
199+
try (val rs = createResultSetWithNullValue()) {
200+
rs.next();
201+
assertThat(rs.getObject(1, String.class)).isNull();
202+
assertThat(rs.wasNull()).isTrue();
203+
}
204+
}
205+
146206
@Test
147207
void queryId() throws Exception {
148208
try (val rs = createResultSet()) {

0 commit comments

Comments
 (0)