github-actions[bot] commented on code in PR #68786:
URL: https://github.com/apache/doris/pull/68786#discussion_r4229820646


##########
be/src/core/data_type_serde/data_type_string_serde.cpp:
##########
@@ -568,7 +568,12 @@ Status 
DataTypeStringSerDeBase<ColumnType>::write_column_to_iceberg_arrow(
         arrow::ArrayBuilder* array_builder, int64_t start, int64_t end,
         const cctz::time_zone& ctz) const {
     if (!is_iceberg_uuid_field(field)) {
-        // Keep the existing CHAR/STRING fixed-binary binding until external 
type mappings change.
+        // Iceberg fixed(N) is a byte binding, so text padding must not change 
its physical value.

Review Comment:
   [P2] Preserve legacy FIXED writes during BE-first upgrades. An older FE with 
the default `enable.mapping.varbinary=false` still plans Iceberg `FIXED(N)` as 
`CHAR(N)` and sends that type to a new BE. This branch rejects its fixed-size 
Arrow builder before the previous width-checked CHAR writer runs, so inserts 
that worked before fail throughout the rolling upgrade. Keep the legacy CHAR 
binding for old-FE plans or version the FE/BE write contract, and cover an 
old-FE/new-BE fixed-column insert.



##########
fe/be-java-extensions/hadoop-hudi-scanner/src/test/java/org/apache/doris/hudi/HadoopHudiColumnValueTest.java:
##########
@@ -31,21 +31,89 @@
 
 public class HadoopHudiColumnValueTest {
     @Test
-    public void testInt64TimestampUsesSessionTimezone() {
+    public void testInstantCarriersAcrossTimezonesAndPrecisions() {
+        java.util.TimeZone original = java.util.TimeZone.getDefault();
+        try {
+            for (String zone : new String[] {"UTC", "Asia/Shanghai", 
"America/New_York"}) {
+                
java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone(zone));
+                for (int precision : new int[] {3, 6}) {
+                    for (long epoch : new long[] {-1, 0, 1636263000123L, 
1636266600123L}) {
+                        if (precision == 6 && epoch > 0) {
+                            epoch = epoch * 1000 + 456;
+                        }
+                        long units = precision == 3 ? 1000L : 1_000_000L;
+                        java.time.Instant instant = 
java.time.Instant.ofEpochSecond(Math.floorDiv(epoch, units),
+                                Math.floorMod(epoch, units) * (1_000_000_000L 
/ units));
+                        LocalDateTime expected = 
LocalDateTime.ofInstant(instant, java.time.ZoneOffset.UTC);
+                        HadoopHudiColumnValue value = new 
HadoopHudiColumnValue(ZoneId.of(zone));
+                        value.setField(ColumnType.parseType("ts", 
"timestamptz(" + precision + ")"),
+                                
PrimitiveObjectInspectorFactory.writableTimestampObjectInspector);
+                        value.setRow(new LongWritable(epoch));
+                        Assertions.assertEquals(expected, 
value.getTimeStampTz());
+                        value.setRow(java.sql.Timestamp.from(instant));
+                        Assertions.assertEquals(expected, 
value.getTimeStampTz());
+                        value.setRow(new 
TimestampWritableV2(Timestamp.ofEpochSecond(
+                                instant.getEpochSecond(), instant.getNano())));
+                        Assertions.assertEquals(expected, 
value.getTimeStampTz());
+                        value.setRow(null);
+                        Assertions.assertTrue(value.isNull());
+                    }
+                }
+            }
+        } finally {
+            java.util.TimeZone.setDefault(original);
+        }
+    }
+
+    @Test
+    public void testInstantTimestampUsesUtcComponents() {
+        HadoopHudiColumnValue value = new 
HadoopHudiColumnValue(ZoneId.of("Asia/Shanghai"));
+        value.setField(ColumnType.parseType("ts", "timestamptz(6)"), null);
+        value.setRow(new LongWritable(-1));
+        Assertions.assertEquals(LocalDateTime.of(1969, 12, 31, 23, 59, 59, 
999999000), value.getTimeStampTz());
+        value.setField(ColumnType.parseType("ts", "timestamptz(6)"),
+                
PrimitiveObjectInspectorFactory.writableTimestampObjectInspector);
+        value.setRow(new TimestampWritableV2(Timestamp.ofEpochSecond(1, 
111333000)));
+        Assertions.assertEquals(LocalDateTime.of(1970, 1, 1, 0, 0, 1, 
111333000), value.getTimeStampTz());
+    }
+
+    @Test
+    public void testInt64LocalTimestampIgnoresSessionTimezone() {
         HadoopHudiColumnValue value = new 
HadoopHudiColumnValue(ZoneId.of("America/Los_Angeles"));
         value.setField(ColumnType.parseType("ts", "datetimev2(6)"), null);
         value.setRow(new LongWritable(0));
 
-        Assertions.assertEquals(LocalDateTime.of(1969, 12, 31, 16, 0), 
value.getDateTime());
+        Assertions.assertEquals(LocalDateTime.of(1970, 1, 1, 0, 0), 
value.getDateTime());
     }
 
     @Test
-    public void testInt96TimestampUsesSessionTimezone() {
+    public void testInt96LocalTimestampIgnoresSessionTimezone() {
         HadoopHudiColumnValue value = new 
HadoopHudiColumnValue(ZoneId.of("America/Los_Angeles"));
         value.setField(ColumnType.parseType("ts", "datetimev2(6)"),
                 
PrimitiveObjectInspectorFactory.writableTimestampObjectInspector);
         value.setRow(new TimestampWritableV2(Timestamp.ofEpochSecond(0)));
 
-        Assertions.assertEquals(LocalDateTime.of(1969, 12, 31, 16, 0), 
value.getDateTime());
+        Assertions.assertEquals(LocalDateTime.of(1970, 1, 1, 0, 0), 
value.getDateTime());
     }
+
+    @Test
+    public void testJniRejectsUtcYearOverflow() {
+        org.apache.doris.jni.spi.utils.OffHeap.setTesting();
+        org.apache.doris.jni.spi.vec.ColumnType columnType =
+                org.apache.doris.jni.spi.vec.ColumnType.parseType("ts", 
"timestamptz(6)");
+        for (String text : new String[] {"0000-12-31T23:59:59Z", 
"+10000-01-01T00:00:00Z"}) {

Review Comment:
   [P1] Make the scanner boundary tests accept UTC year zero. This loop expects 
`appendValue` to throw for `0000-12-31T23:59:59Z`, but Hudi converts it to a 
UTC year-zero `LocalDateTime` and the corrected `VectorColumn.putTimeStampTz` 
accepts years 0–9999; `VectorColumnTimestampTzTest` explicitly expects that 
behavior. The same stale assertion exists in the new Fluss, Paimon, and Trino 
scanner tests, so all four fail on their first iteration. Assert success for 
year zero and reserve `assertThrows` for year 10000. This is separate from the 
earlier encoder bug thread.



##########
regression-test/suites/external_table_p0/hive/test_hive_orc.groovy:
##########
@@ -275,7 +287,8 @@ suite("test_hive_orc", "p0,external") {
             }
 
         } finally {
+            // A failed assertion must not leave derived objects behind for 
the next run.
+            sql "DROP DATABASE IF EXISTS internal.test_view_varbinary_db FORCE"

Review Comment:
   [P3] Leave this fixture available after the suite. The database is already 
dropped before creation at line 247, so this new `finally` force-drop removes 
the view and other derived objects even when an assertion fails. Repository 
regression rules preserve test tables for debugging; remove the post-test drop 
and keep the setup drop.



##########
regression-test/suites/external_table_p0/hive/test_hive_orc.groovy:
##########
@@ -242,22 +244,32 @@ suite("test_hive_orc", "p0,external") {
             order_qt_sql_topn_binary_col4 """ select  
binary_col,cast(binary_col as string) from  orc_all_types order by binary_col 
desc,string_col desc limit 10; """
 
             sql """ switch internal; """
-            sql """ drop database if exists test_view_varbinary_db"""
+            sql """ drop database if exists test_view_varbinary_db force"""
             sql """ create database if not exists test_view_varbinary_db"""
             sql """use test_view_varbinary_db"""
+            // Binary mapping enables transport; compare hexadecimal strings 
without binary hash keys.
+            def binaryQuery = ("SELECT binary_col FROM 
`test_hive_orc_mapping_varbinary`.`default`.`orc_all_types` "
+                    + "ORDER BY int_col, from_binary(binary_col) LIMIT 100")
+            def binarySource = "(${binaryQuery}) binary_src"
+            def expectedBinary = sql "SELECT from_binary(binary_col) FROM 
${binarySource} ORDER BY from_binary(binary_col)"
+            // Views retain execution types; materialized objects still obey 
native storage restrictions.
+            sql "CREATE VIEW test_view_varbinary AS SELECT binary_col FROM 
${binarySource}"
+            assertEquals(expectedBinary,
+                    sql("SELECT from_binary(binary_col) FROM 
test_view_varbinary ORDER BY from_binary(binary_col)"))
             test {
-                sql " create view test_view_varbinary as select binary_col 
from `test_hive_orc_mapping_varbinary`.`default`.`orc_all_types`; "
-                exception " View does not support VARBINARY type: binary_col"
+                sql """CREATE TABLE test_ctas_varbinary DISTRIBUTED BY RANDOM 
BUCKETS 2
+                       PROPERTIES ('replication_num'='1') AS SELECT binary_col 
FROM ${binarySource}"""
+                exception "varbinary"
             }
-
             test {
                 sql """ CREATE MATERIALIZED VIEW test_mv_varbinary
                         BUILD DEFERRED REFRESH AUTO ON MANUAL
                         DISTRIBUTED BY RANDOM BUCKETS 2
                         PROPERTIES ('replication_num' = '1')
-                        AS select binary_col from 
`test_hive_orc_mapping_varbinary`.`default`.`orc_all_types`; """
-                exception " MTMV do not support varbinary type : binary_col"
+                        AS SELECT binary_col FROM ${binarySource}"""
+                exception "varbinary"
             }
+            assertTrue(sql("DESC 
test_view_varbinary")[0][1].toLowerCase().startsWith("varbinary"))

Review Comment:
   [P3] Record this fixed view schema in generated output. `DESC 
test_view_varbinary` has a deterministic VARBINARY result, but this assertion 
keeps it outside the suite's `.out`. The regression rules require determined 
results in a named `qt`/`order_qt` check with runner-generated output; please 
add that check and regenerate this suite's expected output.



##########
fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonScanPlanProvider.java:
##########
@@ -1927,6 +1947,73 @@ private static boolean projectsVariant(
                 .anyMatch(index -> containsVariant(rowType.getTypeAt(index)));
     }
 
+    private static Set<Integer> scanReadFieldIds(RowType rowType, 
List<ConnectorColumnHandle> columns,
+            Optional<ConnectorExpression> filter) {
+        Set<String> names = 
columns.stream().filter(PaimonColumnHandle.class::isInstance)
+                .map(PaimonColumnHandle.class::cast)
+                .filter(column -> !column.isMetadataColumn())
+                .map(column -> column.getName().toLowerCase(Locale.ROOT))
+                .collect(Collectors.toSet());
+        filter.ifPresent(expression -> collectFilterColumnNames(expression, 
names));
+        return rowType.getFields().stream()
+                .filter(field -> 
names.contains(field.name().toLowerCase(Locale.ROOT)))
+                .map(DataField::id).collect(Collectors.toSet());
+    }
+
+    private static void collectFilterColumnNames(ConnectorExpression 
expression, Set<String> names) {
+        if (expression instanceof ConnectorColumnRef) {
+            names.add(((ConnectorColumnRef) 
expression).getColumnName().toLowerCase(Locale.ROOT));
+        }
+        expression.getChildren().forEach(child -> 
collectFilterColumnNames(child, names));
+    }
+
+    static boolean requiresLegacyOrcTimestampReader(Table table, 
Optional<List<RawFile>> rawFiles,
+            Set<Integer> readFieldIds, Map<Long, Boolean> schemaTimestamps) {
+        if (readFieldIds.isEmpty() || !rawFiles.isPresent()
+                || rawFiles.get().stream().noneMatch(f -> 
f.path().endsWith(".orc"))
+                || !new org.apache.paimon.options.Options(table.options()).get(
+                        
org.apache.paimon.format.OrcOptions.ORC_TIMESTAMP_LTZ_LEGACY_TYPE)) {
+            return false;
+        }
+        // Only decoded fields require SDK timezone conversion. Unread LTZ 
columns must not disable
+        // native splitting or metadata columns; stable field IDs also scope 
historical schemas after renames.
+        if (readsTimestampLtz(table.rowType(), readFieldIds)) {
+            return true;
+        }
+        FileStoreTable fileStoreTable = (FileStoreTable) table;
+        for (RawFile file : rawFiles.get()) {
+            if (file.path().endsWith(".orc") && 
schemaTimestamps.computeIfAbsent(file.schemaId(),
+                    id -> readsTimestampLtz(
+                            
fileStoreTable.schemaManager().schema(id).logicalRowType(), readFieldIds))) {

Review Comment:
   [P2] Resolve historical ORC schemas from each fallback split's branch. A 
`scan.fallback-branch` plan can contain raw ORC files from both branches, but 
this lookup uses the pair's `schemaManager()`, which delegates only to its 
wrapped branch, and caches by numeric schema ID. With 
`orc.timestamp-ltz.legacy.type=true`, a fallback file whose schema ID exists 
only on the other branch makes a scan of even a current INT field fail during 
planning. Select the branch from `FallbackDataSplit` before schema lookup and 
include it in the cache key; cover independently advanced branch schema IDs. 
This is separate from the JNI metadata iterator threads.



##########
fe/fe-core/src/main/java/org/apache/doris/tablefunction/ExternalFileTableValuedFunction.java:
##########
@@ -222,15 +222,15 @@ protected Map<String, String> 
parseCommonProperties(Map<String, String> properti
         String formatString = getOrDefaultAndRemove(copiedProps, 
FileFormatConstants.PROP_FORMAT, "").toLowerCase();
         fileFormatProperties = 
FileFormatProperties.createFileFormatProperties(formatString);
 
-        // Parse enable_mapping_varbinary property
+        // The catalog property was removed, but this TVF-only option must 
remain explicit because
+        // changing an ad-hoc TVF result schema also breaks CTAS type 
inference.
         String enableMappingVarbinaryStr = getOrDefaultAndRemove(copiedProps,
                 FileFormatConstants.PROP_ENABLE_MAPPING_VARBINARY, "false");
         fileFormatProperties.enableMappingVarbinary = 
Boolean.parseBoolean(enableMappingVarbinaryStr);
 
-        // Parse enable_mapping_timestamp_tz property
-        String enableMappingTimestampTzStr = getOrDefaultAndRemove(copiedProps,
-                FileFormatConstants.PROP_ENABLE_MAPPING_TIMESTAMP_TZ, "false");
-        fileFormatProperties.enableMappingTimestampTz = 
Boolean.parseBoolean(enableMappingTimestampTzStr);
+        // Consume the legacy option, but let file logical types determine 
timezone semantics.
+        
copiedProps.remove(FileFormatConstants.PROP_ENABLE_MAPPING_TIMESTAMP_TZ);

Review Comment:
   [P2] Preserve the output type of existing file-TVF views. A view created 
over an ORC `TIMESTAMP_INSTANT` file with `enable_mapping_timestamp_tz=false` 
(or the old default) stores a DATETIMEV2 column. This code now discards 
explicit false and always infers TIMESTAMPTZ when the view expands, while 
`LogicalView` keeps the new child type and the saved view schema still 
advertises DATETIMEV2. Preserve the view's stored type contract or migrate 
these views, and cover an upgraded TVF view. The catalog migration view thread 
does not cover this independent TVF path.



##########
fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergWriteSchemaContext.java:
##########
@@ -158,6 +158,10 @@ private IcebergWriteSchemaContext(String tableName, Schema 
schema, int formatVer
             ConnectorColumn column = new ConnectorColumn(
                     field.name(), type, field.doc() == null ? "" : field.doc(),
                     field.isOptional(), null, 
true).withUniqueId(field.fieldId());
+            if (enableMappingVarbinary && field.type().typeId() == 
Type.TypeID.UUID) {

Review Comment:
   [P2] Apply UUID text conversion inside complex fields. `IcebergTypeMapping` 
now maps nested UUID leaves to `VARBINARY(16)`, but this marker is set only 
when the top-level column is UUID. An `ARRAY<UUID>` or struct UUID inserted 
with canonical text therefore reaches the nested fixed-size Arrow writer as 36 
bytes (or fails FE coercion), whereas the former nested STRING writer parsed it 
as UUID. Carry the semantic conversion recursively and cover array and struct 
UUID writes. This is separate from the scalar UUID thread.



##########
fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonScanPlanProvider.java:
##########
@@ -1927,6 +1947,73 @@ private static boolean projectsVariant(
                 .anyMatch(index -> containsVariant(rowType.getTypeAt(index)));
     }
 
+    private static Set<Integer> scanReadFieldIds(RowType rowType, 
List<ConnectorColumnHandle> columns,
+            Optional<ConnectorExpression> filter) {
+        Set<String> names = 
columns.stream().filter(PaimonColumnHandle.class::isInstance)
+                .map(PaimonColumnHandle.class::cast)
+                .filter(column -> !column.isMetadataColumn())
+                .map(column -> column.getName().toLowerCase(Locale.ROOT))
+                .collect(Collectors.toSet());
+        filter.ifPresent(expression -> collectFilterColumnNames(expression, 
names));
+        return rowType.getFields().stream()
+                .filter(field -> 
names.contains(field.name().toLowerCase(Locale.ROOT)))
+                .map(DataField::id).collect(Collectors.toSet());
+    }
+
+    private static void collectFilterColumnNames(ConnectorExpression 
expression, Set<String> names) {
+        if (expression instanceof ConnectorColumnRef) {
+            names.add(((ConnectorColumnRef) 
expression).getColumnName().toLowerCase(Locale.ROOT));
+        }
+        expression.getChildren().forEach(child -> 
collectFilterColumnNames(child, names));
+    }
+
+    static boolean requiresLegacyOrcTimestampReader(Table table, 
Optional<List<RawFile>> rawFiles,
+            Set<Integer> readFieldIds, Map<Long, Boolean> schemaTimestamps) {
+        if (readFieldIds.isEmpty() || !rawFiles.isPresent()
+                || rawFiles.get().stream().noneMatch(f -> 
f.path().endsWith(".orc"))
+                || !new org.apache.paimon.options.Options(table.options()).get(

Review Comment:
   [P2] Check the legacy ORC option on the split's branch. A fallback read can 
combine branches with the same LTZ row type but different 
`orc.timestamp-ltz.legacy.type` settings. This gate reads only the wrapped 
table's option: if wrapped=false and fallback=true, it returns false before 
inspecting a fallback ORC split, so FE chooses the native reader for bytes that 
need Paimon's legacy LTZ conversion. Use the owning branch's option for each 
split and cover mixed branch settings. This is independent of the historical 
schema lookup at line 1987.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to