Skip to content

test(cubesqlplanner): run integration-cubestore tests over cubestore-ws-transport - #11951

Open
ovr wants to merge 2 commits into
masterfrom
cleanup-mysql-client-code
Open

ovr wants to merge 2 commits into
masterfrom
cleanup-mysql-client-code

Conversation

@ovr

@ovr ovr commented Sep 21, 2026

Copy link
Copy Markdown
Member

Check List

  • Tests have been run in packages where changes have been made if available
  • Linter has been run for changed code
  • Tests for the changes have been added if not covered yet
  • Docs have been added / updated if required

Description of Changes Made

The --features integration-cubestore harness talked to cubestored as a MySQL client via mysql_async — the only use of it anywhere in rust/cube, and not the transport Cube actually uses (production goes over WebSocket + FlatBuffers); it now uses cubestore-ws-transport, the Rust port of that driver, already a member of the same workspace. This also closes the lru 0.12.5 advisory (GHSA-rhfx-m35p-ff5j) without the breaking mysql_async 0.37 bump, since mysql_async 0.34.2 was its sole consumer: dropping it removed 38 packages from rust/cube/Cargo.lock (incl. mysql_common and the bindgen/clang-sys/zstd chain) and added none, as ws-transport's graph was already present via cubestore-cli. Results now arrive as Arrow record batches rather than MySQL rows, so mysql_value_to_string is replaced by cubestore_result_rows rendering through ArrayFormatter, with columns from QueryResult::get_columns() — which also fixes an empty result rendering as (empty result), since columns were previously derived from the first row.

cargo test -p cubesqlplanner --features integration-cubestore: 1412 passed, 0 failed, 10 ignored, against a release cubestored built from this tree. Clippy clean; cubestore-ws-transport's own tests still green.

Rendering

Arrow's own rendering is kept as-is, including RFC3339 timestamps. The single FormatOptions override is with_null("NULL"), because arrow renders a null as an empty string, which is indistinguishable from an empty string value in the rendered table.

The rolling-window tests compare the CubeStore result against Postgres, and normalize reconciled the two by matching on the Z suffix that cubestored's MySQL layer emitted. RFC3339 has no Z, so that matcher is now positional on the YYYY-MM-DDTHH:MM:SS shape — a plain contains('T') would have mangled the YTD calc-group value.

Snapshot changes

Only pre_aggregations/*_cubestore_result.snap files changed; the 15 under tests/integration/cubestore/ are untouched, which also confirms YTD survives the new matcher. Compared at cell level (column padding shifts make the line diff unreadable): no row- or column-count changes, two classes, each verified against the reported Arrow schema:

old new arrow type why
2025-01-01T00:00:00.000Z 2025-01-01T00:00:00 Timestamp(Microsecond, None) cubestored's MySQL layer appended a .000Z; arrow renders plain RFC3339
1000 1000.00000 Decimal128(28, 5) MySQL trimmed trailing zeros; arrow renders the declared scale, exposing the real rollup column type (also hit at scale 9 and 13)

I also confirmed the server genuinely answers Arrow, so a silent Legacy fallback isn't passing these tests without exercising the new wire format.

The one remaining mysql reference in the harness is deliberate and commented: CUBESTORE_PORT/CUBESTORE_BIND_ADDR stay pinned to a free port because unset, cubestored binds 0.0.0.0:3306 and parallel test binaries collide.

🤖 Generated with Claude Code

…ws-transport

The `--features integration-cubestore` harness talked to cubestored as a MySQL
client via `mysql_async` — the only use of it in `rust/cube`, and not the
transport Cube actually uses. Production reaches CubeStore over WebSocket +
FlatBuffers; `cubestore-ws-transport` is the Rust port of that driver and is
already a member of the same workspace.

`mysql_async` 0.34.2 was the sole consumer of `lru` 0.12.5
(GHSA-rhfx-m35p-ff5j), so this closes the alert without the breaking 0.37 bump.
Dropping it removed 38 packages from `rust/cube/Cargo.lock` (incl.
`mysql_common` and the bindgen/clang-sys/zstd chain) and added none —
ws-transport's graph was already present via `cubestore-cli`.

Results now arrive as Arrow record batches rather than MySQL rows, so
`mysql_value_to_string` is replaced by `cubestore_result_rows`, rendering
through `ArrayFormatter`. Columns come from `QueryResult::get_columns()`, which
also fixes an empty result rendering as `(empty result)` — they were previously
derived from the first row.

35 `pre_aggregations/*_cubestore_result.snap` snapshots changed; the 15 under
`tests/integration/cubestore/` did not. 61 cell changes, no row- or
column-count changes, in two classes:

| old | new | arrow type | why |
| --- | --- | --- | --- |
| `2025-01-01T00:00:00.000Z` | `2025-01-01 00:00:00` | `Timestamp(Microsecond, None)` | cubestored's MySQL layer stringified to ISO-8601; the new form is what the Postgres side of these tests already produces |
| `1000` | `1000.00000` | `Decimal128(28, 5)` | MySQL trimmed trailing zeros; arrow renders the declared scale (also hit at scale 9 and 13) |

`with_timestamp_format` is load-bearing rather than cosmetic: without it the
switch_rolling CubeStore-vs-Postgres equality assertions fail.

`cargo test -p cubesqlplanner --features integration-cubestore`: 1412 passed,
0 failed, 10 ignored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the rust Pull requests that update Rust code label Sep 21, 2026
@claude

claude Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Claude finished @ovr's task in 5m 27s —— View job


Reviewed — no blockers. 2 medium, 2 low, posted inline.

Review detail
  • Gather context (diff, changed files)
  • Review harness changes (cubestore_service.rs, test_context.rs)
  • Review snapshot / dependency changes
  • Check prior review threads
  • Post findings

Verified

  • mysql_async, mysql_common and lru are all absent from rust/cube/Cargo.lock — the GHSA-rhfx-m35p-ff5j claim holds, and arrow resolves to a single version (58.3.0), so no duplicate graph.
  • mysql_async has no remaining reference anywhere under rust/ (.rs or .toml); connect_with_schema has exactly one caller.
  • The readiness probe moving from the MySQL port to the HTTP port is not weaker: config/mod.rs:166-177 spawns MySqlServer::processing_loop before HttpServer::run_server, both after listen_on_metastore_port has signalled started_rx.
  • The actor terminates on inbox.recv() == None (actor.rs:103-107), so dropping the per-test Client at the end of try_execute_cubestore shuts it down — no leaked tasks across 1400 tests.
  • ResultData::Completed still renders (empty result) via format_rows_table's columns.is_empty() guard, so the PR's "columns survive an empty result" improvement applies to empty Arrow batches only, which matches the snapshot evidence.
  • Sampled snapshot diffs match the two declared change classes (13751375.00000, timestamp reformat), no row- or column-count movement.

Findings

# Severity Where Concern
1 Medium test_context.rs:1359 The Legacy arm renders the old strings, so an older cubestored fails 35 snapshots with 61 unexplained cell diffs rather than degrading. Nothing pins get_format() == Arrow.
2 Medium cubestore_service.rs:179 Default max_connect_retries: 20 + linear backoff = 210 s stall per in-flight query when cubestored dies — a documented failure mode (:43-47 debug stack overflow).
3 Low test_context.rs:1352 with_display_error(true) bakes arrow formatting errors into snapshot text where they can be insta accepted; the adjacent comment is 5 lines for one decision.
4 Low 35 *.snap Regeneration added an assertion_line: header to every touched snapshot (3 → 38 of 57 in the directory); it's volatile line-number metadata insta ignores when comparing.

Review threads

No prior threads — gh list-review-threads cube-js cube 11951 returned 0 nodes, so nothing to resolve or re-affirm.

· branch `cleanup-mysql-client-code`

Comment on lines +1335 to +1366
/// Stringifies a CubeStore result into `(columns, rows)` for
/// `format_rows_table`. The transport always asks for Arrow, but an older
/// server can still answer with the legacy row envelope, so both are handled.
#[cfg(feature = "integration-cubestore")]
fn mysql_value_to_string(value: Option<&mysql_async::Value>) -> String {
use mysql_async::Value;
match value {
None | Some(Value::NULL) => "NULL".to_string(),
Some(Value::Bytes(bytes)) => String::from_utf8_lossy(bytes).into_owned(),
Some(Value::Int(v)) => v.to_string(),
Some(Value::UInt(v)) => v.to_string(),
Some(Value::Float(v)) => v.to_string(),
Some(Value::Double(v)) => v.to_string(),
Some(Value::Date(y, m, d, h, min, s, micros)) => {
if *micros == 0 {
format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}", y, m, d, h, min, s)
} else {
format!(
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:06}",
y, m, d, h, min, s, micros
)
}
}
Some(other) => format!("{:?}", other),
}
fn cubestore_result_rows(
result: &cubestore_ws_transport::QueryResult,
) -> (Vec<String>, Vec<Vec<String>>) {
use cubestore_ws_transport::arrow::util::display::{ArrayFormatter, FormatOptions};
use cubestore_ws_transport::ResultData;

// Arrow's defaults render NULL as an empty string and timestamps as
// RFC3339 (`2024-04-01T00:00:00`). Both are overridden to match the
// Postgres rendering, because the rolling-window tests assert the
// CubeStore and Postgres results are equal. chrono's `%.f` emits
// nothing when the fraction is zero.
let fmt_options = FormatOptions::default()
.with_null("NULL")
.with_display_error(true)
.with_timestamp_format(Some("%Y-%m-%d %H:%M:%S%.f"));

// Column names come from the payload, so they survive an empty result.
let columns = result.get_columns();
let rows = match &result.data {
ResultData::Completed => Vec::new(),
ResultData::Legacy { rows, .. } => rows
.iter()
.map(|row| {
row.iter()
.map(|cell| cell.clone().unwrap_or_else(|| "NULL".to_string()))
.collect()
})
.collect(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Legacy branch can't produce a passing snapshot, so as a fallback it only obscures the failure. The 35 snapshots in this PR were regenerated from the Arrow branch's rendering — 2025-01-01 00:00:00 and 1000.00000. cubestored's legacy envelope stringifies the same cells as 2025-01-01T00:00:00.000Z and 1000, i.e. exactly the old values. So a server that answers Legacy (older binary via @cubejs-backend/cubestore's downloaded/latest/bin/cubestored, which cubestored_bin() at cubestore_service.rs:61 will happily pick up) doesn't degrade gracefully here — it fails 35 tests with 61 unexplained cell diffs.

The PR body says you confirmed the server answers Arrow, but that check is manual and nothing in the harness pins it. Since get_format() is right there, make the assumption enforceable:

assert_eq!(
    result.get_format(),
    ResponseFormat::Arrow,
    "cubestored answered {} — snapshots are recorded against the arrow rendering",
    result.get_format()
);

and drop the Legacy arm (or leave it unreachable!). That turns a wall of diffs into one line naming the cause, and removes an untested branch from the harness.

Comment on lines +179 to 181
let client = Client::connect(ClientConfig::new(url))
.await
.expect("Failed to connect to cubestored");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClientConfig::new leaves max_connect_retries: 20 with the actor's linear backoff of (attempt + 1) * 1000 ms (actor.rs:227-229), so a query in flight when cubestored dies sits in attempt_reconnect for 1+2+…+20 s = 210 s before run() gives up and the query resolves to TransportError::Disconnected.

That's a reachable path, not a hypothetical: cubestored_bin() at :43-47 documents that a debug cubestored stack-overflows on deep multi-stage plans, and CUBESTORED_ALLOW_DEBUG=1 opts into exactly that. Under mysql_async the crash surfaced immediately as a connection error naming the failing SQL; now each in-flight test burns 3.5 minutes first and then panics with Disconnected, which points at nothing.

The fields are public — a one-off config for the harness keeps the fail-fast behaviour:

let mut cfg = ClientConfig::new(url);
// The server is a local child process: if it's gone it isn't coming back,
// and the default 20-attempt backoff would stall the test for ~3.5 minutes.
cfg.max_connect_retries = 1;
let client = Client::connect(cfg)

Comment on lines +1345 to +1353
// Arrow's defaults render NULL as an empty string and timestamps as
// RFC3339 (`2024-04-01T00:00:00`). Both are overridden to match the
// Postgres rendering, because the rolling-window tests assert the
// CubeStore and Postgres results are equal. chrono's `%.f` emits
// nothing when the fraction is zero.
let fmt_options = FormatOptions::default()
.with_null("NULL")
.with_display_error(true)
.with_timestamp_format(Some("%Y-%m-%d %H:%M:%S%.f"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with_display_error(true) is the wrong direction for a snapshot harness. In that mode arrow writes the formatting error into the output instead of propagating it, so a column arrow can't render produces a cell containing the error text — which format_rows_table pads into the table and cargo insta accept then blesses as the expected result. false makes ValueFormatter::to_string() panic instead, which is what you want from a test. It also looks like arrow's default for safe, in which case the call is a no-op as written.

Separately, the comment above it is 5 lines for one decision. The load-bearing sentence is the with_null/with_timestamp_format rationale; the %.f aside and the RFC3339 example are recoverable from the format string:

// Overridden to match the Postgres rendering, because the rolling-window
// tests assert the CubeStore and Postgres results are equal.

@@ -1,7 +1,8 @@
---
source: cubesqlplanner/cubesqlplanner/src/tests/integration/pre_aggregations/multi_fact.rs
assertion_line: 40

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All 35 regenerated snapshots gained an assertion_line: header — that's the entire content of the +2/-1 and part of every other diff here. Before this PR only 3 of the 57 snapshots in this directory carried it; after, 38 do, and the 5 *_cubestore_result.snap files the run didn't touch still don't.

It records the source line of the assert_snapshot! call, so every one of these files now produces a spurious diff the next time a test above it in multi_fact.rs / sql_generation.rs grows or shrinks by a line. insta ignores it when comparing, so it buys nothing.

cargo insta test --force-update-snapshots --accept rewrites them without it, or just strip the added lines.

…sults

Drop the `with_timestamp_format` override and render timestamps as arrow
produces them. `with_display_error(true)` goes too — `safe: true` is already
the `FormatOptions` default, so it was a no-op. Only the null override is
kept, since arrow renders a null as an empty string, which is
indistinguishable from an empty string value.

The rolling-window tests compare CubeStore against Postgres, and `normalize`
reconciled the two by matching on the `Z` suffix that cubestored's MySQL
layer used to emit. Arrow's RFC3339 has no `Z`, so the matcher is now
positional on the `YYYY-MM-DDTHH:MM:SS` shape — a plain `contains('T')` would
have mangled the `YTD` calc-group value.

18 `pre_aggregations/*_cubestore_result.snap` fixtures updated, every change a
space -> `T` on a timestamp, no row- or column-count changes. The 15 snapshots
under `tests/integration/cubestore/` are unaffected, which also confirms `YTD`
survives the new matcher.

`cargo test -p cubesqlplanner --features integration-cubestore`: 1412 passed,
0 failed, 10 ignored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rust Pull requests that update Rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant