This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new b531a5795d3 branch-4.1: [fix](arrow) correct the Flight SQL GetTables
schema and the TIMESTAMPTZ arrow reader #66344 (#66355)
b531a5795d3 is described below
commit b531a5795d3d83e02e09a6736327cd8537f0f259
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Mon Aug 3 13:21:26 2026 +0800
branch-4.1: [fix](arrow) correct the Flight SQL GetTables schema and the
TIMESTAMPTZ arrow reader #66344 (#66355)
Cherry-picked from #66344
Co-authored-by: Mingyu Chen (Rayner) <[email protected]>
---
.../data_type_timestamptz_serde.cpp | 88 +++++++++
.../data_type_serde/data_type_timestamptz_serde.h | 5 +
.../data_type_serde_timestamptz_test.cpp | 85 +++++++++
.../service/arrowflight/FlightSqlSchemaHelper.java | 103 +++++++---
.../FlightSqlSchemaHelperArrowTypeTest.java | 209 +++++++++++++++++++++
5 files changed, 460 insertions(+), 30 deletions(-)
diff --git a/be/src/core/data_type_serde/data_type_timestamptz_serde.cpp
b/be/src/core/data_type_serde/data_type_timestamptz_serde.cpp
index d1ae23835c7..e9968e0bc87 100644
--- a/be/src/core/data_type_serde/data_type_timestamptz_serde.cpp
+++ b/be/src/core/data_type_serde/data_type_timestamptz_serde.cpp
@@ -20,7 +20,9 @@
#include <arrow/builder.h>
#include <cctz/time_zone.h>
+#include "common/config.h"
#include "core/data_type/primitive_type.h"
+#include "core/data_type_serde/arrow_validation.h"
#include "core/data_type_serde/decoded_column_view.h"
#include "core/data_type_serde/parquet_decode_source.h"
#include "core/data_type_serde/parquet_timestamp.h"
@@ -357,6 +359,92 @@ Status
DataTypeTimeStampTzSerDe::write_column_to_arrow(const IColumn& column,
return Status::OK();
}
+/**
+ * Reads an Arrow timestamp array into a TIMESTAMPTZ column.
+ *
+ * <p>Without this the base DataTypeNumberSerDe<TYPE_TIMESTAMPTZ> reader runs
instead, and its
+ * fixed-width path memcpy's the array's int64 epoch values straight into the
column -- whose element
+ * is a PACKED date/time value, not an epoch. Both are 8 bytes wide, so no
check catches it: the scan
+ * succeeds and every row is silently wrong. That is why an unreadable Arrow
type below is an error
+ * rather than a fallback.
+ *
+ * <p>The value is read as an instant on the UTC line, which is the inverse of
what
+ * write_column_to_arrow emits (it converts with cctz::utc_time_zone(), not
with ctz). ctz is
+ * therefore unused here: an Arrow timestamp's zone -- whether it names one or
not -- describes how
+ * to DISPLAY the instant, and TIMESTAMPTZ stores the instant itself.
+ */
+Status DataTypeTimeStampTzSerDe::read_column_from_arrow(IColumn& column,
+ const arrow::Array*
arrow_array,
+ int64_t start, int64_t
end,
+ const cctz::time_zone&
ctz) const {
+ if (config::enable_arrow_input_validation) {
+ check_arrow_no_offset(*arrow_array);
+ }
+ if (arrow_array->type()->id() != arrow::Type::TIMESTAMP) {
+ LOG(WARNING) << "not support convert to timestamptz from arrow type:"
+ << arrow_array->type()->id();
+ return Status::InternalError("not support convert to timestamptz from
arrow type: {}",
+ arrow_array->type()->id());
+ }
+ const auto* concrete_array = assert_cast<const
arrow::TimestampArray*>(arrow_array);
+ const auto type =
std::static_pointer_cast<arrow::TimestampType>(arrow_array->type());
+ // Scale each unit to the microseconds the column stores. NANO is divided
rather than refused:
+ // sub-microsecond precision is beyond what any Doris datetime type keeps,
and rejecting the
+ // column over a digit would make whole tables unreadable.
+ int64_t multiplier = 1;
+ int64_t divisor = 1;
+ switch (type->unit()) {
+ case arrow::TimeUnit::type::SECOND:
+ multiplier = 1000000;
+ break;
+ case arrow::TimeUnit::type::MILLI:
+ multiplier = 1000;
+ break;
+ case arrow::TimeUnit::type::MICRO:
+ break;
+ case arrow::TimeUnit::type::NANO:
+ divisor = 1000;
+ break;
+ default:
+ LOG(WARNING) << "not support convert to timestamptz from time_unit:"
<< type->unit();
+ return Status::InvalidArgument("not support convert to timestamptz
from time_unit: {}",
+ type->unit());
+ }
+
+ auto& col_data = assert_cast<ColumnTimeStampTz&>(column).get_data();
+ const auto* base_ptr = reinterpret_cast<const
uint8_t*>(concrete_array->raw_values());
+ const size_t element_size = sizeof(int64_t);
+ for (auto value_i = start; value_i < end; ++value_i) {
+ // One value per row including the null ones: the caller
(DataTypeNullableSerDe) has already
+ // taken the validity bitmap and hands the whole range down. The value
under a null slot is
+ // whatever the source left there, so it must not be converted -- a
garbage epoch would fail
+ // the range check below and take a well-formed batch down with it.
+ if (concrete_array->IsNull(value_i)) {
+ col_data.push_back(TimestampTzValue());
+ continue;
+ }
+ const uint8_t* raw_byte_ptr = base_ptr + value_i * element_size;
+ auto value = unaligned_load<int64_t>(raw_byte_ptr);
+ int64_t timestamp_micros = 0;
+ if (__builtin_mul_overflow(value, multiplier, ×tamp_micros)) {
+ return Status::DataQualityError(
+ "Arrow timestamp {} in unit {} overflows the microsecond
range of TIMESTAMPTZ",
+ value, static_cast<int>(type->unit()));
+ }
+ if (divisor != 1) {
+ // Floor, not truncate: C++ integer division rounds toward zero,
which would move a
+ // pre-1970 instant forward by up to one microsecond.
+ int64_t remainder = timestamp_micros % divisor;
+ timestamp_micros /= divisor;
+ if (remainder < 0) {
+ --timestamp_micros;
+ }
+ }
+ RETURN_IF_ERROR(append_timestamptz_from_utc_epoch_micros(col_data,
timestamp_micros));
+ }
+ return Status::OK();
+}
+
Status DataTypeTimeStampTzSerDe::write_column_to_orc(const std::string&
timezone,
const IColumn& column,
const NullMap* null_map,
orc::ColumnVectorBatch*
orc_col_batch,
diff --git a/be/src/core/data_type_serde/data_type_timestamptz_serde.h
b/be/src/core/data_type_serde/data_type_timestamptz_serde.h
index 28b51dd3093..ce858c31c06 100644
--- a/be/src/core/data_type_serde/data_type_timestamptz_serde.h
+++ b/be/src/core/data_type_serde/data_type_timestamptz_serde.h
@@ -68,6 +68,11 @@ public:
arrow::ArrayBuilder* array_builder, int64_t
start, int64_t end,
const cctz::time_zone& ctz) const override;
+ // Overridden rather than inherited: DataTypeNumberSerDe's reader would
memcpy Arrow's epoch
+ // integers over this column's packed values, same width and no error. See
the definition.
+ Status read_column_from_arrow(IColumn& column, const arrow::Array*
arrow_array, int64_t start,
+ int64_t end, const cctz::time_zone& ctz)
const override;
+
Status write_column_to_orc(const std::string& timezone, const IColumn&
column,
const NullMap* null_map,
orc::ColumnVectorBatch* orc_col_batch,
int64_t start, int64_t end, Arena& arena,
diff --git a/be/test/core/data_type_serde/data_type_serde_timestamptz_test.cpp
b/be/test/core/data_type_serde/data_type_serde_timestamptz_test.cpp
index b88601a02ad..584106cc635 100644
--- a/be/test/core/data_type_serde/data_type_serde_timestamptz_test.cpp
+++ b/be/test/core/data_type_serde/data_type_serde_timestamptz_test.cpp
@@ -24,7 +24,9 @@
#include <cstddef>
#include <iostream>
+#include <limits>
#include <type_traits>
+#include <vector>
#include "core/assert_cast.h"
#include "core/column/column.h"
@@ -274,4 +276,87 @@ TEST_F(DataTypeTimeStampTzSerDeTest, binary_roundtrip) {
test_func(*serde_tz_3, column_tz_3, 3);
test_func(*serde_tz_6, column_tz_6, 6);
}
+
+// Roundtrip through Arrow. Same failure shape as binary_roundtrip above:
read_column_from_arrow
+// was inherited from DataTypeNumberSerDe, whose fixed-width path memcpy'd
Arrow's int64 EPOCH
+// values over this column's PACKED values. Both are 8 bytes, so nothing
failed -- the read
+// succeeded with every row wrong. Comparing the value that comes back against
the one that went
+// out is what makes deleting the override fail here instead of in a user's
result set.
+TEST_F(DataTypeTimeStampTzSerDeTest, ArrowRoundTrip) {
+ auto test_func = [&](const DataTypeTimeStampTzSerDe& serde,
arrow::TimeUnit::type unit,
+ uint16_t year, uint8_t month, uint8_t day, uint8_t
hour, uint8_t minute,
+ uint8_t second, uint32_t microsecond, int64_t
expected_arrow_value) {
+ auto source_column = ColumnTimeStampTz::create();
+ TimestampTzValue source_value;
+ source_value.unchecked_set_time(year, month, day, hour, minute,
second, microsecond);
+ source_column->insert_value(source_value);
+
+ arrow::TimestampBuilder builder(arrow::timestamp(unit),
arrow::default_memory_pool());
+ ASSERT_TRUE(serde.write_column_to_arrow(*source_column, nullptr,
&builder, 0,
+ source_column->size(),
cctz::utc_time_zone())
+ .ok());
+ std::shared_ptr<arrow::Array> array;
+ ASSERT_TRUE(builder.Finish(&array).ok());
+ // Pinned so the roundtrip cannot pass by both halves sharing one
wrong encoding: what
+ // crosses the wire is an epoch count in the unit, not the column's
packed representation.
+ ASSERT_EQ(expected_arrow_value,
+ assert_cast<const
arrow::TimestampArray*>(array.get())->Value(0));
+
+ auto dest_column = ColumnTimeStampTz::create();
+ ASSERT_TRUE(serde.read_column_from_arrow(*dest_column, array.get(), 0,
array->length(),
+ cctz::utc_time_zone())
+ .ok());
+ ASSERT_EQ(1, dest_column->size());
+ EXPECT_EQ(source_value, dest_column->get_element(0));
+ };
+
+ // Before the epoch, so a division that truncates toward zero instead of
flooring lands one
+ // unit late rather than agreeing by accident.
+ test_func(*serde_tz_6, arrow::TimeUnit::MICRO, 1969, 12, 31, 23, 59, 59,
123456, -876544);
+ test_func(*serde_tz_3, arrow::TimeUnit::MILLI, 1969, 12, 31, 23, 59, 59,
123000, -877);
+ test_func(*serde_tz_0, arrow::TimeUnit::SECOND, 1969, 12, 31, 23, 59, 59,
0, -1);
+ test_func(*serde_tz_6, arrow::TimeUnit::MICRO, 2023, 1, 2, 3, 4, 5,
123456, 1672628645123456);
+}
+
+// An Arrow type this serde cannot decode must fail the scan, not produce
values. The inherited
+// reader accepted anything 8 bytes wide, which is how the corruption above
stayed invisible.
+TEST_F(DataTypeTimeStampTzSerDeTest, ReadArrowRejectsNonTimestampType) {
+ arrow::Int64Builder builder;
+ ASSERT_TRUE(builder.Append(1672628645123456).ok());
+ std::shared_ptr<arrow::Array> array;
+ ASSERT_TRUE(builder.Finish(&array).ok());
+
+ auto dest_column = ColumnTimeStampTz::create();
+ auto st = serde_tz_6->read_column_from_arrow(*dest_column, array.get(), 0,
array->length(),
+ cctz::utc_time_zone());
+ EXPECT_FALSE(st.ok());
+ EXPECT_EQ(0, dest_column->size());
+}
+
+// A null slot's value buffer holds whatever the source left there. Converting
it would range-check
+// garbage and fail a batch whose rows are all well-formed, so nulls take a
default value instead.
+TEST_F(DataTypeTimeStampTzSerDeTest, ReadArrowIgnoresValuesUnderNulls) {
+ TimestampTzValue source_value;
+ source_value.unchecked_set_time(2023, 1, 2, 3, 4, 5, 123456);
+
+ std::vector<int64_t> values = {1672628645123456,
std::numeric_limits<int64_t>::max()};
+ // Bit 0 set, bit 1 clear: row 0 valid, row 1 null.
+ std::vector<uint8_t> validity = {0b01};
+ auto data =
arrow::ArrayData::Make(arrow::timestamp(arrow::TimeUnit::MICRO), values.size(),
+ {arrow::Buffer::Wrap(validity.data(),
validity.size()),
+ arrow::Buffer::Wrap(values.data(),
values.size())},
+ /*null_count=*/1);
+ std::shared_ptr<arrow::Array> array = arrow::MakeArray(data);
+ ASSERT_TRUE(array->IsNull(1));
+
+ auto dest_column = ColumnTimeStampTz::create();
+ ASSERT_TRUE(serde_tz_6
+ ->read_column_from_arrow(*dest_column, array.get(), 0,
array->length(),
+ cctz::utc_time_zone())
+ .ok());
+ // One value per row, nulls included: the caller took the validity bitmap
and expects the
+ // nested column to have grown by the full range.
+ ASSERT_EQ(2, dest_column->size());
+ EXPECT_EQ(source_value, dest_column->get_element(0));
+}
} // namespace doris
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/FlightSqlSchemaHelper.java
b/fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/FlightSqlSchemaHelper.java
index 04389e0feee..b89abf16e83 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/FlightSqlSchemaHelper.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/FlightSqlSchemaHelper.java
@@ -121,7 +121,11 @@ public class FlightSqlSchemaHelper {
case VARIANT:
return new ArrowType.Utf8();
case DATEV2:
- return new ArrowType.Date(DateUnit.MILLISECOND);
+ // DAY, not MILLISECOND: BE writes a DATEV2 column as
arrow::Date32Type (a day number),
+ // so a MILLISECOND unit here describes the metadata as date64
while the data that
+ // follows is date32. A client that trusts this schema -- one
reading through the ADBC
+ // Flight SQL driver does -- then types the column as a
datetime and fails the read.
+ return new ArrowType.Date(DateUnit.DAY);
case DATETIMEV2:
if (scale > 3) {
return new ArrowType.Timestamp(TimeUnit.MICROSECOND,
timeZone);
@@ -285,41 +289,80 @@ public class FlightSqlSchemaHelper {
Integer tableOffset =
describeTablesResult.getTablesOffset().get(tableIndex);
for (; columnIndex < tableOffset; columnIndex++) {
TColumnDef columnDef =
describeTablesResult.getColumns().get(columnIndex);
- TColumnDesc columnDesc = columnDef.getColumnDesc();
- final ArrowType columnArrowType =
columnDescToArrowType(columnDesc);
-
- List<Field> columnArrowTypeChildren;
- // Arrow complex types may require children fields for parsing
the schema on C++
- switch (columnArrowType.getTypeID()) {
- case List:
- case LargeList:
- case FixedSizeList:
- columnArrowTypeChildren = Collections.singletonList(
-
Field.notNullable(BaseRepeatedValueVector.DATA_VECTOR_NAME,
-
ZeroVector.INSTANCE.getField().getType()));
- break;
- case Map:
- columnArrowTypeChildren = Collections.singletonList(
- Field.notNullable(MapVector.DATA_VECTOR_NAME,
new ArrowType.List()));
- break;
- case Struct:
- columnArrowTypeChildren = Collections.emptyList();
- break;
- default:
- columnArrowTypeChildren = null;
- break;
- }
-
- final Field field = new Field(columnDesc.getColumnName(),
- new FieldType(columnDesc.isIsAllowNull(),
columnArrowType, null,
- createFlightSqlColumnMetadata(dbName,
tableName, columnDesc)), columnArrowTypeChildren);
- fields.add(field);
+ fields.add(buildField(dbName, tableName,
columnDef.getColumnDesc()));
}
tableToFields.put(tableName, fields);
}
return tableToFields;
}
+ /** One column, with its nested types described down to the leaves. */
+ private static Field buildField(String dbName, String tableName,
TColumnDesc desc) {
+ ArrowType arrowType = columnDescToArrowType(desc);
+ return new Field(desc.getColumnName(),
+ new FieldType(desc.isIsAllowNull(), arrowType, null,
+ createFlightSqlColumnMetadata(dbName, tableName,
desc)),
+ arrowChildren(dbName, tableName, desc, arrowType));
+ }
+
+ /**
+ * The Arrow children of a complex column, built from the descriptor's own
children.
+ *
+ * <p>These are not decoration. An Arrow ARRAY/MAP/STRUCT type carries its
element types in its
+ * children and nowhere else, so a placeholder child says the column is an
array OF NOTHING --
+ * and BE emits the real element type in the data ({@code
convert_to_arrow_type}: ListType(item),
+ * MapType(key, value), StructType(fields)), which leaves the schema
describing one thing and the
+ * batch carrying another. A client that types its columns from this
schema (one reading through
+ * the ADBC Flight SQL driver does) then rejects the column outright.
+ *
+ * <p>{@code describeTables} already reports the tree -- {@code
Column.createChildrenColumn} names
+ * an array's element "item" and a map's pair "key"/"value", which is what
Arrow calls them too.
+ * When it reports none, the old placeholders are kept rather than an
empty child list: a source
+ * that cannot describe its nested types is no worse off than before.
+ */
+ private static List<Field> arrowChildren(String dbName, String tableName,
TColumnDesc desc,
+ ArrowType arrowType) {
+ List<TColumnDesc> children = desc.isSetChildren() ? desc.getChildren()
: Collections.emptyList();
+ switch (arrowType.getTypeID()) {
+ case List:
+ case LargeList:
+ case FixedSizeList:
+ if (children.size() != 1) {
+ return Collections.singletonList(
+
Field.notNullable(BaseRepeatedValueVector.DATA_VECTOR_NAME,
+ ZeroVector.INSTANCE.getField().getType()));
+ }
+ return Collections.singletonList(buildField(dbName, tableName,
children.get(0)));
+ case Map:
+ // Arrow spells a map as list<entries: struct<key, value>>,
with the entries struct and
+ // the key both non-nullable -- the descriptor's key
nullability is not carried over,
+ // because an Arrow map with a nullable key is not a valid
schema.
+ if (children.size() != 2) {
+ return Collections.singletonList(
+ Field.notNullable(MapVector.DATA_VECTOR_NAME, new
ArrowType.List()));
+ }
+ Field key = buildField(dbName, tableName, children.get(0));
+ Field value = buildField(dbName, tableName, children.get(1));
+ Field entries = new Field(MapVector.DATA_VECTOR_NAME,
+ new FieldType(false, new ArrowType.Struct(), null),
+ Arrays.asList(new Field(key.getName(),
+ new FieldType(false, key.getType(),
null), key.getChildren()),
+ value));
+ return Collections.singletonList(entries);
+ case Struct:
+ if (children.isEmpty()) {
+ return Collections.emptyList();
+ }
+ List<Field> structFields = new ArrayList<>(children.size());
+ for (TColumnDesc child : children) {
+ structFields.add(buildField(dbName, tableName, child));
+ }
+ return structFields;
+ default:
+ return null;
+ }
+ }
+
/**
* for FlightSqlProducer Schemas.GET_CATALOGS_SCHEMA
*/
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/service/arrowflight/FlightSqlSchemaHelperArrowTypeTest.java
b/fe/fe-core/src/test/java/org/apache/doris/service/arrowflight/FlightSqlSchemaHelperArrowTypeTest.java
new file mode 100644
index 00000000000..147a52aa55e
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/service/arrowflight/FlightSqlSchemaHelperArrowTypeTest.java
@@ -0,0 +1,209 @@
+// 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.service.arrowflight;
+
+import org.apache.doris.common.jmockit.Deencapsulation;
+import org.apache.doris.thrift.TColumnDesc;
+import org.apache.doris.thrift.TPrimitiveType;
+
+import org.apache.arrow.vector.complex.BaseRepeatedValueVector;
+import org.apache.arrow.vector.complex.MapVector;
+import org.apache.arrow.vector.ipc.ReadChannel;
+import org.apache.arrow.vector.ipc.message.MessageSerializer;
+import org.apache.arrow.vector.types.DateUnit;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.Schema;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.channels.Channels;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * What {@code CommandGetTables} says a column is, against what the query that
follows actually
+ * carries.
+ *
+ * <p><b>Why these assertions matter.</b> A Flight SQL client is entitled to
type its columns from
+ * the schema in {@code GetTables} and then read the batches without
re-deriving anything -- that is
+ * what the schema is for, and {@code getArrowType} is documented as mirroring
+ * {@code convert_to_arrow_type} in the backend. When the two disagree the
client does not get a
+ * degraded answer, it gets a failed read: it decodes the batch as the type
the metadata promised.
+ * So each case below pins the Arrow type BE emits, not merely "some" type.
+ *
+ * <p>The descriptors are built the way {@code
FrontendServiceImpl.getColumnDesc} builds them --
+ * a complex column carries its element types as {@link TColumnDesc} children,
named "item" for an
+ * array and "key"/"value" for a map by {@code Column.createChildrenColumn}.
+ */
+public class FlightSqlSchemaHelperArrowTypeTest {
+
+ private static final String DB = "test_db";
+ private static final String TABLE = "test_tbl";
+
+ private static TColumnDesc desc(String name, TPrimitiveType type) {
+ TColumnDesc columnDesc = new TColumnDesc(name, type);
+ // Nullable, so that a place where the mapping must force NOT NULL is
proved to force it
+ // rather than to inherit it.
+ columnDesc.setIsAllowNull(true);
+ return columnDesc;
+ }
+
+ private static TColumnDesc desc(String name, TPrimitiveType type,
TColumnDesc... children) {
+ TColumnDesc columnDesc = desc(name, type);
+ columnDesc.setChildren(Arrays.asList(children));
+ return columnDesc;
+ }
+
+ private static Field buildField(TColumnDesc columnDesc) {
+ return Deencapsulation.invoke(FlightSqlSchemaHelper.class,
"buildField", DB, TABLE, columnDesc);
+ }
+
+ /**
+ * BE writes a DATEV2 column as {@code arrow::Date32Type} -- a day number.
{@link DateUnit#MILLISECOND}
+ * is date64, a different width and a different meaning, and a client that
believes it renders and
+ * compares the column as a datetime, then fails the read on the first
batch with "not support convert
+ * to datetimev2 from arrow type: 16".
+ */
+ @Test
+ public void dateV2IsDescribedAsDate32() {
+ Assertions.assertEquals(new ArrowType.Date(DateUnit.DAY),
+ buildField(desc("d", TPrimitiveType.DATEV2)).getType());
+ }
+
+ /**
+ * An Arrow list carries its element type in its child and nowhere else,
so the placeholder child this
+ * replaced ({@code ZeroVector}'s Null type) described every array in the
catalog as an array OF
+ * NOTHING while BE emitted {@code ListType(item)} in the data.
+ */
+ @Test
+ public void arrayDescribesItsElementType() {
+ Field array = buildField(desc("a", TPrimitiveType.ARRAY, desc("item",
TPrimitiveType.INT)));
+
+ Assertions.assertEquals(ArrowType.ArrowTypeID.List,
array.getType().getTypeID());
+ Assertions.assertEquals(1, array.getChildren().size());
+ Field item = array.getChildren().get(0);
+ Assertions.assertEquals("item", item.getName());
+ Assertions.assertEquals(new ArrowType.Int(32, true), item.getType());
+ }
+
+ /**
+ * Arrow spells a map as {@code list<entries: struct<key, value>>}. Both
the entries struct and the key
+ * are non-nullable in a valid Arrow schema, so the descriptor's
nullability must not be carried over to
+ * the key even though it is carried over everywhere else.
+ */
+ @Test
+ public void mapDescribesKeyAndValue() {
+ Field map = buildField(desc("m", TPrimitiveType.MAP,
+ desc("key", TPrimitiveType.VARCHAR), desc("value",
TPrimitiveType.INT)));
+
+ Assertions.assertEquals(ArrowType.ArrowTypeID.Map,
map.getType().getTypeID());
+ Assertions.assertEquals(1, map.getChildren().size());
+
+ Field entries = map.getChildren().get(0);
+ Assertions.assertEquals(MapVector.DATA_VECTOR_NAME, entries.getName());
+ Assertions.assertEquals(ArrowType.ArrowTypeID.Struct,
entries.getType().getTypeID());
+ Assertions.assertFalse(entries.isNullable(), "an arrow map's entries
struct is never nullable");
+
+ List<Field> pair = entries.getChildren();
+ Assertions.assertEquals(2, pair.size());
+ Assertions.assertEquals("key", pair.get(0).getName());
+ Assertions.assertEquals(new ArrowType.Utf8(), pair.get(0).getType());
+ Assertions.assertFalse(pair.get(0).isNullable(), "an arrow map with a
nullable key is not a valid schema");
+ Assertions.assertEquals("value", pair.get(1).getName());
+ Assertions.assertEquals(new ArrowType.Int(32, true),
pair.get(1).getType());
+ }
+
+ /** A struct with no fields is not "a struct", it is a column the client
cannot read at all. */
+ @Test
+ public void structDescribesItsFields() {
+ Field struct = buildField(desc("s", TPrimitiveType.STRUCT,
+ desc("f1", TPrimitiveType.INT), desc("f2",
TPrimitiveType.STRING)));
+
+ Assertions.assertEquals(ArrowType.ArrowTypeID.Struct,
struct.getType().getTypeID());
+ Assertions.assertEquals(2, struct.getChildren().size());
+ Assertions.assertEquals("f1", struct.getChildren().get(0).getName());
+ Assertions.assertEquals(new ArrowType.Int(32, true),
struct.getChildren().get(0).getType());
+ Assertions.assertEquals("f2", struct.getChildren().get(1).getName());
+ Assertions.assertEquals(new ArrowType.Utf8(),
struct.getChildren().get(1).getType());
+ }
+
+ /** Nesting is where a per-column fix would have stopped: the descriptor's
tree is walked to the leaves. */
+ @Test
+ public void nestedComplexTypesAreDescribedToTheLeaves() {
+ Field outer = buildField(desc("a", TPrimitiveType.ARRAY,
+ desc("item", TPrimitiveType.MAP,
+ desc("key", TPrimitiveType.VARCHAR),
+ desc("value", TPrimitiveType.ARRAY, desc("item",
TPrimitiveType.BIGINT)))));
+
+ Field innerMap = outer.getChildren().get(0);
+ Assertions.assertEquals(ArrowType.ArrowTypeID.Map,
innerMap.getType().getTypeID());
+ List<Field> pair = innerMap.getChildren().get(0).getChildren();
+ Assertions.assertEquals(new ArrowType.Utf8(), pair.get(0).getType());
+
+ Field innerArray = pair.get(1);
+ Assertions.assertEquals(ArrowType.ArrowTypeID.List,
innerArray.getType().getTypeID());
+ Assertions.assertEquals(new ArrowType.Int(64, true),
innerArray.getChildren().get(0).getType());
+ }
+
+ /**
+ * A descriptor that reports no children keeps the placeholders rather
than an empty child list: a source
+ * that cannot describe its nested types is no worse off than it was
before this mapping existed.
+ */
+ @Test
+ public void complexColumnWithoutChildrenKeepsThePlaceholder() {
+ Field array = buildField(desc("a", TPrimitiveType.ARRAY));
+ Assertions.assertEquals(1, array.getChildren().size());
+ Assertions.assertEquals(BaseRepeatedValueVector.DATA_VECTOR_NAME,
array.getChildren().get(0).getName());
+ Assertions.assertEquals(ArrowType.ArrowTypeID.Null,
array.getChildren().get(0).getType().getTypeID());
+
+ Field map = buildField(desc("m", TPrimitiveType.MAP));
+ Assertions.assertEquals(1, map.getChildren().size());
+ Assertions.assertEquals(MapVector.DATA_VECTOR_NAME,
map.getChildren().get(0).getName());
+ Assertions.assertEquals(ArrowType.ArrowTypeID.List,
map.getChildren().get(0).getType().getTypeID());
+
+ Assertions.assertTrue(buildField(desc("s",
TPrimitiveType.STRUCT)).getChildren().isEmpty());
+ }
+
+ /** A scalar column has no children to describe, and gaining one would
change how it is read. */
+ @Test
+ public void scalarColumnHasNoChildren() {
+ Assertions.assertTrue(buildField(desc("i",
TPrimitiveType.INT)).getChildren().isEmpty());
+ }
+
+ /**
+ * The client does not see the {@link Field} objects, it sees the
serialized schema in the
+ * {@code table_schema} column of {@code GetTables}. Asserting after a
round trip through that encoding
+ * is what proves the element types actually reach it.
+ */
+ @Test
+ public void theSerializedSchemaCarriesTheChildren() throws IOException {
+ byte[] serialized =
FlightSqlSchemaHelper.getSerializedSchema(Collections.singletonList(
+ buildField(desc("a", TPrimitiveType.ARRAY, desc("item",
TPrimitiveType.INT)))));
+
+ Schema schema = MessageSerializer.deserializeSchema(
+ new ReadChannel(Channels.newChannel(new
ByteArrayInputStream(serialized))));
+
+ Field array = schema.getFields().get(0);
+ Assertions.assertEquals(ArrowType.ArrowTypeID.List,
array.getType().getTypeID());
+ Assertions.assertEquals(new ArrowType.Int(32, true),
array.getChildren().get(0).getType());
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]