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 @@ -57,6 +57,8 @@
import org.apache.arrow.driver.jdbc.utils.SqlTypes;
import org.apache.arrow.driver.jdbc.utils.VectorSchemaRootTransformer;
import org.apache.arrow.flight.FlightInfo;
import org.apache.arrow.flight.FlightRuntimeException;
import org.apache.arrow.flight.FlightStatusCode;
import org.apache.arrow.flight.sql.FlightSqlColumnMetadata;
import org.apache.arrow.flight.sql.FlightSqlProducer.Schemas;
import org.apache.arrow.flight.sql.impl.FlightSql.SqlInfo;
Expand All @@ -72,10 +74,13 @@
import org.apache.arrow.flight.sql.impl.FlightSql.SupportedAnsi92SqlGrammarLevel;
import org.apache.arrow.flight.sql.impl.FlightSql.SupportedSqlGrammar;
import org.apache.arrow.memory.BufferAllocator;
import org.apache.arrow.vector.BitVector;
import org.apache.arrow.vector.IntVector;
import org.apache.arrow.vector.SmallIntVector;
import org.apache.arrow.vector.VarBinaryVector;
import org.apache.arrow.vector.VarCharVector;
import org.apache.arrow.vector.VectorSchemaRoot;
import org.apache.arrow.vector.complex.ListVector;
import org.apache.arrow.vector.extension.UuidType;
import org.apache.arrow.vector.ipc.ReadChannel;
import org.apache.arrow.vector.ipc.message.MessageSerializer;
Expand Down Expand Up @@ -147,6 +152,27 @@ public class ArrowDatabaseMetadata extends AvaticaDatabaseMetaData {
Field.nullable("SOURCE_DATA_TYPE", Types.MinorType.SMALLINT.getType()),
Field.notNullable("IS_AUTOINCREMENT", Types.MinorType.VARCHAR.getType()),
Field.notNullable("IS_GENERATEDCOLUMN", Types.MinorType.VARCHAR.getType())));
private static final Schema GET_TYPE_INFO_SCHEMA =
new Schema(
Arrays.asList(
Field.nullable("TYPE_NAME", Types.MinorType.VARCHAR.getType()),
Field.nullable("DATA_TYPE", Types.MinorType.INT.getType()),
Field.nullable("PRECISION", Types.MinorType.INT.getType()),
Field.nullable("LITERAL_PREFIX", Types.MinorType.VARCHAR.getType()),
Field.nullable("LITERAL_SUFFIX", Types.MinorType.VARCHAR.getType()),
Field.nullable("CREATE_PARAMS", Types.MinorType.VARCHAR.getType()),
Field.nullable("NULLABLE", Types.MinorType.SMALLINT.getType()),
Field.nullable("CASE_SENSITIVE", Types.MinorType.BIT.getType()),
Field.nullable("SEARCHABLE", Types.MinorType.SMALLINT.getType()),
Field.nullable("UNSIGNED_ATTRIBUTE", Types.MinorType.BIT.getType()),
Field.nullable("FIXED_PREC_SCALE", Types.MinorType.BIT.getType()),
Field.nullable("AUTO_INCREMENT", Types.MinorType.BIT.getType()),
Field.nullable("LOCAL_TYPE_NAME", Types.MinorType.VARCHAR.getType()),
Field.nullable("MINIMUM_SCALE", Types.MinorType.SMALLINT.getType()),
Field.nullable("MAXIMUM_SCALE", Types.MinorType.SMALLINT.getType()),
Field.nullable("SQL_DATA_TYPE", Types.MinorType.INT.getType()),
Field.nullable("SQL_DATETIME_SUB", Types.MinorType.INT.getType()),
Field.nullable("NUM_PREC_RADIX", Types.MinorType.INT.getType())));
private final AtomicBoolean isCachePopulated = new AtomicBoolean(false);
private final Map<SqlInfo, Object> cachedSqlInfo = new EnumMap<>(SqlInfo.class);
private static final Map<Integer, Integer> sqlTypesToFlightEnumConvertTypes = new HashMap<>();
Expand Down Expand Up @@ -1001,6 +1027,200 @@ public ResultSet getPrimaryKeys(final String catalog, final String schema, final
connection, flightInfoPrimaryKeys, transformer);
}

@Override
public ResultSet getTypeInfo() throws SQLException {
final ArrowFlightConnection connection = getConnection();
final BufferAllocator allocator = connection.getBufferAllocator();

final FlightInfo flightInfoTypeInfo;
try {
flightInfoTypeInfo = connection.getClientHandler().getXdbcTypeInfo();
} catch (final FlightRuntimeException e) {
if (e.status().code() == FlightStatusCode.UNIMPLEMENTED) {
// Compatible servers may not implement GetXdbcTypeInfo at all; keep the empty JDBC result.
return super.getTypeInfo();
}
throw new SQLException("Failed to retrieve type info.", e);
Comment on lines +1037 to +1043

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes the behavior. getTypeInfo() used to return an empty 18-column result set without any RPC. Now any FlightRuntimeException becomes a SQLException, including UNIMPLEMENTED from servers that don't support CommandGetXdbcTypeInfo. BI tools and ORMs commonly call getTypeInfo() while setting up a connection, so those servers would now fail to connect.

Could we catch CallStatus.UNIMPLEMENTED here (and later), and return super.getTypeInfo() as before?
Only other failures should surface as SQLException.

Maybe we could add a test using setTypeInfoFlightInfoFailure(CallStatus.UNIMPLEMENTED..).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in 284386d: both getFlightInfo and stream initialization now fall back to super.getTypeInfo() for UNIMPLEMENTED. Other Flight failures still become SQLException.

Two real Flight-server regressions check the empty 18-column JDBC schema, no rows and allocator buffers returning to baseline. With identical final test bytes, the reviewed production revision failed those two cases; the fix passes all 11 focused tests. Local affected-reactor verification also passed: Flight SQL 97 tests; JDBC Core 1255 records, including 44 existing skips; packaged driver validation 2 integration tests. Upstream CI results for this follow-up remain pending.

AI-assisted patch and reply using DeepSeek and Codex; the tests above were executed locally.

}

try {
final ArrowFlightJdbcFlightStreamResultSet resultSet =
ArrowFlightJdbcFlightStreamResultSet.fromFlightInfo(
connection,
flightInfoTypeInfo,
(originalRoot, transformedRoot) -> {
if (transformedRoot == null) {
transformedRoot = VectorSchemaRoot.create(GET_TYPE_INFO_SCHEMA, allocator);
}
transformTypeInfoRoot(originalRoot, transformedRoot);
originalRoot.clear();
return transformedRoot;
});
// Empty streams do not initialize a cursor; retain the standard JDBC metadata columns.
if (resultSet.getMetaData().getColumnCount() == 0) {
resultSet.close();
return super.getTypeInfo();
}
return resultSet;
} catch (final FlightRuntimeException e) {
if (e.status().code() == FlightStatusCode.UNIMPLEMENTED) {
// The stream handshake may be the first call to reveal an unsupported GetXdbcTypeInfo.
return super.getTypeInfo();
}
throw new SQLException("Failed to retrieve type info.", e);
}
}

/**
* Maps a {@code CommandGetXdbcTypeInfo} root onto the 18 JDBC {@code getTypeInfo()} columns.
*
* <p>The rows are kept in the order the server sent them (including the server preference for
* types sharing a {@code DATA_TYPE}), the Flight {@code interval_precision} field is dropped as
* it has no JDBC counterpart, {@code create_params} is joined into a comma separated string, and
* {@code NULLABLE}, {@code SEARCHABLE}, {@code MINIMUM_SCALE} and {@code MAXIMUM_SCALE} are
* reported as nullable {@code SMALLINT} as required by the JDBC specification. No value is
* fabricated: SQL {@code NULL} stays {@code NULL}.
*
* @param originalRoot the root received from the server.
* @param transformedRoot the root the JDBC columns are written to; reused across batches.
*/
private static void transformTypeInfoRoot(
final VectorSchemaRoot originalRoot, final VectorSchemaRoot transformedRoot) {
final int rowCount = originalRoot.getRowCount();
if (rowCount == 0) {
transformedRoot.setRowCount(0);
return;
}

final VarCharVector typeName = (VarCharVector) transformedRoot.getVector("TYPE_NAME");
final IntVector dataType = (IntVector) transformedRoot.getVector("DATA_TYPE");
final IntVector precision = (IntVector) transformedRoot.getVector("PRECISION");
final VarCharVector literalPrefix = (VarCharVector) transformedRoot.getVector("LITERAL_PREFIX");
final VarCharVector literalSuffix = (VarCharVector) transformedRoot.getVector("LITERAL_SUFFIX");
final VarCharVector createParams = (VarCharVector) transformedRoot.getVector("CREATE_PARAMS");
final SmallIntVector nullable = (SmallIntVector) transformedRoot.getVector("NULLABLE");
final BitVector caseSensitive = (BitVector) transformedRoot.getVector("CASE_SENSITIVE");
final SmallIntVector searchable = (SmallIntVector) transformedRoot.getVector("SEARCHABLE");
final BitVector unsignedAttribute = (BitVector) transformedRoot.getVector("UNSIGNED_ATTRIBUTE");
final BitVector fixedPrecScale = (BitVector) transformedRoot.getVector("FIXED_PREC_SCALE");
final BitVector autoIncrement = (BitVector) transformedRoot.getVector("AUTO_INCREMENT");
final VarCharVector localTypeName =
(VarCharVector) transformedRoot.getVector("LOCAL_TYPE_NAME");
final SmallIntVector minimumScale = (SmallIntVector) transformedRoot.getVector("MINIMUM_SCALE");
final SmallIntVector maximumScale = (SmallIntVector) transformedRoot.getVector("MAXIMUM_SCALE");
final IntVector sqlDataType = (IntVector) transformedRoot.getVector("SQL_DATA_TYPE");
final IntVector sqlDatetimeSub = (IntVector) transformedRoot.getVector("SQL_DATETIME_SUB");
final IntVector numPrecRadix = (IntVector) transformedRoot.getVector("NUM_PREC_RADIX");

final VarCharVector serverTypeName = (VarCharVector) originalRoot.getVector("type_name");
final IntVector serverDataType = (IntVector) originalRoot.getVector("data_type");
final IntVector serverColumnSize = (IntVector) originalRoot.getVector("column_size");
final VarCharVector serverLiteralPrefix =
(VarCharVector) originalRoot.getVector("literal_prefix");
final VarCharVector serverLiteralSuffix =
(VarCharVector) originalRoot.getVector("literal_suffix");
final ListVector serverCreateParams = (ListVector) originalRoot.getVector("create_params");
final IntVector serverNullable = (IntVector) originalRoot.getVector("nullable");
final BitVector serverCaseSensitive = (BitVector) originalRoot.getVector("case_sensitive");
final IntVector serverSearchable = (IntVector) originalRoot.getVector("searchable");
final BitVector serverUnsignedAttribute =
(BitVector) originalRoot.getVector("unsigned_attribute");
final BitVector serverFixedPrecScale = (BitVector) originalRoot.getVector("fixed_prec_scale");
final BitVector serverAutoIncrement = (BitVector) originalRoot.getVector("auto_increment");
final VarCharVector serverLocalTypeName =
(VarCharVector) originalRoot.getVector("local_type_name");
final IntVector serverMinimumScale = (IntVector) originalRoot.getVector("minimum_scale");
final IntVector serverMaximumScale = (IntVector) originalRoot.getVector("maximum_scale");
final IntVector serverSqlDataType = (IntVector) originalRoot.getVector("sql_data_type");
final IntVector serverDatetimeSubcode = (IntVector) originalRoot.getVector("datetime_subcode");
final IntVector serverNumPrecRadix = (IntVector) originalRoot.getVector("num_prec_radix");

for (int i = 0; i < rowCount; i++) {
copyVarChar(serverTypeName, typeName, i);
copyInt(serverDataType, dataType, i);
copyInt(serverColumnSize, precision, i);
copyVarChar(serverLiteralPrefix, literalPrefix, i);
copyVarChar(serverLiteralSuffix, literalSuffix, i);
copyCreateParams(serverCreateParams, createParams, i);
copyIntAsSmallInt(serverNullable, nullable, i);
copyBit(serverCaseSensitive, caseSensitive, i);
copyIntAsSmallInt(serverSearchable, searchable, i);
copyBit(serverUnsignedAttribute, unsignedAttribute, i);
copyBit(serverFixedPrecScale, fixedPrecScale, i);
copyBit(serverAutoIncrement, autoIncrement, i);
copyVarChar(serverLocalTypeName, localTypeName, i);
copyIntAsSmallInt(serverMinimumScale, minimumScale, i);
copyIntAsSmallInt(serverMaximumScale, maximumScale, i);
copyInt(serverSqlDataType, sqlDataType, i);
copyInt(serverDatetimeSubcode, sqlDatetimeSub, i);
copyInt(serverNumPrecRadix, numPrecRadix, i);
}

transformedRoot.setRowCount(rowCount);
}

private static void copyInt(final IntVector source, final IntVector target, final int index) {
if (source.isNull(index)) {
target.setNull(index);
} else {
target.setSafe(index, source.get(index));
}
}

private static void copyIntAsSmallInt(
final IntVector source, final SmallIntVector target, final int index) {
if (source.isNull(index)) {
target.setNull(index);
} else {
target.setSafe(index, (short) source.get(index));
}
}

private static void copyBit(final BitVector source, final BitVector target, final int index) {
if (source.isNull(index)) {
target.setNull(index);
} else {
target.setSafe(index, source.get(index));
}
}

private static void copyVarChar(
final VarCharVector source, final VarCharVector target, final int index) {
if (source.isNull(index)) {
target.setNull(index);
} else {
target.setSafe(index, source.get(index));
}
}

/**
* Joins the {@code create_params} list into a comma separated string, preserving the difference
* between a SQL {@code NULL} list (reported as {@code NULL}) and an empty list (reported as an
* empty string).
*/
private static void copyCreateParams(
final ListVector source, final VarCharVector target, final int index) {
if (source.isNull(index)) {
target.setNull(index);
return;
}

final VarCharVector items = (VarCharVector) source.getDataVector();
final int start = source.getElementStartIndex(index);
final int end = source.getElementEndIndex(index);
final StringBuilder joined = new StringBuilder();
for (int i = start; i < end; i++) {
if (i > start) {
joined.append(',');
}
final Text item = items.getObject(i);
if (item != null) {
joined.append(item.toString());
}
}
target.setSafe(index, new Text(joined.toString()));
}

@Override
public ResultSet getColumns(
final String catalog,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,15 @@ public FlightInfo getSqlInfo(SqlInfo... info) {
return sqlClient.getSqlInfo(info, getOptions());
}

/**
* Makes an RPC "getXdbcTypeInfo" request based on the provided info.
*
* @return a {@code FlightInfo} of results.
*/
public FlightInfo getXdbcTypeInfo() {
return sqlClient.getXdbcTypeInfo(getOptions());
}

/**
* Makes an RPC "getPrimaryKeys" request based on the provided info.
*
Expand Down
Loading
Loading