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


##########
be/src/format/parquet/parquet_predicate.h:
##########
@@ -211,6 +212,55 @@ class ParquetPredicate {
         RowRange row_group_range;
     };
 
+    // An adjusted-to-UTC INT64 timestamp mapped to DATETIMEV2 is displayed in 
local civil time, so
+    // its converted min/max are only a usable bound when the UTC interval 
contains no backward
+    // clock transition. Mirror the unit/adjust derivation in 
TimestampConverter::init and defer the
+    // transition check to the shared v2 helper. Returns true (usable) for 
anything that is not such
+    // a timestamp: a non-adjusted timestamp is shown in UTC (no transitions), 
and INT96 is handled
+    // by its own singleton rule before this is reached.
+    static bool adjusted_utc_timestamp_range_is_monotonic(const FieldSchema* 
col_schema,
+                                                          const std::string& 
encoded_min,
+                                                          const std::string& 
encoded_max,
+                                                          const 
cctz::time_zone& ctz) {
+        if (col_schema->parquet_schema.type != tparquet::Type::type::INT64) {
+            return true;
+        }
+        const auto& schema = col_schema->parquet_schema;
+        bool adjusted = false;
+        int64_t units_per_second = 0;
+        if (schema.__isset.logicalType && 
schema.logicalType.__isset.TIMESTAMP) {
+            const auto& ts = schema.logicalType.TIMESTAMP;
+            adjusted = ts.isAdjustedToUTC;
+            if (ts.unit.__isset.MILLIS) {
+                units_per_second = 1000;
+            } else if (ts.unit.__isset.MICROS) {
+                units_per_second = 1000000;
+            } else if (ts.unit.__isset.NANOS) {
+                units_per_second = 1000000000;
+            }
+        } else if (schema.__isset.converted_type) {
+            // Legacy TIMESTAMP_MILLIS / TIMESTAMP_MICROS carry instant 
(UTC-normalized) semantics.
+            if (schema.converted_type == 
tparquet::ConvertedType::TIMESTAMP_MILLIS) {
+                adjusted = true;
+                units_per_second = 1000;
+            } else if (schema.converted_type == 
tparquet::ConvertedType::TIMESTAMP_MICROS) {
+                adjusted = true;
+                units_per_second = 1000000;
+            }
+        }
+        if (!adjusted || units_per_second == 0) {

Review Comment:
   Do not treat an adjusted TIMESTAMP with no recognized TimeUnit as usable. A 
present but empty (or forward-unknown) `TimeUnit` union is still mapped to 
DATETIMEV2; `ConvertParams` leaves `second_mask=1` and keeps the session 
timezone, whereas this branch returns before checking transitions. Raw seconds 
`[1636263000,1636266600]` therefore recreate the New York rollback false-prune 
this change is meant to fence. Please share the converter's resolved unit 
semantics or conservatively reject these statistics, and add the malformed-unit 
case.



##########
be/src/format/parquet/parquet_predicate.h:
##########
@@ -211,6 +212,55 @@ class ParquetPredicate {
         RowRange row_group_range;
     };
 
+    // An adjusted-to-UTC INT64 timestamp mapped to DATETIMEV2 is displayed in 
local civil time, so
+    // its converted min/max are only a usable bound when the UTC interval 
contains no backward
+    // clock transition. Mirror the unit/adjust derivation in 
TimestampConverter::init and defer the
+    // transition check to the shared v2 helper. Returns true (usable) for 
anything that is not such
+    // a timestamp: a non-adjusted timestamp is shown in UTC (no transitions), 
and INT96 is handled
+    // by its own singleton rule before this is reached.
+    static bool adjusted_utc_timestamp_range_is_monotonic(const FieldSchema* 
col_schema,
+                                                          const std::string& 
encoded_min,
+                                                          const std::string& 
encoded_max,
+                                                          const 
cctz::time_zone& ctz) {
+        if (col_schema->parquet_schema.type != tparquet::Type::type::INT64) {
+            return true;
+        }
+        const auto& schema = col_schema->parquet_schema;
+        bool adjusted = false;
+        int64_t units_per_second = 0;
+        if (schema.__isset.logicalType && 
schema.logicalType.__isset.TIMESTAMP) {
+            const auto& ts = schema.logicalType.TIMESTAMP;
+            adjusted = ts.isAdjustedToUTC;
+            if (ts.unit.__isset.MILLIS) {
+                units_per_second = 1000;
+            } else if (ts.unit.__isset.MICROS) {
+                units_per_second = 1000000;
+            } else if (ts.unit.__isset.NANOS) {
+                units_per_second = 1000000000;
+            }
+        } else if (schema.__isset.converted_type) {
+            // Legacy TIMESTAMP_MILLIS / TIMESTAMP_MICROS carry instant 
(UTC-normalized) semantics.
+            if (schema.converted_type == 
tparquet::ConvertedType::TIMESTAMP_MILLIS) {
+                adjusted = true;
+                units_per_second = 1000;
+            } else if (schema.converted_type == 
tparquet::ConvertedType::TIMESTAMP_MICROS) {
+                adjusted = true;
+                units_per_second = 1000000;
+            }
+        }
+        if (!adjusted || units_per_second == 0) {
+            return true;
+        }
+        if (encoded_min.size() < sizeof(int64_t) || encoded_max.size() < 
sizeof(int64_t)) {
+            return true;
+        }
+        const auto raw_min = *reinterpret_cast<const 
int64_t*>(encoded_min.data());
+        const auto raw_max = *reinterpret_cast<const 
int64_t*>(encoded_max.data());
+        return format::utc_timestamp_range_is_monotonic(

Review Comment:
   Reject the raw inversion before flooring or taking the adjustment exemption. 
For example, MICROS `raw_min=1500000` and `raw_max=1000000` both floor to 
second 1, so this returns usable even though conversion publishes 
`min=...01.500000` and `max=...01.000000`. All footer/page predicate and 
expr-zone-map callers then trust that inverted interval and can prune a 
matching value. The v2 path checks raw `min_value > max_value` first; please do 
the same here (including non-adjusted/TIMESTAMPTZ paths) and cover the 
same-second case.



##########
be/src/format/parquet/parquet_predicate.h:
##########
@@ -211,6 +212,55 @@ class ParquetPredicate {
         RowRange row_group_range;
     };
 
+    // An adjusted-to-UTC INT64 timestamp mapped to DATETIMEV2 is displayed in 
local civil time, so
+    // its converted min/max are only a usable bound when the UTC interval 
contains no backward
+    // clock transition. Mirror the unit/adjust derivation in 
TimestampConverter::init and defer the
+    // transition check to the shared v2 helper. Returns true (usable) for 
anything that is not such
+    // a timestamp: a non-adjusted timestamp is shown in UTC (no transitions), 
and INT96 is handled
+    // by its own singleton rule before this is reached.
+    static bool adjusted_utc_timestamp_range_is_monotonic(const FieldSchema* 
col_schema,
+                                                          const std::string& 
encoded_min,
+                                                          const std::string& 
encoded_max,
+                                                          const 
cctz::time_zone& ctz) {
+        if (col_schema->parquet_schema.type != tparquet::Type::type::INT64) {
+            return true;
+        }
+        const auto& schema = col_schema->parquet_schema;
+        bool adjusted = false;
+        int64_t units_per_second = 0;
+        if (schema.__isset.logicalType && 
schema.logicalType.__isset.TIMESTAMP) {
+            const auto& ts = schema.logicalType.TIMESTAMP;
+            adjusted = ts.isAdjustedToUTC;
+            if (ts.unit.__isset.MILLIS) {
+                units_per_second = 1000;
+            } else if (ts.unit.__isset.MICROS) {
+                units_per_second = 1000000;
+            } else if (ts.unit.__isset.NANOS) {
+                units_per_second = 1000000000;
+            }
+        } else if (schema.__isset.converted_type) {
+            // Legacy TIMESTAMP_MILLIS / TIMESTAMP_MICROS carry instant 
(UTC-normalized) semantics.
+            if (schema.converted_type == 
tparquet::ConvertedType::TIMESTAMP_MILLIS) {
+                adjusted = true;
+                units_per_second = 1000;
+            } else if (schema.converted_type == 
tparquet::ConvertedType::TIMESTAMP_MICROS) {
+                adjusted = true;
+                units_per_second = 1000000;
+            }
+        }
+        if (!adjusted || units_per_second == 0) {
+            return true;
+        }
+        if (encoded_min.size() < sizeof(int64_t) || encoded_max.size() < 
sizeof(int64_t)) {

Review Comment:
   This length check is too late to protect external statistics: the INT64 
switch arm has already dereferenced both strings as eight-byte values before 
this helper runs, and the INT96 arm has already copied caller-controlled 
lengths under release-disabled `DCHECK`s. Short INT64 bounds can read out of 
bounds; equal 11-byte INT96 bounds are mislaid but pass raw equality; oversized 
INT96 bounds can overrun the 24-byte destination. Validate exact physical 
widths at the start of `parse_min_max_value` and return `DataQualityError` 
before any load/copy.



##########
be/test/format/parquet/parquet_expr_test.cpp:
##########
@@ -2388,4 +2389,90 @@ TEST_F(ParquetExprTest, 
test_bloom_filter_reused_after_first_load) {
     EXPECT_EQ(2, loader_calls);
 }
 
+namespace {
+
+std::string encode_i64(int64_t v) {
+    return std::string(reinterpret_cast<const char*>(&v), sizeof(v));
+}
+
+// #pragma pack(1) ParquetInt96 is {int64 lo (nanos in day); int32 hi (days 
from julian epoch)}.
+std::string encode_int96(int64_t nanos_in_day, int32_t julian_day) {
+    std::string out;
+    out.append(reinterpret_cast<const char*>(&nanos_in_day), 
sizeof(nanos_in_day));
+    out.append(reinterpret_cast<const char*>(&julian_day), sizeof(julian_day));
+    return out;
+}
+
+FieldSchema make_int64_utc_timestamp_schema(bool adjusted_to_utc) {
+    FieldSchema fs;
+    fs.parquet_schema.__set_type(tparquet::Type::INT64);
+    tparquet::MicroSeconds micros;
+    tparquet::TimeUnit unit;
+    unit.__set_MICROS(micros);
+    tparquet::TimestampType ts;
+    ts.__set_isAdjustedToUTC(adjusted_to_utc);
+    ts.__set_unit(unit);
+    tparquet::LogicalType lt;
+    lt.__set_TIMESTAMP(ts);
+    fs.parquet_schema.__set_logicalType(lt);
+    fs.data_type = 
DataTypeFactory::instance().create_data_type(TYPE_DATETIMEV2, false, 0, 6);
+    return fs;
+}
+
+} // namespace
+
+// An adjusted-to-UTC timestamp whose min/max straddle a backward clock 
transition is not a usable
+// bound: an interior instant can map outside the converted civil range. New 
York falls back at
+// 2021-11-07 06:00 UTC.
+TEST_F(ParquetExprTest, ParseMinMaxRejectsTimestampRangeCrossingClockRollback) 
{
+    cctz::time_zone ny;
+    ASSERT_TRUE(cctz::load_time_zone("America/New_York", &ny));
+    auto schema = make_int64_utc_timestamp_schema(/*adjusted_to_utc=*/true);
+    constexpr int64_t kUsPerSec = 1000000;
+
+    auto parse = [&](int64_t min_sec, int64_t max_sec, const cctz::time_zone& 
tz) {
+        Field min_field;
+        Field max_field;
+        return ParquetPredicate::parse_min_max_value(&schema, 
encode_i64(min_sec * kUsPerSec),
+                                                     encode_i64(max_sec * 
kUsPerSec), tz,
+                                                     &min_field, &max_field);
+    };
+
+    // 05:30 .. 06:30 UTC crosses the 06:00 UTC rollback.
+    EXPECT_FALSE(parse(1636263000, 1636266600, ny).ok());
+    // Noon that day, no transition in range.
+    EXPECT_TRUE(parse(1636286400, 1636290000, ny).ok());
+    // Spring-forward on 2021-03-14 07:00 UTC must not be rejected for 
crossing the gap.
+    EXPECT_TRUE(parse(1615703400, 1615707000, ny).ok());
+    // The same rollback range is fine when the timestamp is displayed in UTC 
(not adjusted).
+    auto utc_schema = 
make_int64_utc_timestamp_schema(/*adjusted_to_utc=*/false);
+    Field lo;
+    Field hi;
+    EXPECT_TRUE(
+            ParquetPredicate::parse_min_max_value(&utc_schema, 
encode_i64(1636263000 * kUsPerSec),
+                                                  encode_i64(1636266600 * 
kUsPerSec), ny, &lo, &hi)
+                    .ok());
+}
+
+// INT96 stats are only trustworthy when min == max (PARQUET-1065). This is 
the page-index path,
+// which does not go through read_column_stats.
+TEST_F(ParquetExprTest, ParseMinMaxAppliesInt96SingletonRule) {
+    FieldSchema fs;
+    fs.parquet_schema.__set_type(tparquet::Type::INT96);
+    fs.data_type = 
DataTypeFactory::instance().create_data_type(TYPE_DATETIMEV2, false, 0, 6);
+
+    const int32_t julian_2021_11_07 = ParquetInt96::JULIAN_EPOCH_OFFSET_DAYS + 
18938;
+    auto a = encode_int96(12L * 3600 * 1000000000, julian_2021_11_07);

Review Comment:
   Please make the unequal INT96 case distinguish raw-byte inequality from 
decoded-value inequality. The current bounds differ by an hour, so a regression 
to comparing decoded DATETIMEV2 values would still pass. Two encodings on the 
same Julian day that differ only below one microsecond (for example 
nanos-of-day `N+1` and `N+2`) decode to the same Doris value but must still be 
rejected under the raw singleton rule.



-- 
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