This is an automated email from the ASF dual-hosted git repository.

HyukjinKwon pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark-connect-rust.git


The following commit(s) were added to refs/heads/master by this push:
     new 32d44ec  [SPARK-59075][INFRA] Raise Rust test coverage above 90%
32d44ec is described below

commit 32d44ec2fe8fc2b1bf93312a2f0055be70e1eedd
Author: Hyukjin Kwon <[email protected]>
AuthorDate: Fri Aug 28 16:49:48 2026 +0900

    [SPARK-59075][INFRA] Raise Rust test coverage above 90%
    
    ### What changes were proposed in this pull request?
    
    The Rust coverage job (SPARK-59071) measured **85.6%** line coverage. Using 
the per-file `llvm-cov` report, this PR targets the specific uncovered lines to 
push coverage above 90%. It does two things:
    
    **1. Wire in existing-but-unrun e2e tests + add per-file reporting**
    - Adds `e2e_streaming` and `e2e_new_methods` (previously not run by any CI 
job) to the coverage job's e2e subset, so the `streaming.rs` / 
`dataframe`/`group`/`session` runtime paths they exercise are measured.
    - Prints a per-file coverage summary table into the job log and uploads the 
full `llvm-cov` + Python coverage reports as PR artifacts, so gaps are visible 
without re-running locally.
    
    **2. Add targeted tests for the uncovered lines**
    
    Pure-logic unit/golden tests (no server, run in the default pass):
    - `coverage_types_extra.rs` — `DataType` per-variant serialization 
(`json`/`json_value`/`simple_string`/`type_name`/`to_proto`/`from_proto`), 
`from_json` object + error paths, `from_ddl` error paths 
(char/varchar/time/decimal/geometry/geography).
    - `coverage_plan_extra.rs` — plan builder helpers, 
`LateralJoin`/`StatSampleBy`/`Read` proto conversion, `na_fill` literal arms.
    - `coverage_expression_extra.rs` — every literal variant `to_proto`, inline 
UDF, alias metadata, cast eval modes, case-when else, window frame boundaries.
    - `coverage_ml_extra.rs` — `Params`/`OperatorType`/`MlOperator` + 
estimator/transformer/evaluator builders and accessors 
(`fit`/`transform`/`evaluate` remain e2e).
    - `coverage_column_streaming_extra.rs` — column alias metadata, `try_cast`, 
operator traits, window `over()`, `lit` long branch.
    - inline `arrow_value_tests` in `dataframe.rs` — the private Arrow→`Value` 
converter arms (unsigned ints, Decimal128/256, large/view/time/interval types, 
nested list/struct/map) plus `map_key_to_string` / `i128_to_decimal_string` / 
`micros_to_time_string`.
    
    Behavioral e2e tests (against the live Connect server):
    - `e2e_ops_breadth.rs` — builds and collects a DataFrame for every 
operation, so `assign_plan_ids` walks all `RelType` arms and the per-op 
execution methods run.
    - `e2e_session_catalog.rs` — session runtime helpers + the full `Catalog` 
RPC surface.
    
    The two new e2e files are wired into the coverage job's pass-2 e2e list.
    
    ### Why are the changes needed?
    
    To bring the Rust coverage badge above 90% by genuinely exercising the 
runtime and pure-logic paths that were previously untested, and to make 
per-file gaps visible for future maintenance.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No. Tests and CI only.
    
    ### How was this patch tested?
    
    CI (the coverage workflow itself + the standard Rust build/test jobs that 
compile all test targets). The coverage job's per-file summary and the 
resulting badge percentage verify the improvement.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code
    
    Closes #75 from HyukjinKwon/SPARK-59075.
    
    Authored-by: Hyukjin Kwon <[email protected]>
    Signed-off-by: Hyukjin Kwon <[email protected]>
---
 .github/workflows/coverage.yml                     |   7 +
 crates/spark-connect/src/dataframe.rs              | 248 +++++++++++++++
 crates/spark-connect/src/observation.rs            |  18 ++
 crates/spark-connect/src/profiler.rs               | 130 ++++++++
 crates/spark-connect/src/resource.rs               |  16 +
 .../tests/coverage_column_streaming_extra.rs       |  82 +++++
 .../tests/coverage_expression_extra.rs             | 148 +++++++++
 crates/spark-connect/tests/coverage_ml_extra.rs    | 221 ++++++++++++++
 crates/spark-connect/tests/coverage_plan_extra.rs  | 146 +++++++++
 .../spark-connect/tests/coverage_plan_map_udtf.rs  | 135 ++++++++
 crates/spark-connect/tests/coverage_types_extra.rs | 340 +++++++++++++++++++++
 crates/spark-connect/tests/e2e_ops_breadth.rs      | 257 ++++++++++++++++
 crates/spark-connect/tests/e2e_session_catalog.rs  | 188 ++++++++++++
 13 files changed, 1936 insertions(+)

diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index 9140644..5422ec2 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -152,8 +152,13 @@ jobs:
             cargo llvm-cov --no-cfg-coverage --no-report -p 
apache-spark-connect \
               --test e2e_functions --test e2e_columns --test e2e_dataframe \
               --test e2e_coverage --test e2e_integration \
+              --test e2e_streaming --test e2e_new_methods \
+              --test e2e_ops_breadth --test e2e_session_catalog \
               -- --test-threads=1
           cargo llvm-cov report --json --output-path coverage-rust-llvm.json
+          # Per-file table into the job log, so coverage regressions are easy 
to see
+          # (which file dropped) without downloading the JSON artifact.
+          cargo llvm-cov report --summary-only
 
       - name: Parse Rust coverage and create badge JSON
         # Extract line coverage % from llvm-cov JSON output and convert to 
shields.io
@@ -299,6 +304,8 @@ jobs:
           path: |
             coverage-rust-badge.json
             coverage-python-badge.json
+            coverage-rust-llvm.json
+            python/coverage-python.json
           retention-days: 5
 
       - name: Upload Connect server log
diff --git a/crates/spark-connect/src/dataframe.rs 
b/crates/spark-connect/src/dataframe.rs
index 3f43030..fc771af 100644
--- a/crates/spark-connect/src/dataframe.rs
+++ b/crates/spark-connect/src/dataframe.rs
@@ -2849,3 +2849,251 @@ mod plan_construction_tests {
         ser(&udtf_df);
     }
 }
+
+/// Deterministic coverage of the private Arrow-array -> `Value` converter
+/// (`arrow_value_at`) and its formatting helpers. Builds a one-element array 
of
+/// each Arrow type and asserts the decoded `Value` variant, so the large 
per-type
+/// match runs without a live server (the server->collect->decode path is 
covered
+/// separately by the server-gated e2e_integration tests).
+#[cfg(test)]
+mod arrow_value_tests {
+    use super::*;
+    use arrow::array::*;
+    use arrow::datatypes::{
+        i256, DataType as ArrowDataType, Field, Int32Type, IntervalDayTime, 
IntervalMonthDayNano,
+    };
+    use std::sync::Arc;
+
+    #[test]
+    fn primitives_and_signed_ints() {
+        assert!(matches!(
+            arrow_value_at(&BooleanArray::from(vec![true]), 0).unwrap(),
+            Value::Bool(true)
+        ));
+        assert!(matches!(
+            arrow_value_at(&Int8Array::from(vec![1i8]), 0).unwrap(),
+            Value::Byte(1)
+        ));
+        assert!(matches!(
+            arrow_value_at(&Int16Array::from(vec![1i16]), 0).unwrap(),
+            Value::Short(1)
+        ));
+        assert!(matches!(
+            arrow_value_at(&Int32Array::from(vec![1i32]), 0).unwrap(),
+            Value::Integer(1)
+        ));
+        assert!(matches!(
+            arrow_value_at(&Int64Array::from(vec![1i64]), 0).unwrap(),
+            Value::Long(1)
+        ));
+        assert!(matches!(
+            arrow_value_at(&Float32Array::from(vec![1.0f32]), 0).unwrap(),
+            Value::Float(_)
+        ));
+        assert!(matches!(
+            arrow_value_at(&Float64Array::from(vec![1.0f64]), 0).unwrap(),
+            Value::Double(_)
+        ));
+        assert!(matches!(
+            arrow_value_at(&StringArray::from(vec!["x"]), 0).unwrap(),
+            Value::String(_)
+        ));
+        assert!(matches!(
+            arrow_value_at(&BinaryArray::from_iter_values([b"x".as_ref()]), 
0).unwrap(),
+            Value::Binary(_)
+        ));
+        assert!(matches!(
+            arrow_value_at(&Date32Array::from(vec![1i32]), 0).unwrap(),
+            Value::Date(1)
+        ));
+        assert!(matches!(
+            arrow_value_at(&TimestampMicrosecondArray::from(vec![1i64]), 
0).unwrap(),
+            Value::Timestamp(1)
+        ));
+    }
+
+    #[test]
+    fn unsigned_ints() {
+        assert!(matches!(
+            arrow_value_at(&UInt8Array::from(vec![1u8]), 0).unwrap(),
+            Value::Short(1)
+        ));
+        assert!(matches!(
+            arrow_value_at(&UInt16Array::from(vec![1u16]), 0).unwrap(),
+            Value::Integer(1)
+        ));
+        assert!(matches!(
+            arrow_value_at(&UInt32Array::from(vec![1u32]), 0).unwrap(),
+            Value::Long(1)
+        ));
+        assert!(matches!(
+            arrow_value_at(&UInt64Array::from(vec![1u64]), 0).unwrap(),
+            Value::Long(1)
+        ));
+    }
+
+    #[test]
+    fn decimals_128_and_256() {
+        let d128 = Decimal128Array::from(vec![12345i128])
+            .with_precision_and_scale(10, 2)
+            .unwrap();
+        assert!(matches!(
+            arrow_value_at(&d128, 0).unwrap(),
+            Value::Decimal { .. }
+        ));
+        let d256 = Decimal256Array::from(vec![i256::from_i128(12345)])
+            .with_precision_and_scale(10, 2)
+            .unwrap();
+        assert!(matches!(
+            arrow_value_at(&d256, 0).unwrap(),
+            Value::Decimal { .. }
+        ));
+    }
+
+    #[test]
+    fn large_and_view_bytes() {
+        assert!(matches!(
+            arrow_value_at(&LargeStringArray::from_iter_values(["x"]), 
0).unwrap(),
+            Value::String(_)
+        ));
+        assert!(matches!(
+            
arrow_value_at(&LargeBinaryArray::from_iter_values([b"x".as_ref()]), 
0).unwrap(),
+            Value::Binary(_)
+        ));
+        assert!(matches!(
+            arrow_value_at(&StringViewArray::from_iter_values(["x"]), 
0).unwrap(),
+            Value::String(_)
+        ));
+        assert!(matches!(
+            
arrow_value_at(&BinaryViewArray::from_iter_values([b"x".as_ref()]), 0).unwrap(),
+            Value::Binary(_)
+        ));
+    }
+
+    #[test]
+    fn timestamps_and_date64() {
+        assert!(matches!(
+            arrow_value_at(&TimestampSecondArray::from(vec![1i64]), 
0).unwrap(),
+            Value::Timestamp(_)
+        ));
+        assert!(matches!(
+            arrow_value_at(&TimestampMillisecondArray::from(vec![1i64]), 
0).unwrap(),
+            Value::Timestamp(_)
+        ));
+        assert!(matches!(
+            arrow_value_at(&TimestampNanosecondArray::from(vec![1000i64]), 
0).unwrap(),
+            Value::Timestamp(_)
+        ));
+        assert!(matches!(
+            arrow_value_at(&Date64Array::from(vec![86_400_000i64]), 
0).unwrap(),
+            Value::Date(_)
+        ));
+    }
+
+    #[test]
+    fn time_types_render_as_string() {
+        assert!(matches!(
+            arrow_value_at(&Time64MicrosecondArray::from(vec![1i64]), 
0).unwrap(),
+            Value::String(_)
+        ));
+        assert!(matches!(
+            arrow_value_at(&Time64NanosecondArray::from(vec![1000i64]), 
0).unwrap(),
+            Value::String(_)
+        ));
+        assert!(matches!(
+            arrow_value_at(&Time32MillisecondArray::from(vec![1i32]), 
0).unwrap(),
+            Value::String(_)
+        ));
+        assert!(matches!(
+            arrow_value_at(&Time32SecondArray::from(vec![1i32]), 0).unwrap(),
+            Value::String(_)
+        ));
+    }
+
+    #[test]
+    fn interval_types_render_as_string() {
+        assert!(matches!(
+            arrow_value_at(&IntervalYearMonthArray::from(vec![13i32]), 
0).unwrap(),
+            Value::String(_)
+        ));
+        let dt = IntervalDayTimeArray::from(vec![IntervalDayTime::new(1, 
100)]);
+        assert!(matches!(arrow_value_at(&dt, 0).unwrap(), Value::String(_)));
+        let mdn = 
IntervalMonthDayNanoArray::from(vec![IntervalMonthDayNano::new(1, 2, 3)]);
+        assert!(matches!(arrow_value_at(&mdn, 0).unwrap(), Value::String(_)));
+    }
+
+    #[test]
+    fn fixed_size_binary() {
+        let arr = FixedSizeBinaryArray::try_from_iter(vec![vec![1u8, 
2u8]].into_iter()).unwrap();
+        assert!(matches!(arrow_value_at(&arr, 0).unwrap(), Value::Binary(_)));
+    }
+
+    #[test]
+    fn nested_list_struct_map() {
+        let list =
+            ListArray::from_iter_primitive::<Int32Type, _, 
_>(vec![Some(vec![Some(1), Some(2)])]);
+        assert!(matches!(arrow_value_at(&list, 0).unwrap(), Value::List(_)));
+
+        let field = Arc::new(Field::new("a", ArrowDataType::Int32, false));
+        let col: ArrayRef = Arc::new(Int32Array::from(vec![1]));
+        let s = StructArray::from(vec![(field, col)]);
+        assert!(matches!(arrow_value_at(&s, 0).unwrap(), Value::Struct(_)));
+
+        let mut b = MapBuilder::new(None, StringBuilder::new(), 
Int32Builder::new());
+        b.keys().append_value("k");
+        b.values().append_value(1);
+        b.append(true).unwrap();
+        let m = b.finish();
+        assert!(matches!(arrow_value_at(&m, 0).unwrap(), Value::Map(_)));
+    }
+
+    #[test]
+    fn null_element_and_unsupported_type() {
+        let with_null = Int32Array::from(vec![None as Option<i32>]);
+        assert!(matches!(
+            arrow_value_at(&with_null, 0).unwrap(),
+            Value::Null
+        ));
+        // Duration has no dedicated Value arm -> the final unsupported-type 
Err.
+        let dur = DurationSecondArray::from(vec![1i64]);
+        assert!(arrow_value_at(&dur, 0).is_err());
+    }
+
+    #[test]
+    fn map_key_to_string_covers_scalar_arms() {
+        assert_eq!(map_key_to_string(Value::String("x".to_string())), "x");
+        assert_eq!(map_key_to_string(Value::Bool(true)), "true");
+        assert_eq!(map_key_to_string(Value::Byte(1)), "1");
+        assert_eq!(map_key_to_string(Value::Short(2)), "2");
+        assert_eq!(map_key_to_string(Value::Integer(3)), "3");
+        assert_eq!(map_key_to_string(Value::Long(4)), "4");
+        assert_eq!(map_key_to_string(Value::Float(1.5)), "1.5");
+        assert_eq!(map_key_to_string(Value::Double(2.5)), "2.5");
+        assert_eq!(map_key_to_string(Value::Date(5)), "5");
+        assert_eq!(map_key_to_string(Value::Timestamp(6)), "6");
+        assert_eq!(
+            map_key_to_string(Value::Decimal {
+                value: "7.5".to_string(),
+                precision: None,
+                scale: None,
+            }),
+            "7.5"
+        );
+        // Non-scalar key falls back to the Debug form.
+        let _ = map_key_to_string(Value::List(vec![]));
+    }
+
+    #[test]
+    fn i128_to_decimal_string_branches() {
+        assert_eq!(i128_to_decimal_string(12345, 0), "12345");
+        assert_eq!(i128_to_decimal_string(12345, 2), "123.45");
+        assert_eq!(i128_to_decimal_string(5, 4), "0.0005");
+        assert_eq!(i128_to_decimal_string(-5, 4), "-0.0005");
+    }
+
+    #[test]
+    fn micros_to_time_string_branches() {
+        assert_eq!(micros_to_time_string(0), "00:00:00");
+        assert!(micros_to_time_string(1).contains('.'));
+    }
+}
diff --git a/crates/spark-connect/src/observation.rs 
b/crates/spark-connect/src/observation.rs
index 763ba1d..0d7aad4 100644
--- a/crates/spark-connect/src/observation.rs
+++ b/crates/spark-connect/src/observation.rs
@@ -55,4 +55,22 @@ mod tests {
         assert_eq!(obs.name(), "test_obs");
         assert!(obs.get().is_empty());
     }
+
+    #[test]
+    fn set_metrics_and_dataframe() {
+        let mut obs = Observation::new("o");
+        let mut m = HashMap::new();
+        m.insert("k".to_string(), "v".to_string());
+        obs.set_metrics(m);
+        assert_eq!(obs.get().get("k").map(|s| s.as_str()), Some("v"));
+
+        // set_dataframe just stores the handle; a session-less plan is enough
+        // (no RPC is made here).
+        let spark = crate::session::SparkSession::builder()
+            .remote("sc://localhost:15002")
+            .get_or_create()
+            .expect("session");
+        let df = spark.range(3).unwrap();
+        obs.set_dataframe(df);
+    }
 }
diff --git a/crates/spark-connect/src/profiler.rs 
b/crates/spark-connect/src/profiler.rs
index df84f4b..9c3b6f0 100644
--- a/crates/spark-connect/src/profiler.rs
+++ b/crates/spark-connect/src/profiler.rs
@@ -300,4 +300,134 @@ mod tests {
         assert!(result.contains("udf_time_ms: 150"));
         assert!(result.contains("python_calls: 42"));
     }
+
+    #[test]
+    fn format_literal_covers_all_variants() {
+        use proto::expression::literal::{self, LiteralType};
+        let lit = |lt: LiteralType| proto::expression::Literal {
+            literal_type: Some(lt),
+            data_type: None,
+        };
+        assert_eq!(
+            
format_literal(&lit(LiteralType::Null(proto::DataType::default()))),
+            "null"
+        );
+        assert_eq!(format_literal(&lit(LiteralType::Boolean(true))), "true");
+        assert_eq!(format_literal(&lit(LiteralType::Byte(1))), "1");
+        assert_eq!(format_literal(&lit(LiteralType::Short(2))), "2");
+        assert_eq!(format_literal(&lit(LiteralType::Integer(3))), "3");
+        assert_eq!(format_literal(&lit(LiteralType::Long(4))), "4");
+        assert_eq!(format_literal(&lit(LiteralType::Float(1.5))), "1.5");
+        assert_eq!(format_literal(&lit(LiteralType::Double(2.5))), "2.5");
+        assert_eq!(
+            
format_literal(&lit(LiteralType::Decimal(literal::Decimal::default()))),
+            "decimal"
+        );
+        assert_eq!(
+            format_literal(&lit(LiteralType::String("hi".to_string()))),
+            "hi"
+        );
+        assert!(
+            format_literal(&lit(LiteralType::Binary(vec![1u8, 2, 
3].into()))).contains("3 bytes")
+        );
+        assert_eq!(
+            format_literal(&lit(LiteralType::CalendarInterval(
+                literal::CalendarInterval::default()
+            ))),
+            "calendar_interval"
+        );
+        assert_eq!(
+            format_literal(&lit(LiteralType::YearMonthInterval(0))),
+            "year_month_interval"
+        );
+        assert_eq!(
+            format_literal(&lit(LiteralType::DayTimeInterval(0))),
+            "day_time_interval"
+        );
+        assert_eq!(format_literal(&lit(LiteralType::Date(0))), "date");
+        assert_eq!(format_literal(&lit(LiteralType::Timestamp(0))), 
"timestamp");
+        assert_eq!(
+            format_literal(&lit(LiteralType::TimestampNtz(0))),
+            "timestamp_ntz"
+        );
+        assert_eq!(
+            format_literal(&lit(LiteralType::Time(literal::Time::default()))),
+            "time"
+        );
+        assert_eq!(
+            format_literal(&lit(LiteralType::TimestampNtzNanos(
+                literal::TimestampNtzNanos::default()
+            ))),
+            "timestamp_ntz_nanos"
+        );
+        assert_eq!(
+            format_literal(&lit(LiteralType::TimestampLtzNanos(
+                literal::TimestampLtzNanos::default()
+            ))),
+            "timestamp_ltz_nanos"
+        );
+        assert_eq!(
+            format_literal(&lit(LiteralType::Map(literal::Map::default()))),
+            "map"
+        );
+        assert_eq!(
+            
format_literal(&lit(LiteralType::Array(literal::Array::default()))),
+            "array"
+        );
+        assert_eq!(
+            
format_literal(&lit(LiteralType::Struct(literal::Struct::default()))),
+            "struct"
+        );
+        assert_eq!(
+            format_literal(&lit(LiteralType::SpecializedArray(
+                literal::SpecializedArray::default()
+            ))),
+            "specialized_array"
+        );
+        // literal_type == None falls through to "unknown".
+        assert_eq!(
+            format_literal(&proto::expression::Literal {
+                literal_type: None,
+                data_type: None,
+            }),
+            "unknown"
+        );
+    }
+
+    #[test]
+    fn accumulate_observed_metrics_more_values_than_keys() {
+        // values longer than keys hits the `value_{i}` fallback key branch.
+        let collector = ProfilerCollector::new();
+        let mut metric = 
proto::execute_plan_response::ObservedMetrics::default();
+        metric.name = "m".to_string();
+        metric.plan_id = 7;
+        metric.keys = vec!["a".to_string()];
+        metric.values.push(proto::expression::Literal {
+            literal_type: 
Some(proto::expression::literal::LiteralType::Long(1)),
+            data_type: None,
+        });
+        metric.values.push(proto::expression::Literal {
+            literal_type: 
Some(proto::expression::literal::LiteralType::Long(2)),
+            data_type: None,
+        });
+        collector.accumulate_observed_metrics(&[metric]);
+        let shown = collector.show(Some(7));
+        assert!(shown.contains("a: 1"));
+        assert!(shown.contains("value_1: 2"));
+    }
+
+    #[test]
+    fn dump_to_unwritable_path_errors() {
+        let collector = ProfilerCollector::new();
+        collector.accumulate_profile(1, "data".to_string(), HashMap::new());
+        // A path under a non-existent directory cannot be written -> Err 
branch.
+        let res = collector.dump(Some(1), 
"/nonexistent_dir_zzz_12345/profile.txt");
+        assert!(res.is_err());
+    }
+
+    #[test]
+    fn default_constructs_empty_collector() {
+        let collector = ProfilerCollector::default();
+        assert_eq!(collector.count(), 0);
+    }
 }
diff --git a/crates/spark-connect/src/resource.rs 
b/crates/spark-connect/src/resource.rs
index 135d1ed..385fd74 100644
--- a/crates/spark-connect/src/resource.rs
+++ b/crates/spark-connect/src/resource.rs
@@ -277,4 +277,20 @@ mod tests {
         let task_cpus = 
profile.proto_profile.task_resources.get("cpus").unwrap();
         assert_eq!(task_cpus.amount, 1.0);
     }
+
+    #[test]
+    fn off_heap_memory_and_profile_accessors() {
+        let reqs = ExecutorResourceRequests::new()
+            .off_heap_memory(1024)
+            .cores(2);
+        assert_eq!(reqs.resources.get("offHeap").unwrap().amount, 1024);
+
+        let profile = ResourceProfileBuilder::new()
+            .executor_resources(reqs)
+            .build();
+        // id() is None before the profile is registered with the server.
+        assert!(profile.id().is_none());
+        // proto() exposes the underlying proto with both resource entries.
+        assert_eq!(profile.proto().executor_resources.len(), 2);
+    }
 }
diff --git a/crates/spark-connect/tests/coverage_column_streaming_extra.rs 
b/crates/spark-connect/tests/coverage_column_streaming_extra.rs
new file mode 100644
index 0000000..b8d894d
--- /dev/null
+++ b/crates/spark-connect/tests/coverage_column_streaming_extra.rs
@@ -0,0 +1,82 @@
+//! Pure-logic (no-server) coverage for `column.rs` corners not exercised 
elsewhere:
+//! `alias_with_metadata`, `try_cast`, the non-`CaseWhen` branch of 
`otherwise`, the
+//! window-frame boundary mapping in `over` (all `FrameBound` variants), the 
operator
+//! trait impls (`&`, `|`, `!`), and the `lit` LongType branch for out-of-i32 
values.
+//!
+//! (The `streaming.rs` `foreach`/`foreach_batch` builders need a 
`DataStreamWriter`,
+//! which requires a live session/DataFrame, so they are covered by the e2e 
streaming
+//! tests, not here.)
+
+use std::collections::BTreeMap;
+
+use spark_connect::column::{col, lit, lit_string};
+use spark_connect::types::DataType;
+use spark_connect::window::{FrameBound, WindowSpec};
+
+#[test]
+fn alias_with_metadata_serializes_map() {
+    let mut meta = BTreeMap::new();
+    meta.insert("comment".to_string(), "hello".to_string());
+    meta.insert("unit".to_string(), "ms".to_string());
+    let c = col("x").alias_with_metadata("y", meta);
+    // Build the proto to exercise the alias-with-metadata path end to end.
+    let _ = c.to_proto();
+
+    // Empty metadata also goes through the serializer.
+    let c2 = col("z").alias_with_metadata("w", BTreeMap::new());
+    let _ = c2.to_proto();
+}
+
+#[test]
+fn try_cast_builds_cast_expr() {
+    let c = col("x").try_cast(DataType::Integer);
+    let _ = c.to_proto();
+    let c2 = col("x").try_cast_str("string");
+    let _ = c2.to_proto();
+}
+
+#[test]
+fn otherwise_on_non_casewhen_keeps_expr() {
+    // `col("x")` is a plain column reference, not a CaseWhen, so `otherwise` 
takes the
+    // fall-through branch that returns the receiver's expression unchanged.
+    let c = col("x").otherwise(lit(1));
+    let _ = c.to_proto();
+}
+
+#[test]
+fn over_maps_all_frame_boundaries() {
+    // Three windows together hit every FrameBound arm for both the lower and 
upper
+    // boundary in `Column::over`'s frame-spec mapping.
+    let w1 = WindowSpec::new().rows_between(
+        FrameBound::UnboundedPreceding,
+        FrameBound::UnboundedFollowing,
+    );
+    let _ = col("v").over(w1).to_proto();
+
+    let w2 = WindowSpec::new().rows_between(FrameBound::Preceding(1), 
FrameBound::Following(2));
+    let _ = col("v").over(w2).to_proto();
+
+    let w3 = WindowSpec::new().range_between(FrameBound::CurrentRow, 
FrameBound::CurrentRow);
+    let _ = col("v").over(w3).to_proto();
+}
+
+#[test]
+fn column_operator_traits() {
+    // BitAnd / BitOr / Not operator impls delegate to and()/or()/not.
+    let a = col("a");
+    let b = col("b");
+    let _ = (a.clone() & b.clone()).to_proto();
+    let _ = (a.clone() | b.clone()).to_proto();
+    let _ = (!a).to_proto();
+}
+
+#[test]
+fn lit_long_branch_for_large_values() {
+    // Values outside i32 range take the LongType branch of `lit`.
+    let big = lit(10_000_000_000_i64);
+    let _ = big.to_proto();
+    // Small value takes the IntegerType branch (sanity).
+    let small = lit(7);
+    let _ = small.to_proto();
+    let _ = lit_string("hello").to_proto();
+}
diff --git a/crates/spark-connect/tests/coverage_expression_extra.rs 
b/crates/spark-connect/tests/coverage_expression_extra.rs
new file mode 100644
index 0000000..2e3cdc1
--- /dev/null
+++ b/crates/spark-connect/tests/coverage_expression_extra.rs
@@ -0,0 +1,148 @@
+//! Pure-logic coverage of `expression.rs`: exercises the `to_proto` arms and 
builder
+//! methods that the behavioral (server-gated) column tests do not reach in a 
server-less
+//! coverage run — every `LiteralExpression` variant, the inline-UDF 
expression arm,
+//! `ColumnReference` plan-id/metadata, `Alias` metadata, `Cast` eval modes, 
`CaseWhen`
+//! else, and window frame boundaries. No server required.
+
+use spark_connect::expression::{
+    Alias, CaseWhen, Cast, CastEvalMode, ColumnReference, Expression, 
FrameBoundary,
+    LiteralExpression, SortOrder, UnresolvedFunction, WindowExpressionWrapper,
+};
+use spark_connect::types::DataType;
+use spark_connect::udf::{eval_type, CommonInlineUserDefinedFunctionExpression, 
PythonUDFPayload};
+
+fn col_expr(name: &str) -> Expression {
+    Expression::ColumnReference(ColumnReference::new(name))
+}
+
+#[test]
+fn every_literal_variant_to_proto() {
+    let lits = vec![
+        LiteralExpression::null(DataType::Integer),
+        LiteralExpression::boolean(true),
+        LiteralExpression::Byte(1),
+        LiteralExpression::Short(2),
+        LiteralExpression::int(3),
+        LiteralExpression::long(4),
+        LiteralExpression::Float(1.5),
+        LiteralExpression::double(2.5),
+        LiteralExpression::Decimal {
+            value: "12.34".to_string(),
+            precision: 4,
+            scale: 2,
+        },
+        LiteralExpression::string("hi"),
+        LiteralExpression::binary(vec![1, 2, 3]),
+        LiteralExpression::Date(19000),
+        LiteralExpression::Timestamp(1_693_526_400_000_000),
+        LiteralExpression::TimestampNtz(1_693_526_400_000_000),
+        LiteralExpression::Time {
+            nano: 123,
+            precision: 6,
+        },
+        LiteralExpression::Array {
+            element_type: Box::new(DataType::Integer),
+            elements: vec![LiteralExpression::int(1), 
LiteralExpression::int(2)],
+        },
+    ];
+    for lit in lits {
+        assert!(lit.to_proto().expr_type.is_some());
+    }
+}
+
+#[test]
+fn inline_udf_expression_to_proto() {
+    let payload = PythonUDFPayload::new(
+        DataType::Integer,
+        eval_type::SQL_BATCHED_UDF,
+        vec![1, 2, 3],
+        "3.11".to_string(),
+    );
+    let udf = CommonInlineUserDefinedFunctionExpression::new(
+        "my_udf".to_string(),
+        true,
+        vec![col_expr("x")],
+        payload,
+    );
+    let expr = Expression::CommonInlineUserDefinedFunction(Box::new(udf));
+    assert!(expr.to_proto().expr_type.is_some());
+}
+
+#[test]
+fn column_reference_plan_id_and_metadata() {
+    let bound = ColumnReference::new("a").with_plan_id(7);
+    assert_eq!(bound.plan_id, Some(7));
+    assert!(bound.to_proto().expr_type.is_some());
+
+    let meta = ColumnReference::new("b").metadata();
+    assert!(meta.is_metadata_column);
+    assert!(meta.to_proto().expr_type.is_some());
+}
+
+#[test]
+fn alias_with_metadata_to_proto() {
+    let alias = Alias::new(col_expr("x"), 
"y").with_metadata("{\"comment\":\"c\"}".to_string());
+    assert_eq!(alias.metadata, Some("{\"comment\":\"c\"}".to_string()));
+    assert!(alias.to_proto().expr_type.is_some());
+}
+
+#[test]
+fn cast_eval_modes_and_type_str() {
+    for mode in [CastEvalMode::Legacy, CastEvalMode::Ansi, CastEvalMode::Try] {
+        let cast = Cast::new(col_expr("x"), 
DataType::Integer).with_eval_mode(mode);
+        assert!(cast.to_proto().expr_type.is_some());
+    }
+    let cast_str = Cast::new_str(col_expr("x"), "string");
+    assert!(cast_str.to_proto().expr_type.is_some());
+}
+
+#[test]
+fn case_when_with_else_to_proto() {
+    let cw = CaseWhen::new(vec![(
+        col_expr("cond"),
+        Expression::Literal(LiteralExpression::int(1)),
+    )])
+    .with_else(Expression::Literal(LiteralExpression::int(0)));
+    assert!(cw.else_expr.is_some());
+    assert!(cw.to_proto().expr_type.is_some());
+}
+
+#[test]
+fn window_frame_boundaries_to_proto() {
+    let func = 
Expression::UnresolvedFunction(UnresolvedFunction::new("row_number", vec![]));
+    let order = vec![SortOrder::asc_nulls_first(col_expr("x"))];
+
+    // Preceding / Following value boundaries.
+    let w1 = WindowExpressionWrapper::new(
+        func.clone(),
+        vec![col_expr("g")],
+        order.clone(),
+        Some((1, FrameBoundary::Preceding(1), FrameBoundary::Following(2))),
+    );
+    assert!(w1.to_proto().expr_type.is_some());
+
+    // Unbounded boundaries.
+    let w2 = WindowExpressionWrapper::new(
+        func.clone(),
+        vec![],
+        order.clone(),
+        Some((
+            2,
+            FrameBoundary::UnboundedPreceding,
+            FrameBoundary::UnboundedFollowing,
+        )),
+    );
+    assert!(w2.to_proto().expr_type.is_some());
+
+    // Current-row boundary and no frame.
+    let w3 = WindowExpressionWrapper::new(
+        func.clone(),
+        vec![],
+        order,
+        Some((1, FrameBoundary::CurrentRow, FrameBoundary::CurrentRow)),
+    );
+    assert!(w3.to_proto().expr_type.is_some());
+
+    let w4 = WindowExpressionWrapper::new(func, vec![], vec![], None);
+    assert!(w4.to_proto().expr_type.is_some());
+}
diff --git a/crates/spark-connect/tests/coverage_ml_extra.rs 
b/crates/spark-connect/tests/coverage_ml_extra.rs
new file mode 100644
index 0000000..7efabf9
--- /dev/null
+++ b/crates/spark-connect/tests/coverage_ml_extra.rs
@@ -0,0 +1,221 @@
+//! Extra unit coverage for `ml.rs`.
+//!
+//! Exercises the pure-logic surface the golden tests don't reach directly:
+//! `Params`/`OperatorType`/`MlOperator` proto conversions, every ML class's
+//! builders + getters + `Default`, and the 
`Estimator`/`Transformer`/`Evaluator`
+//! trait accessors (`operator`/`operator_mut`/`params`/`params_mut`). Nothing 
here
+//! contacts a Spark server: no `fit`/`transform`/`evaluate`/`collect` is 
called, so
+//! these run in the default (server-less) `cargo test` pass.
+
+use spark_connect::ml::{
+    BinaryClassificationEvaluator, CrossValidator, Estimator, Evaluator, 
LogisticRegression,
+    LogisticRegressionModel, MaxAbsScaler, MaxAbsScalerModel, MlOperator, 
Model,
+    MulticlassClassificationEvaluator, OperatorType, Params, Pipeline, 
PipelineModel,
+    RegressionEvaluator, StandardScaler, StringIndexer, StringIndexerModel, 
Transformer,
+    VectorAssembler,
+};
+
+#[test]
+fn params_operator_type_and_ml_operator_conversions() {
+    // Params setters + get_param (Some/None) + to_proto (literal branch) + 
from_proto round-trip.
+    let p = Params::new()
+        .set_param_int("i", 7)
+        .set_param_double("d", 1.5)
+        .set_param_string("s", "v")
+        .set_param_bool("b", true);
+    assert!(p.get_param("i").is_some());
+    assert!(p.get_param("missing").is_none());
+    let mp = p.to_proto();
+    let p2 = Params::from_proto(&mp);
+    assert!(p2.get_param("s").is_some());
+
+    // OperatorType round-trips for every variant, plus the unknown-code 
fallback.
+    for ot in [
+        OperatorType::Estimator,
+        OperatorType::Transformer,
+        OperatorType::Evaluator,
+        OperatorType::Model,
+    ] {
+        let code = ot.to_proto() as i32;
+        assert_eq!(OperatorType::from_proto(code), ot);
+    }
+    assert_eq!(OperatorType::from_proto(999), OperatorType::Transformer);
+
+    // MlOperator new/with_uid/to_proto/from_proto.
+    let op = MlOperator::with_uid("n", "u", OperatorType::Model);
+    let round = MlOperator::from_proto(&op.to_proto());
+    assert_eq!(round.name, "n");
+    assert_eq!(round.uid, "u");
+    let _ = MlOperator::new("x", OperatorType::Estimator);
+}
+
+#[test]
+fn estimator_builders_and_accessors() {
+    // StandardScaler
+    let mut e = StandardScaler::new().set_input_col("f").set_output_col("o");
+    assert_eq!(e.input_col(), "f");
+    assert_eq!(e.output_col(), "o");
+    let _ = e.operator();
+    let _ = e.operator_mut();
+    let _ = e.params();
+    let _ = e.params_mut();
+    let _ = StandardScaler::default();
+
+    // StringIndexer
+    let mut e = StringIndexer::new().set_input_col("f").set_output_col("o");
+    assert_eq!(e.input_col(), "f");
+    assert_eq!(e.output_col(), "o");
+    let _ = e.operator();
+    let _ = e.operator_mut();
+    let _ = e.params();
+    let _ = e.params_mut();
+    let _ = StringIndexer::default();
+
+    // MaxAbsScaler
+    let mut e = MaxAbsScaler::new().set_input_col("f").set_output_col("o");
+    assert_eq!(e.input_col(), "f");
+    assert_eq!(e.output_col(), "o");
+    let _ = e.operator();
+    let _ = e.operator_mut();
+    let _ = e.params();
+    let _ = e.params_mut();
+    let _ = MaxAbsScaler::default();
+
+    // LogisticRegression
+    let mut e = LogisticRegression::new()
+        .set_feature_col("f")
+        .set_label_col("l")
+        .set_prediction_col("p")
+        .set_max_iter(5);
+    assert_eq!(e.feature_col(), "f");
+    assert_eq!(e.label_col(), "l");
+    assert_eq!(e.prediction_col(), "p");
+    assert_eq!(e.max_iter(), 5);
+    let _ = e.operator();
+    let _ = e.operator_mut();
+    let _ = e.params();
+    let _ = e.params_mut();
+    let _ = LogisticRegression::default();
+
+    // Pipeline
+    let mut e = Pipeline::new().set_stages(vec!["a", "b"]);
+    assert_eq!(e.stages().len(), 2);
+    let _ = e.operator();
+    let _ = e.operator_mut();
+    let _ = e.params();
+    let _ = e.params_mut();
+    let _ = Pipeline::default();
+
+    // CrossValidator
+    let mut e = CrossValidator::new()
+        .set_num_folds(4)
+        .set_parallelism(2)
+        .set_seed(42);
+    assert_eq!(e.num_folds(), 4);
+    assert_eq!(e.parallelism(), 2);
+    let _ = e.operator();
+    let _ = e.operator_mut();
+    let _ = e.params();
+    let _ = e.params_mut();
+    let _ = CrossValidator::default();
+}
+
+#[test]
+fn transformer_and_model_accessors() {
+    // VectorAssembler (a Transformer).
+    let mut t = VectorAssembler::new()
+        .set_input_cols(vec!["a", "b"])
+        .set_output_col("o");
+    assert_eq!(t.input_cols().len(), 2);
+    assert_eq!(t.output_col(), "o");
+    let _ = t.operator();
+    let _ = t.operator_mut();
+    let _ = t.params();
+    let _ = t.params_mut();
+    let _ = VectorAssembler::default();
+
+    // Models with public fields: construct directly and exercise Transformer 
+ Model methods.
+    let mut m = StringIndexerModel {
+        operator: MlOperator::new("si", OperatorType::Model),
+        params: Params::new(),
+        input_col: "a".to_string(),
+        output_col: "b".to_string(),
+    };
+    let _ = m.operator();
+    let _ = m.operator_mut();
+    let _ = m.params();
+    let _ = m.params_mut();
+    let _ = m.clone_box();
+
+    let mut m = MaxAbsScalerModel {
+        operator: MlOperator::new("ma", OperatorType::Model),
+        params: Params::new(),
+        input_col: "a".to_string(),
+        output_col: "b".to_string(),
+    };
+    let _ = m.operator();
+    let _ = m.operator_mut();
+    let _ = m.params();
+    let _ = m.params_mut();
+    let _ = m.clone_box();
+
+    let mut m = LogisticRegressionModel {
+        operator: MlOperator::new("lr", OperatorType::Model),
+        params: Params::new(),
+        feature_col: "f".to_string(),
+        label_col: "l".to_string(),
+        prediction_col: "p".to_string(),
+    };
+    let _ = m.operator();
+    let _ = m.operator_mut();
+    let _ = m.params();
+    let _ = m.params_mut();
+    let _ = m.clone_box();
+
+    let mut m = PipelineModel {
+        operator: MlOperator::new("pl", OperatorType::Model),
+        params: Params::new(),
+        stages: vec!["a".to_string()],
+    };
+    let _ = m.operator();
+    let _ = m.operator_mut();
+    let _ = m.params();
+    let _ = m.params_mut();
+    let _ = m.clone_box();
+}
+
+#[test]
+fn evaluator_builders_and_accessors() {
+    let e = RegressionEvaluator::new()
+        .set_label_col("l")
+        .set_prediction_col("p")
+        .set_metric_name("mae");
+    assert_eq!(e.label_col(), "l");
+    assert_eq!(e.prediction_col(), "p");
+    assert_eq!(e.metric_name(), "mae");
+    let _ = e.operator();
+    let _ = e.params();
+    let _ = RegressionEvaluator::default();
+
+    let e = BinaryClassificationEvaluator::new()
+        .set_label_col("l")
+        .set_score_col("s")
+        .set_metric_name("areaUnderPR");
+    assert_eq!(e.label_col(), "l");
+    assert_eq!(e.score_col(), "s");
+    assert_eq!(e.metric_name(), "areaUnderPR");
+    let _ = e.operator();
+    let _ = e.params();
+    let _ = BinaryClassificationEvaluator::default();
+
+    let e = MulticlassClassificationEvaluator::new()
+        .set_label_col("l")
+        .set_prediction_col("p")
+        .set_metric_name("accuracy");
+    assert_eq!(e.label_col(), "l");
+    assert_eq!(e.prediction_col(), "p");
+    assert_eq!(e.metric_name(), "accuracy");
+    let _ = e.operator();
+    let _ = e.params();
+    let _ = MulticlassClassificationEvaluator::default();
+}
diff --git a/crates/spark-connect/tests/coverage_plan_extra.rs 
b/crates/spark-connect/tests/coverage_plan_extra.rs
new file mode 100644
index 0000000..01b561e
--- /dev/null
+++ b/crates/spark-connect/tests/coverage_plan_extra.rs
@@ -0,0 +1,146 @@
+//! Golden/unit coverage for `plan.rs` paths not exercised by 
`plans_golden.rs`:
+//! the `plan::*` builder helpers that no golden case constructs, the
+//! `value_to_proto_literal` match arms (via `NAFill` with each `Value`), and 
the
+//! `to_proto()` arms for `LateralJoin`, `RepartitionByRange`, `Read` 
(DataSource),
+//! and `StatSampleBy`. No server: every test just builds a `LogicalPlan` and 
calls
+//! `to_proto()`, so the proto-building lines run. Assertions are light on 
purpose.
+
+use std::collections::HashMap;
+
+use spark_connect::column::lit;
+use spark_connect::plan::{self, JoinType, LogicalPlan};
+use spark_connect::readwriter::ReadType;
+use spark_connect::row::Value;
+use spark_connect::types::DataType;
+
+fn base() -> LogicalPlan {
+    plan::range(0, 10, 1)
+}
+
+/// Exercise the `plan::*` constructor helpers that `plans_golden.rs` does not 
build.
+#[test]
+fn plan_builder_helpers_to_proto() {
+    let plans = vec![
+        plan::range_with_partitions(0, 10, 1, 4),
+        plan::tail(base(), 5),
+        plan::to_schema(base(), DataType::Struct { fields: vec![] }),
+        plan::col_regex(base(), "a.*"),
+        plan::sample(base(), 0.0, 0.5, true, Some(42)),
+        // num_partitions <= 0 -> the `None` branch of RepartitionByExpression.
+        plan::repartition_by_expression(base(), 0, vec![]),
+        // num_partitions > 0 -> the `Some` branch.
+        plan::repartition_by_expression(base(), 2, vec![]),
+        plan::summary(base(), vec!["mean".to_string(), "stddev".to_string()]),
+        plan::describe(base(), vec!["id".to_string()]),
+        plan::subquery_alias(base(), "t"),
+        plan::na_drop(base(), "any", Some(2), vec!["id".to_string()]),
+        plan::na_replace(
+            base(),
+            vec![("a".to_string(), "b".to_string())],
+            vec!["id".to_string()],
+        ),
+        plan::cached_remote_relation("rid-123"),
+        plan::local_relation(DataType::Struct { fields: vec![] }, None),
+        plan::hint(base(), "COALESCE", vec!["1".to_string()]),
+    ];
+    for p in plans {
+        // Building the proto must not panic; a rel_type must be produced.
+        let proto = p.to_proto();
+        assert!(proto.rel_type.is_some());
+    }
+}
+
+/// `value_to_proto_literal` (used by `NAFill`) has one match arm per `Value`
+/// variant; drive them all through `na_fill(...).to_proto()`.
+#[test]
+fn nafill_covers_all_value_literal_arms() {
+    let values = vec![
+        Value::Bool(true),
+        Value::Byte(1),
+        Value::Short(2),
+        Value::Integer(3),
+        Value::Long(4),
+        Value::Float(1.5),
+        Value::Double(2.5),
+        Value::String("s".to_string()),
+        Value::Date(5),
+        Value::Timestamp(6),
+        Value::Decimal {
+            value: "1.23".to_string(),
+            precision: Some(5),
+            scale: Some(2),
+        },
+        // Binary/Null fall through to the catch-all `other =>` arm.
+        Value::Binary(vec![1, 2, 3]),
+        Value::Null,
+    ];
+    for v in values {
+        let proto = plan::na_fill(base(), v, vec![]).to_proto();
+        assert!(proto.rel_type.is_some());
+    }
+}
+
+/// `LateralJoin::to_proto` (join-type match + optional condition), for every 
join type.
+#[test]
+fn lateral_join_to_proto_all_join_types() {
+    for jt in [
+        JoinType::Inner,
+        JoinType::LeftOuter,
+        JoinType::RightOuter,
+        JoinType::FullOuter,
+        JoinType::LeftSemi,
+        JoinType::LeftAnti,
+        JoinType::Cross,
+    ] {
+        let p = LogicalPlan::LateralJoin {
+            left: Box::new(base()),
+            right: Box::new(base()),
+            join_type: jt,
+            on: None,
+        };
+        assert!(p.to_proto().rel_type.is_some());
+    }
+}
+
+/// `RepartitionByRange::to_proto` with an explicit partition count (the 
`Some` branch).
+#[test]
+fn repartition_by_range_to_proto() {
+    let p = LogicalPlan::RepartitionByRange {
+        input: Box::new(base()),
+        num_partitions: Some(4),
+        partition_exprs: vec![],
+    };
+    assert!(p.to_proto().rel_type.is_some());
+}
+
+/// `Read::to_proto` for the DataSource read type with 
format/schema/source_name set.
+#[test]
+fn read_datasource_to_proto() {
+    let p = LogicalPlan::Read {
+        read_type: ReadType::DataSource {
+            format: Some("parquet".to_string()),
+            schema: Some("a INT, b STRING".to_string()),
+            options: HashMap::new(),
+            paths: vec!["/tmp/data".to_string()],
+            predicates: vec![],
+            source_name: Some("src".to_string()),
+        },
+        is_streaming: false,
+    };
+    assert!(p.to_proto().rel_type.is_some());
+}
+
+/// `StatSampleBy::to_proto`: builds the col expression, fraction literals, 
and seed.
+#[test]
+fn stat_sample_by_to_proto() {
+    let p = LogicalPlan::StatSampleBy {
+        input: Box::new(base()),
+        col: "k".to_string(),
+        fractions: vec![
+            (lit(0).expression().clone(), 0.1),
+            (lit(1).expression().clone(), 0.9),
+        ],
+        seed: Some(7),
+    };
+    assert!(p.to_proto().rel_type.is_some());
+}
diff --git a/crates/spark-connect/tests/coverage_plan_map_udtf.rs 
b/crates/spark-connect/tests/coverage_plan_map_udtf.rs
new file mode 100644
index 0000000..98a1c3c
--- /dev/null
+++ b/crates/spark-connect/tests/coverage_plan_map_udtf.rs
@@ -0,0 +1,135 @@
+//! Golden coverage for the `LogicalPlan::to_proto()` map/UDTF arms that
+//! `plans_golden.rs` and `coverage_plan_extra.rs` do not build: 
`MapPartitions`,
+//! `GroupMap` (both the plain and the fully-stateful shapes), `CoGroupMap`,
+//! `NearestByJoin`, and `CommonInlineUdtf` (both with and without a return 
type).
+//! No server: each test constructs the `LogicalPlan` variant and calls 
`to_proto()`
+//! so the proto-building lines run. Assertions are intentionally light.
+
+use spark_connect::expression::{ColumnReference, Expression};
+use spark_connect::plan::{self, LogicalPlan, TransformWithStateInfo};
+use spark_connect::types::DataType;
+use spark_connect::udf::{eval_type, CommonInlineUserDefinedFunctionExpression, 
PythonUDFPayload};
+
+fn col_expr(name: &str) -> Expression {
+    Expression::ColumnReference(ColumnReference::new(name))
+}
+
+fn base() -> LogicalPlan {
+    plan::range(0, 10, 1)
+}
+
+fn udf() -> CommonInlineUserDefinedFunctionExpression {
+    let payload = PythonUDFPayload::new(
+        DataType::Integer,
+        eval_type::SQL_BATCHED_UDF,
+        vec![1, 2, 3],
+        "3.11".to_string(),
+    );
+    CommonInlineUserDefinedFunctionExpression::new(
+        "f".to_string(),
+        true,
+        vec![col_expr("x")],
+        payload,
+    )
+}
+
+#[test]
+fn map_partitions_to_proto() {
+    let p = LogicalPlan::MapPartitions {
+        input: Box::new(base()),
+        func: udf(),
+        is_barrier: true,
+    };
+    assert!(p.to_proto().rel_type.is_some());
+}
+
+#[test]
+fn group_map_to_proto_plain_and_stateful() {
+    // Plain applyInPandas shape: every optional/stateful field empty (the 
`None` branches).
+    let plain = LogicalPlan::GroupMap {
+        input: Box::new(base()),
+        grouping_expressions: vec![col_expr("g")],
+        func: udf(),
+        sorting_expressions: vec![],
+        initial_input: None,
+        initial_grouping_expressions: vec![],
+        is_map_groups_with_state: None,
+        output_mode: None,
+        timeout_conf: None,
+        state_schema: None,
+        transform_with_state_info: None,
+    };
+    assert!(plain.to_proto().rel_type.is_some());
+
+    // Fully-stateful shape: exercise the `Some(...)` branches for 
initial_input,
+    // state_schema, and transform_with_state_info.
+    let stateful = LogicalPlan::GroupMap {
+        input: Box::new(base()),
+        grouping_expressions: vec![col_expr("g")],
+        func: udf(),
+        sorting_expressions: vec![col_expr("s")],
+        initial_input: Some(Box::new(base())),
+        initial_grouping_expressions: vec![col_expr("ig")],
+        is_map_groups_with_state: Some(true),
+        output_mode: Some("append".to_string()),
+        timeout_conf: Some("NoTimeout".to_string()),
+        state_schema: Some(DataType::Struct { fields: vec![] }),
+        transform_with_state_info: Some(TransformWithStateInfo {
+            time_mode: "None".to_string(),
+            event_time_column_name: Some("ts".to_string()),
+            output_schema: Some(DataType::Struct { fields: vec![] }),
+        }),
+    };
+    assert!(stateful.to_proto().rel_type.is_some());
+}
+
+#[test]
+fn co_group_map_to_proto() {
+    let p = LogicalPlan::CoGroupMap {
+        input: Box::new(base()),
+        input_grouping_expressions: vec![col_expr("a")],
+        other: Box::new(base()),
+        other_grouping_expressions: vec![col_expr("b")],
+        func: udf(),
+    };
+    assert!(p.to_proto().rel_type.is_some());
+}
+
+#[test]
+fn nearest_by_join_to_proto() {
+    let p = LogicalPlan::NearestByJoin {
+        left: Box::new(base()),
+        right: Box::new(base()),
+        ranking_expression: col_expr("dist"),
+        num_results: 5,
+        join_type: "inner".to_string(),
+        mode: "brute_force".to_string(),
+        direction: "ascending".to_string(),
+    };
+    assert!(p.to_proto().rel_type.is_some());
+}
+
+#[test]
+fn common_inline_udtf_to_proto_with_and_without_return_type() {
+    let with_rt = LogicalPlan::CommonInlineUdtf {
+        function_name: "my_udtf".to_string(),
+        deterministic: true,
+        arguments: vec![col_expr("x")],
+        return_type: Some(DataType::Struct { fields: vec![] }),
+        eval_type: 200,
+        command: vec![1, 2, 3],
+        python_ver: "3.11".to_string(),
+    };
+    assert!(with_rt.to_proto().rel_type.is_some());
+
+    let no_rt = LogicalPlan::CommonInlineUdtf {
+        function_name: "u2".to_string(),
+        deterministic: false,
+        arguments: vec![],
+        return_type: None,
+        eval_type: 201,
+        command: vec![],
+        python_ver: "3.12".to_string(),
+    };
+    assert!(no_rt.to_proto().rel_type.is_some());
+}
diff --git a/crates/spark-connect/tests/coverage_types_extra.rs 
b/crates/spark-connect/tests/coverage_types_extra.rs
new file mode 100644
index 0000000..deb40a6
--- /dev/null
+++ b/crates/spark-connect/tests/coverage_types_extra.rs
@@ -0,0 +1,340 @@
+//! Extra pure-logic coverage for `types.rs`: the serialization branches, JSON 
and DDL
+//! parsing paths (including error closures), and proto edge cases that the 
exhaustive
+//! round-trip in `types_roundtrip.rs` does not reach. No server; runs in the 
default
+//! `cargo test` pass.
+//!
+//! These deliberately favor exercising code paths (`let _ = ...`) over 
asserting exact
+//! outputs, except where the expected value is unambiguous from the 
implementation.
+
+use std::collections::BTreeMap;
+
+use spark_connect::types::{DataType, StructField};
+use spark_connect_proto as proto;
+
+/// Variants (and branch variations) whose accessors are otherwise 
under-exercised.
+fn extra_variants() -> Vec<DataType> {
+    vec![
+        // Non-default collation: hits the "string collate <name>" branch in 
both
+        // simple_string and json_value.
+        DataType::String {
+            collation: "UTF8_LCASE".to_string(),
+        },
+        DataType::String {
+            collation: String::new(),
+        },
+        DataType::Char { length: 12 },
+        DataType::Varchar { length: 24 },
+        DataType::Time { precision: 3 },
+        DataType::Decimal {
+            precision: 20,
+            scale: 4,
+        },
+        // Geometry / Geography: both the srid == -1 ("any") and concrete-srid 
branches.
+        DataType::Geometry { srid: -1 },
+        DataType::Geometry { srid: 4326 },
+        DataType::Geography { srid: -1 },
+        DataType::Geography { srid: 4326 },
+        DataType::Variant,
+        // Interval: same start/end field, distinct fields, and out-of-range 
codes that
+        // fall through to the bare "interval" string.
+        DataType::YearMonthInterval {
+            start_field: 0,
+            end_field: 0,
+        },
+        DataType::YearMonthInterval {
+            start_field: 0,
+            end_field: 1,
+        },
+        DataType::YearMonthInterval {
+            start_field: 99,
+            end_field: 99,
+        },
+        DataType::DayTimeInterval {
+            start_field: 0,
+            end_field: 3,
+        },
+        DataType::DayTimeInterval {
+            start_field: 42,
+            end_field: 42,
+        },
+        // UDT with every optional field populated (exercises each `if let 
Some` insert).
+        DataType::Udt {
+            type_str: "udt".to_string(),
+            jvm_class: Some("com.example.MyUdt".to_string()),
+            python_class: Some("mymod.MyUdt".to_string()),
+            serialized_python_class: Some("payload".to_string()),
+            sql_type: Some(Box::new(DataType::Integer)),
+        },
+        DataType::Unparsed {
+            data_type_string: "some_custom".to_string(),
+        },
+        // Nested complex types, so the recursive accessor arms run.
+        DataType::Array {
+            element_type: Box::new(DataType::Map {
+                key_type: Box::new(DataType::String {
+                    collation: "UTF8_BINARY".to_string(),
+                }),
+                value_type: Box::new(DataType::Array {
+                    element_type: Box::new(DataType::Integer),
+                    contains_null: false,
+                }),
+                value_contains_null: true,
+            }),
+            contains_null: true,
+        },
+        DataType::Struct {
+            fields: vec![
+                StructField {
+                    name: "a".to_string(),
+                    data_type: DataType::Decimal {
+                        precision: 10,
+                        scale: 2,
+                    },
+                    nullable: true,
+                    metadata: BTreeMap::new(),
+                },
+                StructField {
+                    name: "b".to_string(),
+                    data_type: DataType::Char { length: 4 },
+                    nullable: false,
+                    metadata: {
+                        let mut m = BTreeMap::new();
+                        m.insert("k".to_string(), "v".to_string());
+                        m
+                    },
+                },
+            ],
+        },
+    ]
+}
+
+#[test]
+fn accessors_for_extra_variants() {
+    for dt in extra_variants() {
+        // Exercise every accessor arm; correctness of the exact string is 
asserted
+        // separately below only where unambiguous.
+        let _ = dt.type_name();
+        let _ = dt.simple_string();
+        let _ = dt.json_value();
+        let _ = dt.json();
+        let _ = dt.need_conversion();
+        let _ = format!("{}", dt); // Display -> simple_string
+        let proto = dt.to_proto();
+        // proto round-trips back to an equivalent DataType for these variants.
+        let _ = DataType::from_proto(&proto);
+        // json round-trips through from_json for the string-encoded variants; 
ignore the
+        // result (some encodings intentionally do not round-trip).
+        let _ = DataType::from_json(&dt.json_value());
+    }
+}
+
+#[test]
+fn string_collation_branches() {
+    let lcase = DataType::String {
+        collation: "UTF8_LCASE".to_string(),
+    };
+    assert_eq!(lcase.simple_string(), "string collate UTF8_LCASE");
+    assert_eq!(
+        lcase.json_value(),
+        serde_json::json!("string collate UTF8_LCASE")
+    );
+
+    // Default collation renders as plain "string" (no trailing "collate").
+    let def = DataType::String {
+        collation: "UTF8_BINARY".to_string(),
+    };
+    assert_eq!(def.simple_string(), "string");
+}
+
+#[test]
+fn interval_string_fallback_and_ranges() {
+    // Out-of-range field codes fall through to the bare "interval".
+    let bad = DataType::YearMonthInterval {
+        start_field: 99,
+        end_field: 99,
+    };
+    assert_eq!(bad.simple_string(), "interval");
+
+    // Same field -> "interval <field>"; distinct -> "interval <a> to <b>".
+    let ym_same = DataType::YearMonthInterval {
+        start_field: 0,
+        end_field: 0,
+    };
+    assert_eq!(ym_same.simple_string(), "interval year");
+    let dt_range = DataType::DayTimeInterval {
+        start_field: 0,
+        end_field: 3,
+    };
+    assert_eq!(dt_range.simple_string(), "interval day to second");
+}
+
+#[test]
+fn from_ddl_error_paths() {
+    // Each malformed argument drives the map_err / ok_or_else closure inside 
the
+    // corresponding parse_* helper.
+    for bad in [
+        "char(abc)",
+        "char(3",
+        "varchar(xyz)",
+        "time(nope)",
+        "decimal(x)",
+        "decimal(10,y)",
+        "geometry(notanumber)",
+        "geography(bad)",
+    ] {
+        assert!(
+            DataType::from_ddl(bad).is_err(),
+            "expected from_ddl({bad}) to be Err"
+        );
+    }
+}
+
+#[test]
+fn from_ddl_less_common_valid_forms() {
+    for good in [
+        "geometry(4326)",
+        "geometry(any)",
+        "geography(4326)",
+        "geography(any)",
+        "time(6)",
+        "interval day to second",
+        "interval hour to minute",
+        "string collate UTF8_LCASE",
+        "array<map<string,int>>",
+        "map<string,array<int>>",
+        "struct<a:decimal(10,2),b:char(4)>",
+    ] {
+        // Exercise the parser branch; a few exotic forms may Err on this 
server-less
+        // path, which is fine — the point is to run the code.
+        let _ = DataType::from_ddl(good);
+    }
+    // Concrete expectations for the geometry/geography DDL parser.
+    assert_eq!(
+        DataType::from_ddl("geometry(any)").unwrap(),
+        DataType::Geometry { srid: -1 }
+    );
+    assert!(matches!(
+        DataType::from_ddl("array<map<string,int>>").unwrap(),
+        DataType::Array { .. }
+    ));
+}
+
+#[test]
+fn from_json_string_forms() {
+    for s in [
+        "void",
+        "variant",
+        "string collate UTF8_LCASE",
+        "unparsed(mytype)",
+        "geometry(4326)",
+        "geography(any)",
+        "interval day to second",
+        "char(5)",
+        "varchar(9)",
+        "time(3)",
+        "decimal(10,2)",
+    ] {
+        let _ = DataType::from_json(&serde_json::json!(s));
+    }
+    assert_eq!(
+        DataType::from_json(&serde_json::json!("void")).unwrap(),
+        DataType::Null
+    );
+    assert_eq!(
+        DataType::from_json(&serde_json::json!("variant")).unwrap(),
+        DataType::Variant
+    );
+}
+
+#[test]
+fn from_json_object_forms() {
+    let array = serde_json::json!({
+        "type": "array",
+        "elementType": "integer",
+        "containsNull": true,
+    });
+    assert!(matches!(
+        DataType::from_json(&array).unwrap(),
+        DataType::Array { .. }
+    ));
+
+    let map = serde_json::json!({
+        "type": "map",
+        "keyType": "string",
+        "valueType": "integer",
+        "valueContainsNull": false,
+    });
+    assert!(matches!(
+        DataType::from_json(&map).unwrap(),
+        DataType::Map { .. }
+    ));
+
+    let strct = serde_json::json!({
+        "type": "struct",
+        "fields": [
+            {"name": "a", "type": "integer", "nullable": true, "metadata": {}},
+            {"name": "b", "type": {"type": "array", "elementType": "string", 
"containsNull": true}, "nullable": false, "metadata": {}},
+        ],
+    });
+    assert!(matches!(
+        DataType::from_json(&strct).unwrap(),
+        DataType::Struct { .. }
+    ));
+}
+
+#[test]
+fn from_json_error_paths() {
+    // Non-string / non-object JSON value.
+    assert!(DataType::from_json(&serde_json::json!(123)).is_err());
+    // Unknown type string.
+    assert!(DataType::from_json(&serde_json::json!("no_such_type")).is_err());
+    // Array missing elementType.
+    assert!(DataType::from_json(&serde_json::json!({"type": 
"array"})).is_err());
+    // Map missing keyType / valueType.
+    assert!(DataType::from_json(&serde_json::json!({"type": "map", "keyType": 
"string"})).is_err());
+    // Struct missing fields.
+    assert!(DataType::from_json(&serde_json::json!({"type": 
"struct"})).is_err());
+    // Struct field missing name.
+    assert!(DataType::from_json(&serde_json::json!({
+        "type": "struct",
+        "fields": [{"type": "integer"}],
+    }))
+    .is_err());
+    // Struct field missing type.
+    assert!(DataType::from_json(&serde_json::json!({
+        "type": "struct",
+        "fields": [{"name": "a"}],
+    }))
+    .is_err());
+}
+
+#[test]
+fn from_proto_kind_not_set_is_error() {
+    // A default proto DataType has no `kind`, driving the `None => Err(...)` 
arm.
+    let empty = proto::DataType::default();
+    assert!(DataType::from_proto(&empty).is_err());
+}
+
+#[test]
+fn struct_helpers_and_non_struct_errors() {
+    let s = DataType::Struct {
+        fields: vec![StructField {
+            name: "x".to_string(),
+            data_type: DataType::Integer,
+            nullable: true,
+            metadata: BTreeMap::new(),
+        }],
+    };
+    assert_eq!(s.field_names().unwrap(), vec!["x".to_string()]);
+    assert_eq!(s.names().unwrap(), vec!["x".to_string()]);
+    let added = s
+        .add("y", DataType::Boolean, false, None)
+        .expect("add on struct");
+    assert_eq!(added.field_names().unwrap().len(), 2);
+
+    // The same accessors on a non-struct must Err rather than panic.
+    let not_struct = DataType::Integer;
+    assert!(not_struct.field_names().is_err());
+    assert!(not_struct.names().is_err());
+    assert!(not_struct.add("z", DataType::Long, true, None).is_err());
+}
diff --git a/crates/spark-connect/tests/e2e_ops_breadth.rs 
b/crates/spark-connect/tests/e2e_ops_breadth.rs
new file mode 100644
index 0000000..f471538
--- /dev/null
+++ b/crates/spark-connect/tests/e2e_ops_breadth.rs
@@ -0,0 +1,257 @@
+//! Server-gated breadth coverage: build DataFrames with (nearly) every 
operation and
+//! execute them, so the per-operation execution methods and the recursive
+//! `assign_plan_ids` match over `RelType` variants in `dataframe.rs` actually 
run.
+//! Correctness is asserted elsewhere; here the point is to exercise the 
reachable
+//! execution surface, so most calls are resilient (`let _ = ...`) and a few 
safe ones
+//! are asserted. Run with SPARK_REMOTE set.
+
+use spark_connect::column::{col, lit};
+use spark_connect::dataframe::DataFrame;
+use spark_connect::functions as f;
+use spark_connect::plan::JoinType;
+use spark_connect::session::SparkSession;
+use spark_connect::types::DataType;
+
+fn should_run() -> bool {
+    std::env::var("SPARK_REMOTE").is_ok()
+}
+
+fn session() -> SparkSession {
+    let url = std::env::var("SPARK_REMOTE").unwrap_or_else(|_| 
"sc://localhost:15002".to_string());
+    SparkSession::builder()
+        .remote(&url)
+        .get_or_create()
+        .expect("session")
+}
+
+// (id, name, v) with a duplicate row for dedup/set-op paths.
+fn base(s: &SparkSession) -> DataFrame {
+    s.sql("SELECT * FROM VALUES 
(1,'a',1.0),(2,'b',2.0),(2,'b',2.0),(3,'c',3.0) AS t(id, name, v)")
+        .expect("base df")
+}
+
+fn expr(c: spark_connect::column::Column) -> 
spark_connect::expression::Expression {
+    c.expression().clone()
+}
+
+/// Relational transforms: project, filter, joins, set ops, aggregates, sort, 
slicing,
+/// dedup, repartition, sample, reshape. Each is collected so 
`assign_plan_ids` walks
+/// the corresponding `RelType` arm.
+#[test]
+fn transforms_execute() {
+    if !should_run() {
+        return;
+    }
+    let s = session();
+    let df = base(&s);
+
+    // Project / filter / column ops.
+    assert_eq!(df.select(vec![col("id")]).count().unwrap(), 4);
+    let _ = df.select_expr(vec!["id + 1 AS x"]).collect().unwrap();
+    let _ = df.filter(col("id").gt(lit(1))).collect().unwrap();
+    let _ = df.where_(col("id").gt(lit(1))).collect().unwrap();
+    let _ = df.with_column("y", lit(1)).collect().unwrap();
+    let _ = df
+        .with_columns(vec![("p".to_string(), lit(1)), ("q".to_string(), 
lit(2))])
+        .collect()
+        .unwrap();
+    let _ = df.with_column_renamed("name", "nm").collect().unwrap();
+    let _ = df
+        .with_columns_renamed(vec![("v".to_string(), "val".to_string())])
+        .collect()
+        .unwrap();
+    let _ = df.drop(vec!["name"]).collect().unwrap();
+
+    // Slicing / dedup / distinct.
+    let _ = df.limit(2).collect().unwrap();
+    let _ = df.offset(1).collect().unwrap();
+    let _ = df.tail(2).collect().unwrap();
+    let _ = df.distinct().collect().unwrap();
+    let _ = df.drop_duplicates(None).collect().unwrap();
+    let _ = df.drop_duplicates(Some(vec!["id"])).collect().unwrap();
+
+    // Sort variants.
+    let _ = df.sort(vec![expr(col("id"))]).collect().unwrap();
+    let _ = df.order_by(vec![expr(col("id"))]).collect().unwrap();
+    let _ = df
+        .sort_within_partitions(vec![expr(col("id"))])
+        .collect()
+        .unwrap();
+
+    // Repartition variants / hint / broadcast / to_df / alias.
+    let _ = df.repartition(3).collect().unwrap();
+    let _ = df.coalesce(1).collect().unwrap();
+    let _ = df
+        .repartition_by_range(2, vec![expr(col("id"))])
+        .collect()
+        .unwrap();
+    let _ = df.repartition_by_id(2).collect().unwrap();
+    let _ = df.hint("broadcast", vec![]).collect().unwrap();
+    let _ = df.broadcast().collect().unwrap();
+    let _ = df.to_df(vec!["a", "b", "c"]).collect().unwrap();
+    let _ = df.alias("t2").collect().unwrap();
+
+    // Sample.
+    let _ = df.sample(0.9, Some(7)).collect().unwrap();
+}
+
+/// Joins (multiple kinds) and set operations.
+#[test]
+fn joins_and_setops_execute() {
+    if !should_run() {
+        return;
+    }
+    let s = session();
+    let df = base(&s);
+    let other = s
+        .sql("SELECT * FROM VALUES (2,'b',2.0),(9,'z',9.0) AS t(id, name, v)")
+        .unwrap();
+    let right = s
+        .sql("SELECT * FROM VALUES (1,10),(2,20) AS t(rid, w)")
+        .unwrap();
+
+    // Join kinds.
+    for jt in [
+        JoinType::Inner,
+        JoinType::LeftOuter,
+        JoinType::RightOuter,
+        JoinType::FullOuter,
+        JoinType::LeftSemi,
+        JoinType::LeftAnti,
+    ] {
+        let _ = df
+            .join(&right, Some(col("id").eq(col("rid"))), jt)
+            .collect();
+    }
+    let _ = df
+        .join_using(&other, vec!["id".to_string()], JoinType::Inner)
+        .collect()
+        .unwrap();
+    let _ = df.cross_join(&right).collect().unwrap();
+
+    // Set ops (distinct + all variants).
+    let _ = df.union(&other).collect().unwrap();
+    let _ = df.union_all(&other).collect().unwrap();
+    let _ = df.union_by_name(&other).collect().unwrap();
+    let _ = df.union_by_name_opt(&other, true).collect().unwrap();
+    let _ = df.intersect(&other).collect().unwrap();
+    let _ = df.intersect_all(&other).collect().unwrap();
+    let _ = df.subtract(&other).collect().unwrap();
+    let _ = df.except_all(&other).collect().unwrap();
+}
+
+/// Grouping / aggregation surfaces (group_by/agg/rollup/cube/pivot + 
shortcuts).
+#[test]
+fn aggregates_execute() {
+    if !should_run() {
+        return;
+    }
+    let s = session();
+    let df = base(&s);
+
+    let _ = df
+        .group_by(vec![col("id")])
+        .agg(vec![expr(f::sum(col("v")))])
+        .collect()
+        .unwrap();
+    let _ = df.group_by(vec![col("id")]).count().collect().unwrap();
+    let _ = df
+        .group_by(vec![col("id")])
+        .sum(vec!["v"])
+        .collect()
+        .unwrap();
+    let _ = df
+        .group_by(vec![col("id")])
+        .avg(vec!["v"])
+        .collect()
+        .unwrap();
+    let _ = df
+        .group_by(vec![col("id")])
+        .min(vec!["v"])
+        .collect()
+        .unwrap();
+    let _ = df
+        .group_by(vec![col("id")])
+        .max(vec!["v"])
+        .collect()
+        .unwrap();
+    let _ = df
+        .group_by(vec![col("id")])
+        .mean(vec!["v"])
+        .collect()
+        .unwrap();
+    let _ = df.rollup(vec![col("id")]).count().collect().unwrap();
+    let _ = df.cube(vec![col("id")]).count().collect().unwrap();
+    let _ = df
+        .group_by(vec![col("id")])
+        .pivot(col("name"), None)
+        .agg(vec![expr(f::sum(col("v")))])
+        .collect();
+    let _ = df.agg(vec![expr(f::sum(col("v")))]).collect().unwrap();
+    let _ = df.observe("obs", vec![expr(f::sum(col("v")))]).collect();
+}
+
+/// NA / stat / reshape / schema-shaping operations.
+#[test]
+fn na_reshape_execute() {
+    if !should_run() {
+        return;
+    }
+    let s = session();
+    let df = base(&s);
+
+    // NA functions.
+    let _ = df.fillna(0, Some(vec!["id"])).collect().unwrap();
+    let _ = df.fillna_double(0.0, Some(vec!["v"])).collect().unwrap();
+    let _ = df.fillna_string("x", Some(vec!["name"])).collect().unwrap();
+    let _ = df.dropna(Some("any"), None, None).collect().unwrap();
+    let _ = df
+        .replace(vec![("a".to_string(), "A".to_string())], Some(vec!["name"]))
+        .collect()
+        .unwrap();
+
+    // Describe / summary.
+    let _ = df.describe(vec!["id", "v"]).collect().unwrap();
+    let _ = df.summary(vec!["count", "mean"]).collect().unwrap();
+
+    // Reshape: melt / unpivot / transpose / to(schema).
+    let _ = df
+        .melt(vec!["id"], Some(vec!["v"]), "var", "val")
+        .collect()
+        .unwrap();
+    let _ = df
+        .unpivot(vec![col("id")], Some(vec![col("v")]), "var", "val")
+        .collect()
+        .unwrap();
+    let _ = df.select(vec![col("id"), col("v")]).transpose();
+    let _ = df
+        .to(DataType::from_ddl("id INT, name STRING, v DOUBLE").unwrap())
+        .collect();
+}
+
+/// Range/SQL sources and metadata/scalar accessors that trigger execution.
+#[test]
+fn sources_and_accessors_execute() {
+    if !should_run() {
+        return;
+    }
+    let s = session();
+    let df = base(&s);
+
+    assert!(s.range(5).unwrap().count().unwrap() >= 0);
+    assert!(s.range_full(0, 6, 2, None).unwrap().count().unwrap() >= 0);
+
+    let _ = df.schema().unwrap();
+    let _ = df.columns().unwrap();
+    let _ = df.dtypes().unwrap();
+    let _ = df.is_empty().unwrap();
+    let _ = df.first().unwrap();
+    let _ = df.head().unwrap();
+    let _ = df.take(2).unwrap();
+    let _ = df.to_json().unwrap();
+    let _ = df.select(vec![col("id")]).limit(1).scalar();
+    let _ = df.semantic_hash();
+    let _ = df.same_semantics(&df);
+    let _ = df.to_local_iterator(false);
+    let _ = df.collect_record_batches();
+}
diff --git a/crates/spark-connect/tests/e2e_session_catalog.rs 
b/crates/spark-connect/tests/e2e_session_catalog.rs
new file mode 100644
index 0000000..036825d
--- /dev/null
+++ b/crates/spark-connect/tests/e2e_session_catalog.rs
@@ -0,0 +1,188 @@
+//! Server-gated end-to-end coverage of the runtime (RPC) methods of 
`catalog.rs`
+//! and `session.rs`: the full Catalog surface (current/list/get/exists/create/
+//! cache/refresh across catalogs, databases, tables, columns, functions) and 
the
+//! SparkSession runtime helpers (version, table, empty_data_frame, conf 
get/set,
+//! interrupt, tags, progress handlers, new/clone session). These execute 
against a
+//! live server, covering paths a server-less run reads as uncovered.
+//!
+//! Run with: `SPARK_REMOTE=sc://localhost:15002 cargo test --test 
e2e_session_catalog`
+
+use spark_connect::session::SparkSession;
+
+fn should_run() -> bool {
+    std::env::var("SPARK_REMOTE").is_ok()
+}
+fn session() -> SparkSession {
+    let url = std::env::var("SPARK_REMOTE").unwrap_or_else(|_| 
"sc://localhost:15002".to_string());
+    SparkSession::builder()
+        .remote(&url)
+        .get_or_create()
+        .expect("session")
+}
+
+/// Exercise the whole Catalog RPC surface. Most `list_*`/`get_*` calls return 
a lazy
+/// DataFrame, so we collect them (tolerantly) to actually drive the server.
+#[test]
+fn catalog_full_surface() {
+    if !should_run() {
+        return;
+    }
+    let spark = session();
+    let cat = spark.catalog();
+
+    // Seed a temp view so the table/column lookups have something to find.
+    spark
+        .sql("SELECT 1 AS a, 'x' AS b")
+        .expect("sql")
+        .create_or_replace_temp_view("e2e_cat_v")
+        .expect("create view");
+
+    // Catalog-level.
+    let _ = cat.current_catalog();
+    let _ = cat.set_current_catalog("spark_catalog");
+    let _ = cat.list_catalogs().and_then(|d| d.collect());
+    let _ = cat
+        .list_catalogs_with_pattern(Some("*"))
+        .and_then(|d| d.collect());
+
+    // Database-level.
+    let _ = cat.current_database();
+    let _ = cat.set_current_database("default");
+    let _ = cat.list_databases().and_then(|d| d.collect());
+    let _ = cat
+        .list_databases_with_pattern(Some("*"))
+        .and_then(|d| d.collect());
+    let _ = cat.get_database("default").and_then(|d| d.collect());
+    assert!(
+        cat.database_exists("default").unwrap_or(false) || 
cat.database_exists("default").is_ok()
+    );
+    let _ = cat.database_exists("no_such_db_e2e");
+
+    // Table-level.
+    let _ = cat.list_tables().and_then(|d| d.collect());
+    let _ = cat
+        .list_tables_in_database("default")
+        .and_then(|d| d.collect());
+    let _ = cat
+        .list_tables_with_pattern(Some("default"), Some("*"))
+        .and_then(|d| d.collect());
+    let _ = cat.get_table("e2e_cat_v").and_then(|d| d.collect());
+    let _ = cat
+        .get_table_with_database("e2e_cat_v", None)
+        .and_then(|d| d.collect());
+    let _ = cat.table_exists("e2e_cat_v");
+    let _ = cat.table_exists_with_database("e2e_cat_v", None);
+    let _ = cat.table_exists("no_such_table_e2e");
+
+    // Column-level.
+    let _ = cat.list_columns("e2e_cat_v").and_then(|d| d.collect());
+    let _ = cat
+        .list_columns_with_database("e2e_cat_v", None)
+        .and_then(|d| d.collect());
+
+    // Function-level.
+    let _ = cat.list_functions().and_then(|d| d.collect());
+    let _ = cat
+        .list_functions_in_database("default")
+        .and_then(|d| d.collect());
+    let _ = cat
+        .list_functions_with_pattern(Some("default"), Some("*"))
+        .and_then(|d| d.collect());
+    let _ = cat.get_function("abs").and_then(|d| d.collect());
+    let _ = cat
+        .get_function_with_database("abs", None)
+        .and_then(|d| d.collect());
+    let _ = cat.function_exists("abs");
+    let _ = cat.function_exists_with_database("abs", None);
+
+    // Create table paths (likely error on this single-node server; tolerated 
— the
+    // proto-building body is what we're covering).
+    let _ = cat
+        .create_table("e2e_created_tbl", None, Some("parquet"), Some("desc"))
+        .and_then(|d| d.collect());
+    let _ = cat
+        .create_external_table("e2e_ext_tbl", None, Some("parquet"))
+        .and_then(|d| d.collect());
+    let _ = spark
+        .sql("DROP TABLE IF EXISTS e2e_created_tbl")
+        .and_then(|d| d.collect());
+    let _ = spark
+        .sql("DROP TABLE IF EXISTS e2e_ext_tbl")
+        .and_then(|d| d.collect());
+
+    // Cache / refresh / recover.
+    let _ = cat.cache_table("e2e_cat_v");
+    let _ = cat.is_cached("e2e_cat_v");
+    let _ = cat.uncache_table("e2e_cat_v");
+    let _ = cat.clear_cache();
+    let _ = cat.refresh_table("e2e_cat_v");
+    let _ = cat.refresh_by_path("/tmp");
+    let _ = cat.recover_partitions("e2e_cat_v");
+
+    // Drop the temp view we created (returns whether it existed).
+    let _ = cat.drop_temp_view("e2e_cat_v");
+    let _ = cat.drop_global_temp_view("no_such_global_view_e2e");
+}
+
+/// SparkSession runtime helpers: version, table, empty_data_frame, conf 
get/set,
+/// interrupt, tags, and new/clone session.
+#[test]
+fn session_runtime_helpers() {
+    if !should_run() {
+        return;
+    }
+    let spark = session();
+
+    // Metadata / basic RPCs.
+    let v = spark.version().expect("version");
+    assert!(!v.is_empty());
+    let _ = spark.empty_data_frame().and_then(|d| d.collect());
+
+    spark
+        .sql("SELECT 1 AS a")
+        .expect("sql")
+        .create_or_replace_temp_view("e2e_sess_v")
+        .expect("view");
+    let _ = spark.table("e2e_sess_v").and_then(|d| d.collect());
+
+    // Runtime conf.
+    let conf = spark.conf();
+    let _ = conf.set("spark.sql.shuffle.partitions", "7");
+    let _ = conf.get("spark.sql.shuffle.partitions");
+    let _ = conf.get_all();
+    let _ = conf.is_modifiable("spark.sql.shuffle.partitions");
+    let _ = conf.unset("spark.sql.shuffle.partitions");
+
+    // Interrupts (no active ops; should return an empty list cleanly).
+    let _ = spark.interrupt_all();
+    let _ = spark.interrupt_tag("no_such_tag_e2e");
+    let _ = spark.interrupt_operation("no_such_op_e2e");
+
+    // Tags are local state.
+    spark.add_tag("e2e_tag").expect("add_tag");
+    assert!(spark.get_tags().contains(&"e2e_tag".to_string()));
+    spark.remove_tag("e2e_tag");
+    spark.clear_tags();
+
+    // Derived sessions share the transport but are independent handles.
+    let ns = spark.new_session();
+    let _ = ns.range(1);
+    let cs = spark.clone_session();
+    let _ = cs.range(1);
+}
+
+/// Register a progress handler, run a query so the execute loop invokes it, 
then
+/// remove and clear. Covers the progress-handler registration surface.
+#[test]
+fn session_progress_handlers() {
+    if !should_run() {
+        return;
+    }
+    let spark = session();
+
+    let id = spark.register_progress_handler(|_progress| {});
+    // Drive a query so the execute loop has a chance to invoke handlers.
+    let _ = spark.range(1000).and_then(|d| d.collect());
+    spark.remove_progress_handler(id);
+    spark.clear_progress_handlers();
+}


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

Reply via email to