Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,53 @@ public Reader getNCharacterStream() throws SQLException {
throw getOperationNotSupported(this.getClass());
}

@Override
public <T> T getObject(Class<T> aClass) throws SQLException {
throw getOperationNotSupported(aClass.getClass());
/**
* Default {@code getObject(Class)} implementation.
*
* <p>Resolution order:
* <ol>
* <li>{@code null} type → {@code SQLException} with SQLState {@code 22023}.
* <li>{@link String} → delegate to {@link #getString()} (JDBC 4.2 Table B-5 mandates this
* conversion for every column type).
* <li>Otherwise, take the raw object from {@link #getObject()} and return it via
* {@code type.isInstance} when it fits — covering identity, supertype and interface
* matches.
* <li>Anything left throws {@link SQLFeatureNotSupportedException}.
* </ol>
*
* <p>Numeric narrowing (e.g. {@code Integer.class} on a BIGINT column) is intentionally
* <em>not</em> handled here: the accessor-level primitive getters use unchecked Java casts,
* so silently truncating a {@code Long.MAX_VALUE} to {@code -1} would be the wrong default.
* Accessors that need lossless cross-type conversion override this method
* (see {@link com.salesforce.datacloud.jdbc.core.accessor.impl.TimeStampVectorAccessor} for
* the timestamp → {@link java.time.Instant} / {@link java.time.OffsetDateTime} path).
*/
@Override
public <T> T getObject(Class<T> type) throws SQLException {
if (type == null) {
throw new SQLException("type parameter must not be null", "22023");
}

// String works on every column type per JDBC 4.2 Table B-5; handle it before the
// raw-object fallback so callers don't depend on getObject() returning a String.
if (type == String.class) {
return type.cast(getString());
}

// Generic raw-object path: works whenever the column's natural Object representation
// is already an instance of the requested type (identity, supertype, or interface).
// Concrete accessors set wasNull inside getObject(); set it defensively here too so
// a callsite reading a null does not see stale state from an earlier non-null read.
Object raw = getObject();
if (raw == null) {
wasNull = true;
return null;
}
if (type.isInstance(raw)) {
return type.cast(raw);
}
throw new SQLFeatureNotSupportedException(
"Cannot convert column value of type " + raw.getClass().getName() + " to " + type.getName());
}

private static SQLException getOperationNotSupported(final Class<?> type) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,24 @@ class StreamingResultSetMethodTest {

@SneakyThrows
private StreamingResultSet createResultSet() {
return createSingleVarCharResultSet(false);
}

@SneakyThrows
private StreamingResultSet createResultSetWithNullValue() {
return createSingleVarCharResultSet(true);
}

@SneakyThrows
private StreamingResultSet createSingleVarCharResultSet(boolean nullValue) {
val allocator = ext.getRootAllocator();
val vector = new VarCharVector("col1", allocator);
vector.allocateNew();
vector.set(0, "hello".getBytes(StandardCharsets.UTF_8));
if (nullValue) {
vector.setNull(0);
} else {
vector.set(0, "hello".getBytes(StandardCharsets.UTF_8));
}
vector.setValueCount(1);

val root = new VectorSchemaRoot(Arrays.asList(vector.getField()), Arrays.asList(vector));
Expand Down Expand Up @@ -143,6 +157,65 @@ void methodsThrowAfterClose() throws Exception {
.hasMessageContaining("closed");
}

@Test
void getObjectWithClassUsesAccessorBaseFallback() throws Exception {
// VarCharVectorAccessor does not override getObject(Class); it inherits the default in
// QueryJDBCAccessor that does raw + isInstance. Pin that this delivers a String for a
// VARCHAR column — regressing the base-class fallback breaks every accessor that does
// not implement typed conversion of its own.
try (val rs = createResultSet()) {
rs.next();
assertThat(rs.getObject(1, String.class)).isEqualTo("hello");
}
}

@Test
void getObjectWithNullClassThrows() throws Exception {
try (val rs = createResultSet()) {
rs.next();
assertThatThrownBy(() -> rs.getObject(1, (Class<?>) null))
.isInstanceOf(SQLException.class)
.hasMessageContaining("must not be null");
}
}

@Test
void getObjectWithIncompatibleClassThrows() throws Exception {
// VarCharVectorAccessor returns a String. Asking for an unrelated type (StringBuilder
// here) cannot be satisfied by isInstance, so the fallback should surface a typed
// conversion error rather than silently returning null or the raw string.
try (val rs = createResultSet()) {
rs.next();
assertThatThrownBy(() -> rs.getObject(1, StringBuilder.class))
.isInstanceOf(SQLException.class)
.hasMessageContaining("Cannot convert");
}
}

@Test
void getObjectWithClassReturnsNullForNullValue() throws Exception {
// A null column value should round-trip as null regardless of the requested type — the
// fallback short-circuits before the isInstance check.
try (val rs = createResultSetWithNullValue()) {
rs.next();
assertThat(rs.getObject(1, String.class)).isNull();
assertThat(rs.wasNull()).isTrue();
}
}

@Test
void getObjectWithSupertypeOrInterfaceReturnsValue() throws Exception {
// The isInstance check accepts any supertype or interface the raw object implements,
// not just the exact runtime class. Polymorphic callers (e.g. Object.class for
// generic introspection, CharSequence.class for tools that don't care about
// String-vs-StringBuffer) need this to work.
try (val rs = createResultSet()) {
rs.next();
assertThat((String) rs.getObject(1, Object.class)).isEqualTo("hello");
assertThat(rs.getObject(1, CharSequence.class).toString()).isEqualTo("hello");
}
}

@Test
void queryId() throws Exception {
try (val rs = createResultSet()) {
Expand Down
Loading