This is an automated email from the ASF dual-hosted git repository.
morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new f109e130f77 [fix](arrow-flight) Support table type metadata and
filters (#67899)
f109e130f77 is described below
commit f109e130f77c672f763a5082c9be856380bd32ae
Author: hutiefang76 <[email protected]>
AuthorDate: Tue Nov 3 02:21:21 2026 -0800
[fix](arrow-flight) Support table type metadata and filters (#67899)
### What problem does this PR solve?
Related to #67578 (partial A3).
Flight SQL clients can obtain FlightInfo for GetTableTypes, but reading
its ticket fails with UNIMPLEMENTED. GetTables also forwards only the
first table_types entry to a service that treats only VIEW as a filter:
[VIEW, BASE TABLE] loses base tables, while [BASE TABLE] or an unknown
type can return excluded rows.
Implement the type stream using the same Doris names as GetTables (BASE
TABLE, SYSTEM VIEW and VIEW), sorted according to Flight SQL. Match the
complete filter against the existing authorized listing before
collecting optional schemas. Request only name/type metadata to avoid
unused table statistics, and retain the session guard and Arrow buffer
cleanup on failed sends.
A type is the TABLE_TYPE that information_schema.tables reports for the
object: table streams are VIEW, and external catalogs report their
tables and views as BASE TABLE. The service lists every authorized
object and the filter runs on that listing. Requested names match
case-insensitively, and TABLE and SYSTEM TABLE, which Flight SQL calls
commonly supported, select BASE TABLE and SYSTEM VIEW: the ADBC Flight
SQL driver asks for `table` (Doris's own ADBC catalog lists a Doris
source with `getObjects(..., {"table"})`), and JDBC tools ask for
`TABLE`. Any other name matches only a type of that name, so UNKNOWN
selects nothing.
Because the filter now runs on the full listing,
`FrontendServiceImpl.listTableStatus` refreshes a cloud follower's
partition versions from the meta service only when UPDATE_TIME is
requested, the one column that reads them. GetTables, which asks for
names and types, never waits on those RPCs or is cut short when they
fail, and information_schema.tables scans that do not read UPDATE_TIME
skip the refresh too.
Co-authored-by: morningman <[email protected]>
Co-authored-by: Gabriel <[email protected]>
---
.../doris/arrowflight/DorisFlightSqlProducer.java | 13 +-
.../doris/arrowflight/FlightSqlSchemaHelper.java | 66 +++++++-
.../apache/doris/service/FrontendServiceImpl.java | 7 +-
.../FlightSqlSchemaHelperTableTypesTest.java | 177 +++++++++++++++++++++
.../doris/arrowflight/FlightSqlTableTypesTest.java | 151 ++++++++++++++++++
.../doris/service/FrontendServiceImplTest.java | 57 +++++++
.../test_table_types_over_arrow_flight.groovy | 141 ++++++++++++++++
7 files changed, 601 insertions(+), 11 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/arrowflight/DorisFlightSqlProducer.java
b/fe/fe-core/src/main/java/org/apache/doris/arrowflight/DorisFlightSqlProducer.java
index 5a7c1dd1f8b..a90c86f279f 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/arrowflight/DorisFlightSqlProducer.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/arrowflight/DorisFlightSqlProducer.java
@@ -619,7 +619,16 @@ public class DorisFlightSqlProducer implements
FlightSqlProducer, AutoCloseable
@Override
public void getStreamTableTypes(final CallContext context, final
ServerStreamListener listener) {
- throw CallStatus.UNIMPLEMENTED.withDescription("getStreamTableTypes
unimplemented").toRuntimeException();
+ streamMetadata(context, listener, connectContext -> {
+ try (VectorSchemaRoot vectorSchemaRoot = VectorSchemaRoot.create(
+ Schemas.GET_TABLE_TYPES_SCHEMA, rootAllocator)) {
+ listener.start(vectorSchemaRoot);
+ vectorSchemaRoot.allocateNew();
+ FlightSqlSchemaHelper.getTableTypes(vectorSchemaRoot);
+ listener.putNext();
+ listener.completed();
+ }
+ });
}
@Override
@@ -761,7 +770,7 @@ public class DorisFlightSqlProducer implements
FlightSqlProducer, AutoCloseable
void send(ConnectContext connectContext) throws Exception;
}
- // Answers a metadata request (catalogs, schemas, tables) from the
session's context, as one
+ // Answers a metadata request (catalogs, schemas, tables, table types)
from the session's context, as one
// command of that session.
private void streamMetadata(final CallContext context, final
ServerStreamListener listener,
MetadataStream stream) {
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/arrowflight/FlightSqlSchemaHelper.java
b/fe/fe-core/src/main/java/org/apache/doris/arrowflight/FlightSqlSchemaHelper.java
index 336c3dc7a36..f7db5af80ba 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/arrowflight/FlightSqlSchemaHelper.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/arrowflight/FlightSqlSchemaHelper.java
@@ -22,6 +22,7 @@ import org.apache.doris.catalog.ArrayType;
import org.apache.doris.catalog.Env;
import org.apache.doris.catalog.MapType;
import org.apache.doris.catalog.StructType;
+import org.apache.doris.catalog.TableIf.TableType;
import org.apache.doris.catalog.Type;
import org.apache.doris.datasource.CatalogIf;
import org.apache.doris.qe.ConnectContext;
@@ -64,18 +65,27 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
+import java.util.stream.Collectors;
public class FlightSqlSchemaHelper {
private static final Logger LOG =
LogManager.getLogger(FlightSqlSchemaHelper.class);
+ private static final List<String> TABLE_TYPES =
Arrays.stream(TableType.values())
+ .map(TableType::toMysqlType)
+ .filter(Objects::nonNull)
+ .distinct()
+ .sorted()
+ .collect(Collectors.toList());
private final ConnectContext ctx;
private final FrontendServiceImpl impl;
private boolean includeSchema;
private String catalogFilterPattern = null;
private String dbSchemaFilterPattern = null;
private String tableNameFilterPattern = null;
- private List<String> tableTypesList = null;
+ private Set<String> tableTypeFilter = null;
public FlightSqlSchemaHelper(ConnectContext context) {
ctx = context;
@@ -161,7 +171,28 @@ public class FlightSqlSchemaHelper {
catalogFilterPattern = command.hasCatalog() ? command.getCatalog() :
"internal";
dbSchemaFilterPattern = command.hasDbSchemaFilterPattern() ?
command.getDbSchemaFilterPattern() : null;
tableNameFilterPattern = command.hasTableNameFilterPattern() ?
command.getTableNameFilterPattern() : null;
- tableTypesList = command.getTableTypesList().isEmpty() ? null :
command.getTableTypesList();
+ tableTypeFilter = command.getTableTypesList().isEmpty() ? null
+ :
command.getTableTypesList().stream().map(FlightSqlSchemaHelper::toDorisTableType)
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * Maps a type a GetTables request filters on to the name GetTableTypes
advertises. Flight SQL leaves the
+ * names to the server but calls TABLE, VIEW and SYSTEM TABLE the commonly
supported ones, and clients send
+ * those in either case: the ADBC Flight SQL driver asks for "table", JDBC
tools for "TABLE". So a type
+ * matches case-insensitively, and TABLE and SYSTEM TABLE select BASE
TABLE and SYSTEM VIEW. Any other name
+ * matches only a type of that name.
+ */
+ private static String toDorisTableType(String requestedType) {
+ String type = requestedType.toUpperCase(Locale.ROOT);
+ switch (type) {
+ case "TABLE":
+ return "BASE TABLE";
+ case "SYSTEM TABLE":
+ return "SYSTEM VIEW";
+ default:
+ return type;
+ }
}
/**
@@ -199,9 +230,8 @@ public class FlightSqlSchemaHelper {
if (tableNameFilterPattern != null) {
getTablesParams.setPattern(tableNameFilterPattern);
}
- if (tableTypesList != null) {
- getTablesParams.setType(tableTypesList.get(0)); // currently only
one type is supported.
- }
+ // Flight SQL only consumes names and types, not the potentially
expensive table statistics.
+ getTablesParams.setRequiredColumns(Set.of("TABLE_NAME", "TABLE_TYPE"));
getTablesParams.setCurrentUserIdent(ctx.getCurrentUserIdentity().toThrift());
return impl.listTableStatus(getTablesParams);
}
@@ -318,6 +348,17 @@ public class FlightSqlSchemaHelper {
vectorSchemaRoot.setRowCount(getDbsResult.getDbs().size());
}
+ /**
+ * for FlightSqlProducer Schemas.GET_TABLE_TYPES_SCHEMA
+ */
+ public static void getTableTypes(VectorSchemaRoot vectorSchemaRoot) {
+ VarCharVector tableTypeVector = (VarCharVector)
vectorSchemaRoot.getVector("table_type");
+ for (int i = 0; i < TABLE_TYPES.size(); i++) {
+ tableTypeVector.setSafe(i, new Text(TABLE_TYPES.get(i)));
+ }
+ vectorSchemaRoot.setRowCount(TABLE_TYPES.size());
+ }
+
/**
* for FlightSqlProducer Schemas.GET_TABLES_SCHEMA_NO_SCHEMA and
Schemas.GET_TABLES_SCHEMA
*/
@@ -333,12 +374,21 @@ public class FlightSqlSchemaHelper {
for (int dbIndex = 0; dbIndex < getDbsResult.getDbs().size();
dbIndex++) {
String dbName = getDbsResult.getDbs().get(dbIndex);
String catalogName = getDbsResult.isSetCatalogs() ?
getDbsResult.getCatalogs().get(dbIndex) : "";
- TListTableStatusResult listTableStatusResult =
listTableStatus(dbName, catalogName);
+ List<TTableStatus> tables = listTableStatus(dbName,
catalogName).getTables();
+ if (tableTypeFilter != null) {
+ // The service's single-type filter only handles VIEW. Match
all requested types here
+ // against the authorized results, before collecting their
optional schemas.
+ tables = tables.stream().filter(table ->
tableTypeFilter.contains(table.getType()))
+ .collect(Collectors.toList());
+ }
+ if (tables.isEmpty()) {
+ continue;
+ }
Map<String, List<Field>> tableToFields;
if (includeSchema) {
List<String> tablesName = new ArrayList<>();
- for (TTableStatus tableStatus :
listTableStatusResult.getTables()) {
+ for (TTableStatus tableStatus : tables) {
tablesName.add(tableStatus.getName());
}
TDescribeTablesResult describeTablesResult =
describeTables(dbName, catalogName, tablesName);
@@ -347,7 +397,7 @@ public class FlightSqlSchemaHelper {
tableToFields = null;
}
- for (TTableStatus tableStatus : listTableStatusResult.getTables())
{
+ for (TTableStatus tableStatus : tables) {
catalogNameVector.setSafe(tablesCount, new Text(catalogName));
schemaNameVector.setSafe(tablesCount, new Text(dbName));
tableNameVector.setSafe(tablesCount, new
Text(tableStatus.getName()));
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/service/FrontendServiceImpl.java
b/fe/fe-core/src/main/java/org/apache/doris/service/FrontendServiceImpl.java
index 139a823ce51..9356c237b6f 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/service/FrontendServiceImpl.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/service/FrontendServiceImpl.java
@@ -756,8 +756,13 @@ public class FrontendServiceImpl implements
FrontendService.Iface {
// the version needs to be updated.
// Otherwise, the version will always be the old value
// unless there is a query for the table in the
follower node.
+ // UPDATE_TIME is the only status column that reads
the partition versions
+ // (OlapTable.getUpdateTime() takes the partitions'
visible version time), so a
+ // request that does not need it, such as Flight SQL
GetTables asking for names
+ // and types, does not wait on the meta service.
if (!Env.getCurrentEnv().isMaster() &&
Config.isCloudMode()
- && table instanceof OlapTable) {
+ && table instanceof OlapTable
+ && needTableStatusColumn(requiredColumns,
"UPDATE_TIME")) {
OlapTable olapTable = (OlapTable) table;
List<CloudPartition> partitions =
olapTable.getAllPartitions().stream()
.filter(p -> p instanceof CloudPartition)
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/arrowflight/FlightSqlSchemaHelperTableTypesTest.java
b/fe/fe-core/src/test/java/org/apache/doris/arrowflight/FlightSqlSchemaHelperTableTypesTest.java
new file mode 100644
index 00000000000..3f9cae0087a
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/arrowflight/FlightSqlSchemaHelperTableTypesTest.java
@@ -0,0 +1,177 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.arrowflight;
+
+import org.apache.doris.analysis.UserIdentity;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.service.ExecuteEnv;
+import org.apache.doris.service.FrontendServiceImpl;
+import org.apache.doris.thrift.TColumnDef;
+import org.apache.doris.thrift.TColumnDesc;
+import org.apache.doris.thrift.TDescribeTablesParams;
+import org.apache.doris.thrift.TDescribeTablesResult;
+import org.apache.doris.thrift.TGetDbsParams;
+import org.apache.doris.thrift.TGetDbsResult;
+import org.apache.doris.thrift.TGetTablesParams;
+import org.apache.doris.thrift.TListTableStatusResult;
+import org.apache.doris.thrift.TPrimitiveType;
+import org.apache.doris.thrift.TTableStatus;
+
+import org.apache.arrow.flight.sql.FlightSqlProducer.Schemas;
+import org.apache.arrow.flight.sql.impl.FlightSql.CommandGetTables;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.VarBinaryVector;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.ipc.ReadChannel;
+import org.apache.arrow.vector.ipc.message.MessageSerializer;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.Schema;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.MockedConstruction;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.io.ByteArrayInputStream;
+import java.nio.channels.Channels;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class FlightSqlSchemaHelperTableTypesTest {
+ private static Stream<Arguments> tableFilters() {
+ return Stream.of(false, true).flatMap(includeSchema -> Stream.of(
+ Arguments.of(includeSchema, Collections.emptyList(),
Arrays.asList("base", "system", "view")),
+ Arguments.of(includeSchema, Arrays.asList("VIEW", "BASE
TABLE"), Arrays.asList("base", "view")),
+ Arguments.of(includeSchema, Arrays.asList("BASE TABLE",
"VIEW"), Arrays.asList("base", "view")),
+ Arguments.of(includeSchema, Arrays.asList("BASE TABLE"),
Arrays.asList("base")),
+ Arguments.of(includeSchema, Arrays.asList("SYSTEM VIEW"),
Arrays.asList("system")),
+ Arguments.of(includeSchema, Arrays.asList("VIEW"),
Arrays.asList("view")),
+ Arguments.of(includeSchema, Arrays.asList("UNKNOWN"),
Collections.emptyList()),
+ Arguments.of(includeSchema, Arrays.asList("UNKNOWN", "VIEW",
"VIEW"), Arrays.asList("view")),
+ // The names Flight SQL calls common, in the spellings clients
send: the ADBC Flight SQL
+ // driver asks for "table", JDBC tools for "TABLE".
+ Arguments.of(includeSchema, Arrays.asList("table"),
Arrays.asList("base")),
+ Arguments.of(includeSchema, Arrays.asList("TABLE", "VIEW"),
Arrays.asList("base", "view")),
+ Arguments.of(includeSchema, Arrays.asList("view"),
Arrays.asList("view")),
+ Arguments.of(includeSchema, Arrays.asList("SYSTEM TABLE"),
Arrays.asList("system")),
+ Arguments.of(includeSchema, Arrays.asList("base table",
"System View"),
+ Arrays.asList("base", "system"))));
+ }
+
+ @ParameterizedTest
+ @MethodSource("tableFilters")
+ public void filtersAuthorizedTablesBeforeDescribing(boolean includeSchema,
List<String> types,
+ List<String> expectedNames) throws Exception {
+ checkTables("internal", includeSchema, types, expectedNames);
+ }
+
+ @Test
+ public void retainsExternalCatalogForFilteredSchemas() throws Exception {
+ checkTables("external", true, Arrays.asList("VIEW", "BASE TABLE"),
Arrays.asList("base", "view"));
+ }
+
+ private void checkTables(String catalog, boolean includeSchema,
List<String> types,
+ List<String> expectedNames) throws Exception {
+ ConnectContext ctx = Mockito.mock(ConnectContext.class);
+
Mockito.when(ctx.getCurrentUserIdentity()).thenReturn(UserIdentity.ROOT);
+ List<TTableStatus> authorizedTables = Arrays.asList(
+ new TTableStatus().setName("base").setType("BASE TABLE"),
+ new TTableStatus().setName("system").setType("SYSTEM VIEW"),
+ new TTableStatus().setName("view").setType("VIEW"));
+ List<String> describedNames = new ArrayList<>();
+ try (MockedStatic<ExecuteEnv> executeEnv =
Mockito.mockStatic(ExecuteEnv.class);
+ MockedConstruction<FrontendServiceImpl> services =
Mockito.mockConstruction(
+ FrontendServiceImpl.class, (service, construction) -> {
+
Mockito.when(service.getDbNames(Mockito.any(TGetDbsParams.class))).thenAnswer(invocation
-> {
+ TGetDbsParams params =
invocation.getArgument(0);
+ Assertions.assertEquals(catalog,
params.getCatalog());
+ Assertions.assertEquals("db%",
params.getPattern());
+
Assertions.assertEquals(UserIdentity.ROOT.toThrift(),
params.getCurrentUserIdent());
+ return new
TGetDbsResult().setDbs(Arrays.asList("db"))
+ .setCatalogs(Arrays.asList(catalog));
+ });
+
Mockito.when(service.listTableStatus(Mockito.any(TGetTablesParams.class)))
+ .thenAnswer(invocation -> {
+ TGetTablesParams params =
invocation.getArgument(0);
+ Assertions.assertEquals(catalog,
params.getCatalog());
+ Assertions.assertEquals("db",
params.getDb());
+ Assertions.assertEquals("%",
params.getPattern());
+
Assertions.assertEquals(UserIdentity.ROOT.toThrift(),
+ params.getCurrentUserIdent());
+ // Names and types only: no table
statistics, and no UPDATE_TIME, the one
+ // column a cloud follower refreshes
partition versions from the meta
+ // service for.
+
Assertions.assertEquals(Set.of("TABLE_NAME", "TABLE_TYPE"),
+ params.getRequiredColumns());
+ // The service only recognizes VIEW;
other types return all authorized tables.
+ return new
TListTableStatusResult().setTables(authorizedTables.stream()
+ .filter(table ->
!"VIEW".equals(params.getType())
+ ||
"VIEW".equals(table.getType()))
+ .collect(Collectors.toList()));
+ });
+
Mockito.when(service.describeTables(Mockito.any(TDescribeTablesParams.class)))
+ .thenAnswer(invocation -> {
+ TDescribeTablesParams params =
invocation.getArgument(0);
+
describedNames.addAll(params.getTablesName());
+ Assertions.assertEquals(catalog,
params.getCatalog());
+
Assertions.assertEquals(UserIdentity.ROOT.toThrift(),
+ params.getCurrentUserIdent());
+ List<TColumnDef> columns = new
ArrayList<>();
+ List<Integer> offsets = new
ArrayList<>();
+ for (String name :
params.getTablesName()) {
+ columns.add(new TColumnDef(new
TColumnDesc(name + "_id", TPrimitiveType.INT)));
+ offsets.add(columns.size());
+ }
+ return new
TDescribeTablesResult().setColumns(columns).setTablesOffset(offsets);
+ });
+ });
+ RootAllocator allocator = new RootAllocator();
+ VectorSchemaRoot root = VectorSchemaRoot.create(includeSchema
+ ? Schemas.GET_TABLES_SCHEMA :
Schemas.GET_TABLES_SCHEMA_NO_SCHEMA, allocator)) {
+ FlightSqlSchemaHelper helper = new FlightSqlSchemaHelper(ctx);
+
helper.setParameterForGetTables(CommandGetTables.newBuilder().setCatalog(catalog)
+
.setDbSchemaFilterPattern("db%").setTableNameFilterPattern("%")
+
.addAllTableTypes(types).setIncludeSchema(includeSchema).build());
+ root.allocateNew();
+ helper.getTables(root);
+ List<String> actualNames = new ArrayList<>();
+ for (int row = 0; row < root.getRowCount(); row++) {
+
actualNames.add(root.getVector("table_name").getObject(row).toString());
+ if (includeSchema) {
+ byte[] bytes = ((VarBinaryVector)
root.getVector("table_schema")).get(row);
+ Schema schema = MessageSerializer.deserializeSchema(
+ new ReadChannel(Channels.newChannel(new
ByteArrayInputStream(bytes))));
+ Assertions.assertEquals(1, schema.getFields().size());
+ Assertions.assertEquals(actualNames.get(row) + "_id",
schema.getFields().get(0).getName());
+ Assertions.assertEquals(new ArrowType.Int(32, true),
schema.getFields().get(0).getType());
+ }
+ }
+ Assertions.assertEquals(expectedNames, actualNames);
+ Assertions.assertEquals(includeSchema ? expectedNames :
Collections.emptyList(), describedNames);
+
Mockito.verify(services.constructed().get(0)).listTableStatus(Mockito.any(TGetTablesParams.class));
+ }
+ }
+}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/arrowflight/FlightSqlTableTypesTest.java
b/fe/fe-core/src/test/java/org/apache/doris/arrowflight/FlightSqlTableTypesTest.java
new file mode 100644
index 00000000000..ec6c4a44e24
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/arrowflight/FlightSqlTableTypesTest.java
@@ -0,0 +1,151 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.arrowflight;
+
+import org.apache.doris.arrowflight.sessions.FlightSessionsManager;
+import org.apache.doris.common.FeConstants;
+import org.apache.doris.common.jmockit.Deencapsulation;
+import org.apache.doris.qe.ConnectContext;
+
+import org.apache.arrow.flight.FlightClient;
+import org.apache.arrow.flight.FlightInfo;
+import org.apache.arrow.flight.FlightProducer.CallContext;
+import org.apache.arrow.flight.FlightProducer.ServerStreamListener;
+import org.apache.arrow.flight.FlightServer;
+import org.apache.arrow.flight.FlightStream;
+import org.apache.arrow.flight.Location;
+import org.apache.arrow.flight.sql.FlightSqlClient;
+import org.apache.arrow.flight.sql.FlightSqlProducer.Schemas;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class FlightSqlTableTypesTest {
+ @Test
+ public void streamsSortedDorisTypesAndReleasesBuffers() throws Exception {
+ checkStream(false);
+ }
+
+ @Test
+ public void releasesBuffersWhenSendingFails() throws Exception {
+ checkStream(true);
+ }
+
+ // GetFlightInfo and DoGet through a real Flight SQL server, on repeated
calls: the info carries the
+ // GetTableTypes schema and one endpoint, and the stream returns the same
sorted types every time.
+ @Test
+ public void tableTypesAreSortedAndStableThroughFlightRpc() throws
Exception {
+ boolean previous = FeConstants.runningUnitTest;
+ FeConstants.runningUnitTest = true;
+ ConnectContext ctx = ConnectContext.forFlight("metadata-token");
+ try {
+ FlightSessionsManager sessions =
Mockito.mock(FlightSessionsManager.class);
+
Mockito.when(sessions.getConnectContext(Mockito.anyString())).thenReturn(ctx);
+ Location location = Location.forGrpcInsecure("127.0.0.1", 0);
+ try (DorisFlightSqlProducer producer = new
DorisFlightSqlProducer(location, sessions);
+ RootAllocator allocator = new RootAllocator();
+ FlightServer server = FlightServer.builder(allocator,
location, producer).build().start();
+ FlightClient client = FlightClient.builder(allocator,
+ Location.forGrpcInsecure("127.0.0.1",
server.getPort())).build()) {
+ FlightSqlClient sql = new FlightSqlClient(client);
+ for (int iteration = 0; iteration < 3; ++iteration) {
+ FlightInfo info = sql.getTableTypes();
+ Assertions.assertEquals(Schemas.GET_TABLE_TYPES_SCHEMA,
info.getSchema());
+ Assertions.assertEquals(1, info.getEndpoints().size());
+ List<String> types = new ArrayList<>();
+ try (FlightStream stream =
client.getStream(info.getEndpoints().get(0).getTicket())) {
+ Assertions.assertEquals(info.getSchema(),
stream.getSchema());
+ while (stream.next()) {
+ VectorSchemaRoot root = stream.getRoot();
+ for (int row = 0; row < root.getRowCount(); ++row)
{
+
types.add(root.getVector("table_type").getObject(row).toString());
+ }
+ }
+ }
+ Assertions.assertEquals(Arrays.asList("BASE TABLE",
"SYSTEM VIEW", "VIEW"), types);
+ }
+ // GetFlightInfo answers from the static schema; only each
DoGet resolves the session.
+ Mockito.verify(sessions,
Mockito.times(3)).getConnectContext(Mockito.anyString());
+ }
+ } finally {
+ ctx.getFlightSqlChannel().close();
+ FeConstants.runningUnitTest = previous;
+ }
+ }
+
+ private void checkStream(boolean failSend) throws Exception {
+ boolean previous = FeConstants.runningUnitTest;
+ FeConstants.runningUnitTest = true;
+ try {
+ ConnectContext ctx = ConnectContext.forFlight("metadata-token");
+ FlightSessionsManager sessions =
Mockito.mock(FlightSessionsManager.class);
+
Mockito.when(sessions.getConnectContext("metadata-token")).thenReturn(ctx);
+ CallContext context = Mockito.mock(CallContext.class);
+ Mockito.when(context.peerIdentity()).thenReturn("metadata-token");
+ try (DorisFlightSqlProducer producer = new DorisFlightSqlProducer(
+ Location.forGrpcInsecure("127.0.0.1", 9090), sessions)) {
+ BufferAllocator allocator = Deencapsulation.getField(producer,
"rootAllocator");
+ for (int i = 0; i < 10; i++) {
+ ServerStreamListener listener =
Mockito.mock(ServerStreamListener.class);
+ AtomicReference<VectorSchemaRoot> batch = new
AtomicReference<>();
+ List<String> values = new ArrayList<>();
+ Mockito.doAnswer(invocation -> {
+ batch.set(invocation.getArgument(0));
+
Assertions.assertEquals(Schemas.GET_TABLE_TYPES_SCHEMA,
batch.get().getSchema());
+ return null;
+
}).when(listener).start(Mockito.any(VectorSchemaRoot.class));
+ Mockito.doAnswer(invocation -> {
+ VectorSchemaRoot root = batch.get();
+ for (int row = 0; row < root.getRowCount(); row++) {
+
values.add(root.getVector("table_type").getObject(row).toString());
+ }
+ if (failSend) {
+ throw new IllegalStateException("simulated send
failure");
+ }
+ return null;
+ }).when(listener).putNext();
+ if (failSend) {
+ Assertions.assertThrows(RuntimeException.class,
+ () -> producer.getStreamTableTypes(context,
listener));
+
Mockito.verify(listener).error(Mockito.any(Throwable.class));
+ Mockito.verify(listener, Mockito.never()).completed();
+ } else {
+ producer.getStreamTableTypes(context, listener);
+ Mockito.verify(listener).completed();
+ Mockito.verify(listener,
Mockito.never()).error(Mockito.any(Throwable.class));
+ }
+ Assertions.assertEquals(Arrays.asList("BASE TABLE",
"SYSTEM VIEW", "VIEW"), values);
+ Assertions.assertEquals(0, allocator.getAllocatedMemory());
+ }
+ Mockito.verify(sessions,
Mockito.times(10)).getConnectContext("metadata-token");
+ } finally {
+ ctx.getFlightSqlChannel().close();
+ }
+ } finally {
+ FeConstants.runningUnitTest = previous;
+ }
+ }
+}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/service/FrontendServiceImplTest.java
b/fe/fe-core/src/test/java/org/apache/doris/service/FrontendServiceImplTest.java
index 6c139546424..a55eded6c6a 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/service/FrontendServiceImplTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/service/FrontendServiceImplTest.java
@@ -25,6 +25,7 @@ import org.apache.doris.catalog.OlapTable;
import org.apache.doris.catalog.Partition;
import org.apache.doris.catalog.TableIf;
import org.apache.doris.catalog.Tablet;
+import org.apache.doris.cloud.catalog.CloudPartition;
import org.apache.doris.common.AuthenticationException;
import org.apache.doris.common.Config;
import org.apache.doris.common.ErrorCode;
@@ -37,6 +38,7 @@ import org.apache.doris.nereids.trees.plans.commands.Command;
import org.apache.doris.nereids.trees.plans.commands.CreateDatabaseCommand;
import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
import org.apache.doris.qe.StmtExecutor;
+import org.apache.doris.rpc.RpcException;
import org.apache.doris.tablefunction.BackendsTableValuedFunction;
import org.apache.doris.thrift.TBackendsMetadataParams;
import org.apache.doris.thrift.TCheckAuthRequest;
@@ -78,6 +80,7 @@ import org.apache.doris.transaction.TransactionState;
import org.apache.doris.transaction.WriteBlockAllocatingTransaction;
import org.apache.doris.utframe.TestWithFeService;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import org.apache.logging.log4j.Level;
import org.apache.thrift.TException;
@@ -521,6 +524,60 @@ public class FrontendServiceImplTest extends
TestWithFeService {
}
}
+ /**
+ * On a follower in cloud mode, listing a table refreshes its partition
versions from the meta service, which
+ * only UPDATE_TIME reads. A listing that asks for names and types, as
Flight SQL GetTables does, must not
+ * depend on that RPC: when it fails, the enumeration stops and the caller
gets only the rows before it.
+ */
+ @Test
+ public void
testListTableStatusRefreshesCloudFollowerVersionsOnlyForUpdateTime() throws
Exception {
+ createTable("CREATE TABLE test.follower_status_table(\n"
+ + " k1 INT,\n"
+ + " v1 INT\n"
+ + ")\n"
+ + "DUPLICATE KEY(k1)\n"
+ + "DISTRIBUTED BY HASH(k1) BUCKETS 1\n"
+ + "PROPERTIES(\"replication_num\" = \"1\");");
+ createView("CREATE VIEW test.follower_status_view AS SELECT k1 FROM
test.follower_status_table");
+ TGetTablesParams namesAndTypes = followerStatusParams("TABLE_NAME",
"TABLE_TYPE");
+ TGetTablesParams updateTimes = followerStatusParams("UPDATE_TIME");
+ Env followerEnv = Mockito.spy(Env.getCurrentEnv());
+ Mockito.doReturn(false).when(followerEnv).isMaster();
+ FrontendServiceImpl impl = new FrontendServiceImpl(exeEnv);
+ try (MockedStatic<Env> env = Mockito.mockStatic(Env.class,
Mockito.CALLS_REAL_METHODS);
+ MockedStatic<Config> config = Mockito.mockStatic(Config.class,
Mockito.CALLS_REAL_METHODS);
+ MockedStatic<CloudPartition> versions =
Mockito.mockStatic(CloudPartition.class)) {
+ env.when(Env::getCurrentEnv).thenReturn(followerEnv);
+ config.when(Config::isCloudMode).thenReturn(true);
+ // The meta service is down: every partition version refresh fails.
+ versions.when(() ->
CloudPartition.getSnapshotVisibleVersion(Mockito.anyList()))
+ .thenThrow(new RpcException("", "injected meta service
failure"));
+
+ TListTableStatusResult listed =
impl.listTableStatus(namesAndTypes);
+
+ Assertions.assertEquals(
+ ImmutableMap.of("follower_status_table", "BASE TABLE",
"follower_status_view", "VIEW"),
+ listed.getTables().stream()
+ .collect(Collectors.toMap(TTableStatus::getName,
TTableStatus::getType)));
+ versions.verify(() ->
CloudPartition.getSnapshotVisibleVersion(Mockito.anyList()), Mockito.never());
+
+ // UPDATE_TIME reads the partitions' visible version time, so it
still refreshes them first.
+ impl.listTableStatus(updateTimes);
+
+ versions.verify(() ->
CloudPartition.getSnapshotVisibleVersion(Mockito.anyList()));
+ }
+ }
+
+ private TGetTablesParams followerStatusParams(String... requiredColumns) {
+ TGetTablesParams params = new TGetTablesParams();
+ params.setCatalog(InternalCatalog.INTERNAL_CATALOG_NAME);
+ params.setDb("test");
+ params.setPattern("follower_status_%");
+ params.setRequiredColumns(Sets.newHashSet(requiredColumns));
+
params.setCurrentUserIdent(connectContext.getCurrentUserIdentity().toThrift());
+ return params;
+ }
+
@Test
public void testCreatePartitionRange() throws Exception {
diff --git
a/regression-test/suites/arrow_flight_sql_p0/test_table_types_over_arrow_flight.groovy
b/regression-test/suites/arrow_flight_sql_p0/test_table_types_over_arrow_flight.groovy
new file mode 100644
index 00000000000..ec5f53f4c50
--- /dev/null
+++
b/regression-test/suites/arrow_flight_sql_p0/test_table_types_over_arrow_flight.groovy
@@ -0,0 +1,141 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import org.apache.arrow.driver.jdbc.shaded.org.apache.arrow.flight.CallOption
+import org.apache.arrow.driver.jdbc.shaded.org.apache.arrow.flight.CallOptions
+import
org.apache.arrow.driver.jdbc.shaded.org.apache.arrow.flight.CloseSessionRequest
+import org.apache.arrow.driver.jdbc.shaded.org.apache.arrow.flight.FlightClient
+import org.apache.arrow.driver.jdbc.shaded.org.apache.arrow.flight.Location
+import
org.apache.arrow.driver.jdbc.shaded.org.apache.arrow.flight.sql.FlightSqlClient
+import
org.apache.arrow.driver.jdbc.shaded.org.apache.arrow.memory.RootAllocator
+import
org.apache.arrow.driver.jdbc.shaded.org.apache.arrow.vector.types.pojo.ArrowType
+import org.apache.doris.regression.util.JdbcUtils
+
+import java.util.concurrent.TimeUnit
+
+suite("test_table_types_over_arrow_flight", "arrow_flight_sql") {
+ def flightConn = context.getArrowFlightSqlConnection()
+ def runOnFlight = { String statement ->
+ JdbcUtils.executeToList(flightConn, statement)
+ }
+ runOnFlight "USE `${context.dbName}`"
+ runOnFlight "DROP VIEW IF EXISTS test_flight_table_types_view"
+ runOnFlight "DROP TABLE IF EXISTS test_flight_table_types_base"
+ runOnFlight """
+ CREATE TABLE test_flight_table_types_base (id INT)
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES ("replication_num" = "1")
+ """
+ runOnFlight """
+ CREATE VIEW test_flight_table_types_view AS
+ SELECT id FROM test_flight_table_types_base
+ """
+
+ // These JDBC metadata methods consume Flight GetTableTypes/GetTables
streams.
+ // SQL result snapshots alone do not exercise those metadata RPCs.
+ def metadata = flightConn.getMetaData()
+ def typeRows = metadata.getTableTypes()
+ try {
+ def types = []
+ while (typeRows.next()) {
+ types.add(typeRows.getString("TABLE_TYPE"))
+ }
+ assertEquals(["BASE TABLE", "SYSTEM VIEW", "VIEW"], types)
+ } finally {
+ typeRows.close()
+ }
+
+ def getTables = { List<String> types ->
+ def rows = metadata.getTables("internal", context.dbName,
"test_flight_table_types%",
+ types == null ? null : types.toArray(new String[0]))
+ try {
+ def tables = []
+ while (rows.next()) {
+ assertEquals("internal", rows.getString("TABLE_CAT"))
+ assertEquals(context.dbName, rows.getString("TABLE_SCHEM"))
+ tables.add([rows.getString("TABLE_NAME"),
rows.getString("TABLE_TYPE")])
+ }
+ return tables.sort { left, right -> left[0] <=> right[0] }
+ } finally {
+ rows.close()
+ }
+ }
+
+ def baseTable = ["test_flight_table_types_base", "BASE TABLE"]
+ def view = ["test_flight_table_types_view", "VIEW"]
+ assertEquals([baseTable, view], getTables(null))
+ assertEquals([baseTable, view], getTables([]))
+ assertEquals([baseTable], getTables(["BASE TABLE"]))
+ assertEquals([view], getTables(["VIEW"]))
+ // Both orders must work: forwarding only the first type loses or leaks
rows.
+ assertEquals([baseTable, view], getTables(["VIEW", "BASE TABLE"]))
+ assertEquals([baseTable, view], getTables(["BASE TABLE", "VIEW"]))
+ assertEquals([], getTables(["SYSTEM VIEW"]))
+ assertEquals([], getTables(["UNKNOWN"]))
+ assertEquals([view], getTables(["UNKNOWN", "VIEW", "VIEW"]))
+ // Flight SQL calls TABLE, VIEW and SYSTEM TABLE the commonly supported
names, and clients send them
+ // in either case: the ADBC Flight SQL driver asks for "table", JDBC tools
for "TABLE".
+ assertEquals([baseTable], getTables(["TABLE"]))
+ assertEquals([baseTable], getTables(["table"]))
+ assertEquals([baseTable, view], getTables(["table", "view"]))
+
+ // The same RPCs from a plain Flight SQL client, called repeatedly:
GetFlightInfo carries the
+ // GetTableTypes schema and one endpoint, DoGet returns the same sorted
types every time, and
+ // the session still answers a query afterwards.
+ def config = context.config.otherConfigs
+ def location =
Location.forGrpcInsecure(config.get("extArrowFlightSqlHost"),
+ Integer.parseInt(config.get("extArrowFlightSqlPort")))
+ new RootAllocator().withCloseable { allocator ->
+ FlightClient.builder(allocator, location).build().withCloseable {
flight ->
+ def token =
flight.authenticateBasicToken(config.get("extArrowFlightSqlUser"),
+ config.get("extArrowFlightSqlPassword")).get()
+ CallOption[] options = [token, CallOptions.timeout(30,
TimeUnit.SECONDS)]
+ def client = new FlightSqlClient(flight)
+ try {
+ 3.times {
+ def info = client.getTableTypes(options)
+ assertEquals(["table_type"],
info.getSchema().getFields()*.getName())
+ assertEquals(new ArrowType.Utf8(),
info.getSchema().getFields()[0].getType())
+ assertFalse(info.getSchema().getFields()[0].isNullable())
+ assertEquals(1, info.getEndpoints().size())
+ def types = []
+ flight.getStream(info.getEndpoints()[0].getTicket(),
options).withCloseable { stream ->
+ assertEquals(info.getSchema(), stream.getSchema())
+ while (stream.next()) {
+ def root = stream.getRoot()
+ for (int row = 0; row < root.getRowCount(); ++row)
{
+
types.add(root.getVector("table_type").getObject(row).toString())
+ }
+ }
+ }
+ assertEquals(["BASE TABLE", "SYSTEM VIEW", "VIEW"], types)
+ }
+ def info = client.execute("SHOW VARIABLES LIKE
'query_timeout'", options)
+ int rows = 0
+ flight.getStream(info.getEndpoints()[0].getTicket(),
options).withCloseable { stream ->
+ while (stream.next()) {
+ rows += stream.getRoot().getRowCount()
+ }
+ }
+ assertEquals(1, rows)
+ } finally {
+ // End the session the way the drivers do rather than leaving
it to wait_timeout.
+ flight.closeSession(new CloseSessionRequest(), token)
+ }
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]