HyukjinKwon commented on code in PR #146:
URL: 
https://github.com/apache/spark-connect-rust/pull/146#discussion_r4462607391


##########
crates/pyspark-rs/src/dataframe.rs:
##########
@@ -2491,3 +2519,40 @@ impl PyLocalRowIterator {
         }
     }
 }
+
+/// Convert an observed-metric literal to a Python value (mirrors
+/// `LiteralExpression._to_value` for the scalar kinds observations produce). 
Kinds
+/// without a natural Python scalar fall back to their rendered string.
+fn literal_to_py(
+    py: Python<'_>,
+    literal: &spark_connect_proto::expression::Literal,
+) -> PyResult<Py<PyAny>> {
+    use pyo3::IntoPyObjectExt;
+    use spark_connect_proto::expression::literal::LiteralType;
+    Ok(match &literal.literal_type {
+        None | Some(LiteralType::Null(_)) => py.None(),
+        Some(LiteralType::Boolean(b)) => b.into_py_any(py)?,
+        Some(LiteralType::Byte(v))
+        | Some(LiteralType::Short(v))
+        | Some(LiteralType::Integer(v)) => v.into_py_any(py)?,
+        Some(LiteralType::Long(v)) => v.into_py_any(py)?,
+        Some(LiteralType::Float(v)) => (*v as f64).into_py_any(py)?,
+        Some(LiteralType::Double(v)) => v.into_py_any(py)?,
+        Some(LiteralType::String(s)) => s.into_py_any(py)?,
+        Some(LiteralType::Binary(b)) => pyo3::types::PyBytes::new(py, 
b).into_any().unbind(),
+        Some(LiteralType::Decimal(d)) => py
+            .import("decimal")?
+            .getattr("Decimal")?
+            .call1((&d.value,))?
+            .unbind(),
+        Some(LiteralType::Array(a)) => {
+            let items = a
+                .elements
+                .iter()
+                .map(|e| literal_to_py(py, e))
+                .collect::<PyResult<Vec<_>>>()?;
+            items.into_py_any(py)?
+        }
+        Some(_) => 
spark_connect::profiler::format_literal(literal).into_py_any(py)?,

Review Comment:
   **Blocking (P1):** This fallback sends every literal kind not matched above 
through `profiler::format_literal`, which is a display helper. For date, 
timestamp, timestamp_ntz, time, the nanosecond timestamps, every interval kind, 
map, struct and specialized arrays, it returns a fixed type name such as 
`"timestamp"` or `"map"`, not a rendered value (profiler.rs:30-44). So 
`df.observe("o", F.max("ts").alias("m"))` followed by an action gives 
`executionInfo.flows == [("o", {"m": "timestamp"})]`, where PySpark's 
`LiteralExpression._to_value` returns a `datetime`. The wrong value comes back 
without any error, and the doc comment above says these kinds fall back to 
their "rendered string", which is not what happens. These kinds need real 
decoding (date, datetime, time, timedelta, dict, Row, list) to match the 
reference client.
   
   **Verification:**
   - **Regression:** For each literal kind that previously fell back to a type 
name (date, timestamp, timestamp_ntz, time, the nanosecond timestamps, 
calendar, year-month and day-time intervals, map, struct, specialized array), 
an observation whose server response carries that literal yields the reference 
Python value in executionInfo.flows rather than a type-name string.
   - **Behavior:** Nested arrays, maps and structs decode their elements 
recursively, while existing scalar kinds (numbers, strings, binary, decimal) 
keep their current values.



##########
crates/spark-connect/src/session.rs:
##########
@@ -713,6 +720,10 @@ fn datatype_to_arrow(dt: &DataType) -> 
Result<arrow::datatypes::DataType> {
         // TIMESTAMP (LTZ) and TIMESTAMP_NTZ are both micros; NTZ carries no 
zone.
         DataType::Timestamp => A::Timestamp(TimeUnit::Microsecond, 
Some("UTC".into())),
         DataType::TimestampNtz => A::Timestamp(TimeUnit::Microsecond, None),
+        DataType::TimestampLtzNanos { .. } => {

Review Comment:
   **Non-blocking (P2):** The schema side now maps the nanosecond variants to 
`Timestamp(Nanosecond, ..)`, but `values_to_arrow` still only has the 
`DataType::Timestamp | DataType::TimestampNtz` arm (line 867). These variants 
fall to the `Unsupported type for Arrow createDataFrame conversion` catch-all. 
On the base branch a server-reported `TIMESTAMP_LTZ` nanos column decoded as 
`DataType::Timestamp`, so `spark.createDataFrame(df.collect(), df.schema)` 
worked. With this change it raises. `spark.createDataFrame([], 
StructType([StructField("t", TimestampNTZNanosType())]))` and nested 
array/struct/map element types fail the same way. A builder arm that scales the 
row micros to a `TimestampNanosecondArray` (UTC only for LTZ) would keep it 
consistent with this mapping.
   
   **Verification:**
   - **Regression:** Building a local relation from timestamp rows with a 
timestamp_ltz(9) schema, as when re-creating a DataFrame from its own 
server-reported nanos schema, succeeds and keeps the instant, as the base 
branch did when this type decoded as a microsecond timestamp.
   - **Behavior:** Empty and non-empty local data with timestamp_ntz(p) and 
timestamp_ltz(p) columns, including inside an array, produce nanosecond Arrow 
arrays whose zone matches the declared field.



##########
crates/pyspark-rs/src/pipelines.rs:
##########
@@ -162,12 +162,14 @@ pub fn pipeline_define_flow(
 }
 
 /// `PipelineCommand.DefineFlow` carrying an Auto-CDC flow body. 
`stored_as_scd_type` is
-/// the `SCDType` enum value (0 unspecified, 1 SCD_TYPE_1, 2 SCD_TYPE_2).
+/// the `SCDType` enum value (0 unspecified, 1 SCD_TYPE_1, 2 SCD_TYPE_2). The 
SCD2-only
+/// track-history lists and `sql_conf` are validated client-side in 
`pipelines/api.py`.

Review Comment:
   **Nit (P3):** Only the track-history lists are validated in 
`pipelines/api.py` (CANNOT_SET_TOGETHER / 
INVALID_MULTIPLE_ARGUMENT_CONDITIONS). `create_auto_cdc_flow` passes 
`spark_conf=spark_conf or {}` with no check, and the registry forwards 
`dict(flow.spark_conf)` as is, so the `sql_conf` part of this sentence is 
inaccurate.
   
   **Verification:**
   - **Inspection:** The documented validation matches the checks 
create_auto_cdc_flow actually performs on track-history lists and spark_conf.



##########
crates/pyspark-rs/src/errors.rs:
##########
@@ -75,6 +76,11 @@ pub fn spark_error_to_py_exception(err: SparkError) -> PyErr 
{
                 // This creates a PyObject exception instance
                 match exc_class.call1((msg.clone(),)) {
                     Ok(exc_instance) => {
+                        // Mirror the reference client, which stamps 
`_operation_id` on
+                        // errors raised for an ExecutePlan (read via 
`operation_id`).
+                        if let Some(op) = &operation_id {
+                            let _ = exc_instance.setattr("_operation_id", op);

Review Comment:
   **Nit (P3):** This `_operation_id` stamping is never exercised: 
`test_execution_info_and_operation_id` sets `err._operation_id` by hand and 
builds `ExecutionInfo` directly, so it only tests the Python property and the 
vendored classes. Removing or misspelling this setattr, passing the wrong id 
into `ExecutionInfo`, or swapping observed keys and values would keep every 
suite green. Offline tests driven by the mock Connect server (one action whose 
responses carry an operation id and observed metrics, and one failing 
`ExecutePlan`) would cover both the getter and this conversion.
   
   **Verification:**
   - **Regression:** After a mocked action that returns an operation id and 
observed metrics, executionInfo.operation_id equals that id and flows reports 
the observed keys and decoded values.
   - **Regression:** A mocked ExecutePlan failure raises a PySpark exception 
whose operation_id equals the operation id of the failed request.



##########
crates/spark-connect/src/session.rs:
##########
@@ -31,6 +31,9 @@ pub struct ExecutionInfo {
     /// Observed metrics collected during execution (also where `observe(...)`
     /// and UDF-profiler results surface).
     pub observed_metrics: Vec<proto::execute_plan_response::ObservedMetrics>,
+    /// The server-assigned ExecutePlan operation id 
(`ExecutePlanResponse.operation_id`).

Review Comment:
   **Nit (P3):** The id is not server-assigned. 
`ExecutePlanResponseReattachableIterator::new` uses the request's 
`operation_id` or generates `Uuid::new_v4()` on the client before sending 
(crates/spark-connect-core/src/reattach.rs:38-44), and the server echoes it in 
`ExecutePlanResponse.operation_id`. Describing it as client-assigned and echoed 
by the server would avoid misleading callers about when the id is known.
   
   **Verification:**
   - **Inspection:** The field documentation matches the id assignment in the 
reattachable ExecutePlan iterator and no longer calls it server-assigned.



##########
python/pyspark/pipelines/spark_connect_graph_element_registry.py:
##########
@@ -165,7 +167,12 @@ def register_flow(self, flow: Flow) -> None:
 
     def register_auto_cdc_flow(self, flow: AutoCdcFlow) -> None:
         scl_file, scl_line = _scl(flow.source_code_location)
-        stored_as_scd_type = _SCD_TYPE_1 if flow.stored_as_scd_type is not 
None else 0
+        # SCDType enum value: 0 unspecified, else SCD_TYPE_1 / SCD_TYPE_2.
+        stored_as_scd_type = (
+            _SCD_TYPE_BY_VALUE[str(flow.stored_as_scd_type)]

Review Comment:
   **Nit (P3):** This SCD-type mapping and the positional forwarding of 
`track_history_column_list`, `track_history_except_column_list` and 
`spark_conf` below are fork-specific code with no failing signal. 
`test_create_auto_cdc_flow_scd2_track_history` uses 
`LocalGraphElementRegistry`, which only stores the `AutoCdcFlow`, and no Python 
or Rust test inspects the `DefineFlow` this path builds. Mapping `"2"` to 
SCD_TYPE_1, swapping the two track-history arguments, or dropping `sql_conf` 
would all pass. A test through this registry that captures the `DefineFlow` 
offline (as other offline tests do with the mock Connect server) would cover it.
   
   **Verification:**
   - **Regression:** An SCD2 flow registered with track_history_column_list and 
spark_conf produces a DefineFlow carrying SCD_TYPE_2, the expected 
track-history expressions in the include field only, and the given sql_conf.
   - **Behavior:** A flow with no SCD type and no track-history arguments still 
produces an unspecified SCD type, empty track-history lists and an empty 
sql_conf.



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