JingsongLi commented on code in PR #1106:
URL: https://github.com/apache/paimon-rust/pull/1106#discussion_r4261113667


##########
crates/integrations/datafusion/src/procedures.rs:
##########
@@ -800,6 +804,192 @@ async fn proc_drop_global_index(
     ok_result(ctx)
 }
 
+/// Semicolon-separated alternatives, each containing comma-separated 
key=value constraints.
+fn parse_global_index_partitions(table: &Table, partitions: &str) -> 
DFResult<Predicate> {
+    let partition_fields = table.schema().partition_fields();
+    if partition_fields.is_empty() {
+        return Err(DataFusionError::Plan(
+            "Cannot filter global index partitions on an unpartitioned 
table".to_string(),
+        ));
+    }
+    let pb = PredicateBuilder::new(table.schema().fields());
+    let options = CoreOptions::new(table.schema().options());
+    let mut alternatives = Vec::new();
+    for partition in partitions.split(';') {
+        let mut constraints = Vec::new();
+        let mut keys = std::collections::HashSet::new();
+        for entry in partition.split(',') {
+            let (key, value) = entry.split_once('=').ok_or_else(|| {
+                DataFusionError::Plan(format!(
+                    "Invalid partitions entry '{entry}'. Expected key=value"
+                ))
+            })?;
+            // Java ParameterUtils uses String.trim(), not Unicode whitespace 
trimming.
+            let key = key.trim_matches(|ch| ch <= ' ');
+            let value = value.trim_matches(|ch| ch <= ' ');
+            let field = partition_fields
+                .iter()
+                .find(|field| field.name() == key)
+                .ok_or_else(|| {
+                    DataFusionError::Plan(format!("Column '{key}' is not a 
partition column"))
+                })?;
+            if !keys.insert(key) {
+                return Err(DataFusionError::Plan(format!(
+                    "Duplicate partition column '{key}'"
+                )));
+            }
+            if value.is_empty() {
+                return Err(DataFusionError::Plan(format!(
+                    "Empty partitions value for column '{key}'"
+                )));
+            }
+            let predicate = if value == options.partition_default_name() {
+                pb.is_null(key)
+            } else {
+                let datum = parse_global_index_partition_value(
+                    value,
+                    field.data_type(),
+                    options.legacy_partition_name(),
+                )
+                .ok_or_else(|| {
+                    DataFusionError::Plan(format!(
+                        "Invalid partitions value '{value}' for column '{key}' 
with type {:?}",
+                        field.data_type()
+                    ))
+                })?;
+                pb.equal(key, datum)
+            };
+            constraints.push(predicate.map_err(to_datafusion_error)?);
+        }
+        alternatives.push(Predicate::and(constraints));
+    }
+    Ok(Predicate::or(alternatives))
+}
+
+fn parse_global_index_partition_value(
+    value: &str,
+    data_type: &DataType,
+    legacy_partition_name: bool,
+) -> Option<Datum> {
+    match data_type {
+        DataType::Char(typ) => {
+            (value.chars().count() <= typ.length()).then(|| 
Datum::String(value.to_string()))
+        }
+        DataType::VarChar(typ) => (value.chars().count() <= typ.length() as 
usize)
+            .then(|| Datum::String(value.to_string())),
+        DataType::Binary(_) => Some(Datum::Bytes(value.as_bytes().to_vec())),
+        DataType::VarBinary(typ) => {
+            (value.len() <= typ.length() as usize).then(|| 
Datum::Bytes(value.as_bytes().to_vec()))
+        }
+        DataType::Float(_) => {
+            // Match TypeUtils.castFromString's check for FLOAT precision loss.
+            let double = value.parse::<f64>().ok()?;
+            let float = value.parse::<f32>().ok()?;
+            (f64::from(float) == double
+                || (float.is_nan() && double.is_nan())
+                || paimon::spec::format_java_float(float).parse::<f64>().ok()? 
== double)
+                .then_some(Datum::Float(float))
+        }
+        DataType::Double(_) => value.parse().ok().map(Datum::Double),
+        DataType::Decimal(typ) => {
+            // Java Decimal.fromBigDecimal rounds HALF_UP; Arrow's string cast 
truncates.
+            if value.contains('_') {
+                return None;
+            }
+            let decimal = value
+                .parse::<bigdecimal::BigDecimal>()
+                .ok()?
+                .with_scale_round(i64::from(typ.scale()), 
bigdecimal::RoundingMode::HalfUp);
+            let unscaled = decimal
+                .as_bigint_and_exponent()
+                .0
+                .to_string()
+                .parse::<i128>()
+                .ok()?;
+            if unscaled
+                .unsigned_abs()
+                .checked_ilog10()
+                .map_or(1, |log| log + 1)
+                > typ.precision()
+            {
+                return None;
+            }
+            Some(Datum::Decimal {
+                unscaled,
+                precision: typ.precision(),
+                scale: typ.scale(),
+            })
+        }
+        DataType::Time(_) => {
+            paimon::table::parse_format_partition_value(value, 
data_type).or_else(|| {
+                let arrow_type = 
paimon::arrow::paimon_type_to_arrow(data_type).ok()?;
+                let scalar = ScalarValue::try_from_string(value.to_string(), 
&arrow_type).ok()?;
+                crate::filter_pushdown::scalar_to_datum(&scalar, data_type)
+            })
+        }
+        DataType::Timestamp(typ) => {
+            parse_global_index_partition_timestamp(value, data_type, 
typ.precision(), false)
+        }
+        DataType::LocalZonedTimestamp(typ) => 
parse_global_index_partition_timestamp(
+            value,
+            data_type,
+            typ.precision(),
+            !legacy_partition_name,
+        ),
+        _ => paimon::table::parse_format_partition_value(value, data_type),
+    }
+}
+
+fn parse_global_index_partition_timestamp(
+    value: &str,
+    data_type: &DataType,
+    precision: u32,
+    local_timezone: bool,
+) -> Option<Datum> {
+    let arrow_type = paimon::arrow::paimon_type_to_arrow(data_type).ok()?;
+    let scalar = if value.bytes().all(|byte| byte.is_ascii_digit()) {
+        // Java only accepts numeric timestamps at these precisions.
+        if !matches!(precision, 0 | 3 | 6 | 9) {
+            return None;
+        }
+        ScalarValue::Int64(Some(value.parse().ok()?))
+            .cast_to(&arrow_type)
+            .ok()?
+    } else {
+        // Legacy partition names omit seconds when both seconds and fractions 
are zero.
+        let value = chrono::NaiveDateTime::parse_from_str(value, 
"%Y-%m-%dT%H:%M")
+            .map(|value| value.format("%Y-%m-%dT%H:%M:%S").to_string())
+            .unwrap_or_else(|_| value.to_string());
+        let value = if local_timezone {
+            // Non-legacy LTZ names use the system-local timezone. Preserve 
explicit offsets
+            // and fail on ambiguous/nonexistent local times instead of 
falling back to UTC.
+            
datafusion::arrow::compute::kernels::cast_utils::string_to_datetime(
+                &chrono::Local,
+                &value,
+            )
+            .ok()?
+            // RFC3339 rounds offsets containing seconds; UTC preserves the 
resolved instant.
+            .with_timezone(&chrono::Utc)
+            .to_rfc3339()
+        } else {
+            value
+        };
+        ScalarValue::try_from_string(value, &arrow_type).ok()?
+    };
+    let mut datum = crate::filter_pushdown::scalar_to_datum(&scalar, 
data_type)?;
+    let (Datum::Timestamp { millis, nanos } | Datum::LocalZonedTimestamp { 
millis, nanos }) =
+        &mut datum
+    else {
+        return None;
+    };
+    // Paimon truncates to the declared precision, which may be between Arrow 
time units.
+    let millis_quantum = 10_i64.pow(3_u32.saturating_sub(precision));
+    *millis = millis.div_euclid(millis_quantum) * millis_quantum;

Review Comment:
   [P2] Preserve the exact stored timestamp partition before deletion
   
   This truncates a selector to the declared precision even when existing 
partitions retain finer values. I reproduced through public APIs: create a 
partitioned table with Catalog::create_table / Schema::builder and 
TimestampType::new(1), then ordinary SQL INSERT rows at 1970-01-01 00:00:00.100 
and .199 and build BTree indexes. Both are valid, readable stored partitions; 
$partitions displays ts=...00.1 and ts=...00.199. CALL 
sys.drop_global_index(..., partitions => 'ts=1970-01-01 00:00:00.199') succeeds 
but deletes the .100 index, leaving the requested .199 index (remaining 
serialized millis [199], expected [100]). SQL CREATE rounds precisions to Arrow 
units, but native/imported table schemas retain intermediate precisions and are 
served by this same SQL procedure. The writer stores the raw milliseconds and 
partition formatting intentionally preserves nonzero fractions beyond the 
declared precision. Please preserve exact stored identity or reject a selector 
that changes during conver
 sion before any deletion. An actual persisted-data regression passes when this 
new truncation is omitted; cover native/imported intermediate-precision schemas 
as well as SQL-created tables.



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

Reply via email to