viirya commented on PR #54:
URL: 
https://github.com/apache/spark-connect-rust/pull/54#issuecomment-5415125899

   Reviewed the whole change — core semantics, the PyO3/skin layer, and the CI 
restructuring. A lot of this is genuinely good work, and two of my earlier 
findings are properly fixed. My concerns are concentrated in the parity gate, 
which I think is meaningfully weaker than it looks, and in a few smaller items 
below.
   
   ## First, what's clearly right
   
   Worth stating because it's the bulk of the diff:
   
   - **The variadic fix is a real, severe bug fix.** On current `master`, 
`array()`, `coalesce()`, `concat()`, `r#struct()`, `hash()`, `create_map()`, 
`elt()`, `stack()`, `xxhash64()`, `arrays_zip()` and friends take **zero 
arguments** — so `F.struct(a, b, c)` really did produce an empty struct. 16 
functions across two commits, and the same bug class as the `dropna`/`hint` 
family from the earlier reviews. Making them `Vec<Column>` and adding a 
variadic category to the dispatch generator is the right structural fix rather 
than another one-off.
   - **The `Sort` fix is correct.** A bare column previously fell through both 
branches and was dropped, leaving an empty `order` and an invalid plan. The new 
default of `direction = 1` / `null_ordering = 1` maps to 
`SORT_DIRECTION_ASCENDING` / `SORT_NULLS_FIRST`, which matches reference 
`asc()`. (Minor: those two magic numbers would read better as the generated 
enum constants — `SortDirection::Ascending as i32` — since a future proto 
reorder would silently change meaning.)
   - **The Arrow decoder unification is a clean win with no regression.** I 
checked every type the deleted `catalog.rs` decoder handled — all 11 are 
present in the surviving `dataframe::arrow_value_at`, and coverage goes from 11 
to 36 array types. Decimal128/256 preserve precision/scale as strings rather 
than going through `f64`, which is the correct choice. Deleting a duplicate 
decoder rather than patching both is the right call.
   - **`session.rs` GIL is fixed.** This was my open item from the last round: 
it now has 10 `py.detach()` calls covering `get_or_create`, `stop`, `version`, 
`interrupt_all`/`interrupt_tag`/`interrupt_operation`, `add_artifact(s)`, 
`build_resource_profile`, and `create_dataframe`. Thanks for closing that.
   - The API-fidelity work checks out where I sampled it: `head()` correctly 
returns `Row | None` vs `List[Row]` on the `n` overload, `fillna` dispatches 
dict vs scalar properly, `repartition(numPartitions, *cols)` matches, 
`Row(*args)` yields `_1.._n` while `Row(**kwargs)` yields named fields and 
mixing them raises, and the exception types 
(`IndexError`/`KeyError`/`AttributeError`) match reference Row semantics.
   
   ## The parity gate is weaker than the description implies
   
   This is my main concern, and it's about the gate's design rather than any 
single line.
   
   **Retries are applied to every file, not just the flaky ones.** The 
description says there's "a per-file retry for the timing-sensitive 
streaming-listener/observation tests," and the `--retries` help text gives a 
well-reasoned justification scoped to those tests. But the implementation in 
`work()` has no file filter at all — the `while` loop at 
`run_official_tests.py:165-170` re-runs *any* file that reports a failure, and 
CI invokes the script without `--retries`, so the default of 2 (three attempts 
total) applies to all 84 files.
   
   The consequence: a client bug that reproduces intermittently — a race in 
reattach, a timing-dependent streaming path, anything resource-sensitive — now 
needs to fail **three times in a row** to be reported. A bug that fails 50% of 
the time has a ~12% chance of being caught per run. The help text's claim that 
"a real regression still fails every attempt" holds only for deterministic 
regressions; it's exactly the non-deterministic ones this masks. Scoping the 
retry to the handful of genuinely event-driven files (an explicit list, or a 
marker) would keep the intended benefit without blanket-hiding flaky failures 
everywhere else.
   
   **Nothing detects a skiplist entry that starts passing.** 
`parity_known_failures.txt` has 65 entries and there's no "unexpectedly 
passing" check anywhere in `run_official_tests.py` or `gen_parity_skiplist.py`, 
and the generator only runs manually on a version bump. So the list can only 
ever rot in the direction of less coverage: once a test is in it, nothing 
re-examines it, and a genuine future regression in a skiplisted area is 
permanently invisible. A cheap fix is to run the skiplisted tests separately 
(not deselected) and report — not fail — any that now pass, so drift surfaces 
without blocking.
   
   **The reasons are uniform boilerplate.** All 65 entries carry the identical 
comment `# reference also fails (environmental)`. I want to be fair here: these 
were *generated* by actually running the reference client, so "reference also 
fails" is an empirical observation, not a guess — and several are plausible in 
a single-node pure-Connect environment (`test_udf_with_input_file_name` with no 
real input file, the `foreach_batch` family needing a working Python-worker 
callback path). But because no entry records *why* the reference failed, nobody 
can later distinguish "environmental, still true" from "was a real gap, now 
fixed" from "was misclassified." That's what makes the staleness problem above 
unfixable in practice. Capturing the reference's actual error class per entry 
when generating would cost little and make the manifest auditable.
   
   On the mechanics, two things I checked that are **fine**: entries are 
matched via pytest `--deselect` with full node IDs 
(`run_official_tests.py:104`), which is exact, not substring — so a loose match 
can't silently swallow neighbouring tests. And a malformed entry matches 
nothing, meaning the test runs and fails loudly. That fails safe, which is the 
right direction.
   
   ## The coverage gate described in the PR doesn't exist
   
   The description says "a new `rust-coverage` CI job fails under 
`COVERAGE_MIN` (90%)", and the testing section lists "the coverage gate (≥90%)" 
as having run on this PR. Neither is true of the current branch: no workflow 
references `rust_coverage.sh`, `COVERAGE_MIN`, or a coverage job. Commit 
`3c05ab9` ("Keep coverage as tooling; defer the gating CI job") deliberately 
removed it, so the description is describing an earlier state of the branch.
   
   The script itself is fine as local tooling, but two `|| true` at 
`rust_coverage.sh:81,85` swallow failures from the e2e and official-test runs, 
so a run where the Python suite dies still produces a report — just with 
silently lower coverage. If the gate is later turned on, that needs to fail 
instead. I'd just correct the description now so reviewers aren't told a gate 
is enforcing something it isn't.
   
   ## `e2e_wrapper.py` proves absence of exceptions, not correctness
   
   The 174-operation exercise is a reasonable coverage vehicle, and driving the 
real drop-in API is exactly the gap the transport-injection gate leaves. But 
`ck()` (line 46) only catches exceptions:
   
   ```python
   def ck(label, fn):
       try:
           fn()
           _ok.append(label)
       except Exception as e:
           _fail.append(...)
   ```
   
   No result is ever asserted. `ck("session.range", lambda: 
spark.range(3).collect())` passes whether `collect()` returns 3 correct rows, 0 
rows, or garbage. That matters specifically here, because **the bug class this 
PR fixes is silently-wrong values, not exceptions** — a nullary `F.struct()` 
returning an empty struct throws nothing, so this harness would have reported 
174/174 green while the bug was live. Asserting even coarse expectations for a 
subset (row counts, a known value or two) would turn it from a smoke test into 
something that can catch a regression.
   
   The same caveat applies to the three new `*_coverage_golden.rs` files, 
though they're honest about it — the headers say "this only guards that no 
builder panics or is left untested," and `functions_coverage_golden.rs` is 949 
lines with one assertion. That's a legitimate purpose (it's the coverage 
vehicle), and real parity does live in `functions_golden.rs` plus 
`tests/golden/functions.jsonl`, which I confirmed contains `struct`, `hash`, 
`array`, `coalesce`, `concat`, `elt`, `create_map`, and `arrays_zip`. Worth 
being aware that ~1,200 lines of new test code deliberately assert almost 
nothing, so a coverage number derived from them overstates how much behaviour 
is actually pinned.
   
   ## Smaller items
   
   **GIL held in the two new binding files.** `stat.rs` and `conf.rs` have zero 
`py.detach()`. In `stat.rs`, `corr` and `cov` call `df.scalar()?`, which 
executes a plan and round-trips to the server, so those hold the GIL for the 
duration (`crosstab`/`freq_items` return DataFrames and are lazy, so they're 
fine). In `conf.rs`, `RuntimeConf::set`/`get` reach 
`block_on(self.client.set_config(...))` / `get_configs(...)` in the core. Same 
one-line fix as `session.rs` already uses.
   
   For the record, I checked `sql`, `range`, `table`, and `empty_data_frame` in 
`session.rs` and they are **lazy plan builders** with no network call, so 
holding the GIL there is harmless — not a bug despite looking like one.
   
   **Non-string map keys render as Rust Debug output.** `Value::Map` is 
`BTreeMap<String, Value>`, and the new `MapArray` arm coerces non-string keys 
with `format!("{other:?}")` (`dataframe.rs` ~2192). So a `map<int, string>` 
column yields the key `"Integer(1)"` rather than `"1"`. Given this PR is 
specifically about completing the Arrow mapping, that's worth either fixing 
(format the scalar, not the enum) or noting as a known limitation.
   
   **This branch is behind `master`.** `pr-54` forks from `9d01795` and is 
missing three commits including the docs site (#53). No textual conflicts — 
both sides touch `build_python_connect.yml` in different places — but it needs 
a rebase, and after #53 the docs now describe the API this PR changes 
(`functions` signatures in particular), so the docs pages may need a matching 
update.
   
   ## What I did and didn't verify
   
   I read the core diffs, compiled nothing against a live server, and did not 
run the parity suite or the coverage script. The bug-fix claims above I 
verified by reading `master` and the branch side by side; the gate behaviour I 
verified by reading the scripts and the workflow invocation. I have not 
independently confirmed that the 65 skiplisted tests do fail under the 
reference client — that I'm taking from how the manifest was generated.
   


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