From 9a2fed157ef0647e46c47b410af6c5a61416739d Mon Sep 17 00:00:00 2001 From: Jan Kadlec Date: Wed, 6 May 2026 12:13:26 +0200 Subject: [PATCH] GH-44: Clear signature columns before re-populating in ArrowFlightStatement#executeFlightInfoQuery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ArrowFlightStatement#executeFlightInfoQuery appended the dataset schema columns to the statement's reused Meta.Signature without first clearing them, doubling the column list on every invocation. When the FlightInfo has at least one endpoint, ArrowFlightJdbcVectorSchemaRootResultSet overwrites signature.columns from the actual stream schema and hides the duplication. With an empty endpoint list — reported against both Rust- and Denodo-based Flight SQL servers — that overwrite never runs and ResultSetMetaData#getColumnCount() reports 2x the schema width. Regression introduced in 15.0.0 (GH-33475 prepared-statement parameter binding) when handle.signature became mutable across executions. Adds a regression test that registers a mock query with no endpoints and asserts ResultSetMetaData#getColumnCount() matches the schema. Closes #44. --- .../driver/jdbc/ArrowFlightStatement.java | 1 + .../driver/jdbc/ResultSetMetadataTest.java | 19 +++++++++++++++++++ .../jdbc/utils/CoreMockedSqlProducers.java | 12 ++++++++++++ 3 files changed, 32 insertions(+) diff --git a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/ArrowFlightStatement.java b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/ArrowFlightStatement.java index 577aee3b4a..ff3d060c50 100644 --- a/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/ArrowFlightStatement.java +++ b/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/ArrowFlightStatement.java @@ -52,6 +52,7 @@ public FlightInfo executeFlightInfoQuery() throws SQLException { } final Schema resultSetSchema = preparedStatement.getDataSetSchema(); + signature.columns.clear(); signature.columns.addAll( ConvertUtils.convertArrowFieldsToColumnMetaDataList(resultSetSchema.getFields())); setSignature(signature); diff --git a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/ResultSetMetadataTest.java b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/ResultSetMetadataTest.java index 4583194f50..7000113100 100644 --- a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/ResultSetMetadataTest.java +++ b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/ResultSetMetadataTest.java @@ -220,4 +220,23 @@ public void testShouldIsSearchable() throws SQLException { public void testShouldGetColumnTypesFromOutOfBoundIndex() { assertThrows(IndexOutOfBoundsException.class, () -> metadata.getColumnType(4)); } + + /** + * Regression test for issue #44: + * {@code ArrowFlightStatement#executeFlightInfoQuery} appends schema columns to the reused {@code + * Meta.Signature} without clearing the existing list. When the result has at least one endpoint, + * the {@code FlightStream} consumption path overwrites {@code signature.columns} from the actual + * stream schema and hides the duplication. With an empty endpoint list — the scenario reported + * against both Rust- and Denodo-based Flight SQL servers — that overwrite never runs and {@code + * ResultSetMetaData#getColumnCount()} reports double the schema width. + */ + @Test + public void testShouldNotDuplicateColumnsWhenFlightInfoHasNoEndpoints() throws SQLException { + try (Connection conn = FLIGHT_SERVER_TEST_EXTENSION.getConnection(false); + Statement st = conn.createStatement(); + ResultSet rs = + st.executeQuery(CoreMockedSqlProducers.LEGACY_REGULAR_NO_ENDPOINTS_SQL_CMD)) { + assertThat(rs.getMetaData().getColumnCount(), equalTo(1)); + } + } } diff --git a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/CoreMockedSqlProducers.java b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/CoreMockedSqlProducers.java index 7c17755693..9c5d972b15 100644 --- a/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/CoreMockedSqlProducers.java +++ b/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/CoreMockedSqlProducers.java @@ -67,6 +67,8 @@ public final class CoreMockedSqlProducers { public static final String LEGACY_METADATA_SQL_CMD = "SELECT * FROM METADATA"; public static final String LEGACY_CANCELLATION_SQL_CMD = "SELECT * FROM TAKES_FOREVER"; public static final String LEGACY_REGULAR_WITH_EMPTY_SQL_CMD = "SELECT * FROM TEST_EMPTIES"; + public static final String LEGACY_REGULAR_NO_ENDPOINTS_SQL_CMD = + "SELECT * FROM TEST_NO_ENDPOINTS"; public static final String UUID_SQL_CMD = "SELECT * FROM UUID_TABLE"; public static final String UUID_PREPARED_SELECT_SQL_CMD = @@ -100,12 +102,22 @@ public static MockFlightSqlProducer getLegacyProducer() { addLegacyMetadataSqlCmdSupport(producer); addLegacyCancellationSqlCmdSupport(producer); addQueryWithEmbeddedEmptyRoot(producer); + addQueryWithNoEndpoints(producer); addUuidSqlCmdSupport(producer); addUuidPreparedSelectSqlCmdSupport(producer); addUuidPreparedUpdateSqlCmdSupport(producer); return producer; } + private static void addQueryWithNoEndpoints(final MockFlightSqlProducer producer) { + final Schema querySchema = + new Schema( + ImmutableList.of( + new Field("ID", new FieldType(true, new ArrowType.Int(64, true), null), null))); + producer.addSelectQuery( + LEGACY_REGULAR_NO_ENDPOINTS_SQL_CMD, querySchema, Collections.emptyList()); + } + /** * Gets a {@link MockFlightSqlProducer} configured with UUID test data. *