fix(cubesql): Preserve lastRefreshTime with post-processing and SQL pushdown - #11394
fix(cubesql): Preserve lastRefreshTime with post-processing and SQL pushdown#11394MazterQyou wants to merge 1 commit into
lastRefreshTime with post-processing and SQL pushdown#11394Conversation
|
Claude finished @MazterQyou's task in 4m 54s —— View job Review complete — approach is sound; 1 medium (
|
| # | Severity | Where | Issue |
|---|---|---|---|
| 1 | Medium | service.rs:128-140 |
external aggregates "any wins" while last_refresh_time right above aggregates "oldest wins". A join where one scan hits CubeStore and one hits the source DB reports external: true on a partially-live result. Elsewhere the flag is computed with .all(...) (base_query.rs:84, top_level_planner.rs:56). Needs a tri-state to be conservative — or an explicit note that "any" is deliberate. |
| 2 | Low | node_export.rs:377-386 |
The span read happens before the first stream.next(), so it works only because execute is async in this DataFusion fork and parents await children eagerly. Undocumented load-bearing assumption for a header that must be written first. Also, the comment's "every cube scan that ran" isn't true in stream mode (scan.rs:554 never calls load_data) — not a regression, but the comment overstates coverage. |
| 3 | Low | gateway.ts:2228-2233 |
disableExternalPreAggregations is set for pushdown queries, so response.external should always be falsy here. Harmless to mirror, but worth a note — the two new tests read as if both true and false are live cases. |
| 4 | Nit | sql-api-load.test.ts:64-78 |
No case for a response without lastRefreshTime — the common path, and the one that exercises the ?. and the undefined-is-dropped serialization contract. Plus a stray import * as console. |
Also minor, not worth an inline: span_id_for_schema (node_export.rs:306) is a second Arc clone alongside span_id_clone; one clone reused would do.
What's good
- Test design is deliberate: the
plain measure projectioncase in the parameterized native test is a real control that passed before the fix, so the pair actually isolates the regression rather than just asserting the happy path. - The
SpanIddoc comments explain the aggregation semantics, not just the mechanics — that's what made finding ReferenceError: self is not defined - Issue with whatwg-fetch when using the core lib with Node.js #1 reviewable at all. - Fallback ordering (
span.or_else(schema)) is the right precedence and keeps plans without a span working.
Not verified
I could not execute either test suite — node_modules is not installed in this checkout and a Rust build wasn't attempted. All findings are from reading code; the ordering analysis in #2 is from tracing execute() call sites, not from running the plans.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #11394 +/- ##
==========================================
+ Coverage 79.41% 79.43% +0.02%
==========================================
Files 480 480
Lines 98646 98679 +33
Branches 3636 3636
==========================================
+ Hits 78338 78390 +52
+ Misses 19790 19768 -22
- Partials 518 521 +3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
… pushdown Signed-off-by: Alex Qyoun-ae <4062971+MazterQyou@users.noreply.github.com>
5e3ae90 to
34e7db2
Compare
|
Claude finished @MazterQyou's task in 1m 59s —— View job Re-review of the update — all 4 prior findings addressed; 1 new low (
|
| # | Prior finding | Status |
|---|---|---|
| 1 | external aggregated "any wins" vs last_refresh_time's "oldest wins" |
Fixed. SpanId::external is now RWLockAsync<Option<bool>> with *write = Some(write.unwrap_or(true) && external) — the conservative tri-state. span_id_external_only_when_every_load_is_external covers the sticky-false case (true → false → true stays false), which is the part that's easy to get wrong. Doc comment now states the semantics and cross-references usages.iter().all(..). |
| 2 | Undocumented eager-child-execution assumption + comment overstated coverage | Fixed. The comment in node_export.rs:381-392 now spells out both the async execute dependency ("a node that deferred child execution to first poll would silently empty this header again, and the tests would not catch it") and the stream-mode gap. |
| 3 | external unreachable-as-true on the pushdown branch |
Fixed. gateway.ts:2233-2238 explains the disableExternalPreAggregations chain and that the mirror is for shape parity. The test mock carries the same note. |
| 4 | Missing negative test case + stray import * as console |
Fixed. omits lastRefreshTime when absent added, with a comment tying it to JSON.stringify dropping undefined → Option<String> on the Rust side. Import removed. |
Resolved all four threads.
New finding
Low — node_export.rs:412-420: the external schema fallback uses ||, which can undo the aggregation that was just made conservative.
Two blocks up, last_refresh_time uses or_else: span wins outright, schema is consulted only when the span is silent. external ORs them instead, so a span that deliberately folded to false can be overridden by leftover schema metadata.
Reachable case: a UNION of two cube scans, first from a CubeStore pre-aggregation, second live. UnionExec delegates to child streams rather than rebuilding a batch schema, so stream.schema() is the first child's batch schema and still carries external=true. The span correctly says false; the || re-reports external: true alongside lastRefreshTime = min(A, B) — precisely the disagreement the new set_external doc comment argues against.
Fix needs the tri-state to leak out (external() -> Option<bool>, .unwrap_or_else(schema), with shim.rs callers taking .unwrap_or(false)). Details in the inline comment. Narrow in practice, but the two adjacent blocks reading differently is the kind of thing that gets copied.
Also checked, no issue
set_externalread-modify-write happens under a single write guard, so concurrent loads can't interleave the fold.response.lastRefreshTime?.toISOString()—QueryOrchestrator.ts:262/287produces aDate, and this matches the existingprepareResultTransformDatacall atgateway.ts:1935.set_last_refresh_timestill only fires on a parseable RFC3339 value whileset_externalfires on every load; asymmetric but correct — a missing timestamp shouldn't make the span look fresher.span_id_for_schemais still a secondArcclone alongsidespan_id_clone; cosmetic, not worth a thread.
Not verified
Could not execute either suite — no node_modules in this checkout and no Rust build attempted. The UnionExec schema-delegation claim is from reading the operator's stream handling, not from running a union plan.
| if span_external | ||
| || stream | ||
| .schema() | ||
| .metadata() | ||
| .get("external") | ||
| .map(|v| v == "true") | ||
| .unwrap_or(false) | ||
| { | ||
| schema_response.insert("external".into(), serde_json::Value::Bool(true)); |
There was a problem hiding this comment.
The schema fallback for external uses ||, which can undo the conservative aggregation that SpanId::set_external was just changed to do.
last_refresh_time above uses or_else — span wins outright, schema is consulted only when the span is silent. external instead ORs the two, so a span that deliberately reported false can still be overridden to true by leftover schema metadata.
Concrete case: a UNION of two cube scans, first served from a CubeStore pre-aggregation, second live. UnionExec delegates to its child streams rather than rebuilding a batch schema, so stream.schema() here is the first child's batch schema and carries external=true. The span correctly folded to false (not every load was external), but the || re-reports external: true for a partially-live result — exactly the disagreement with lastRefreshTime = min(A, B) that the new doc comment on set_external argues against.
Matching the precedence used two blocks up needs the tri-state to leak out, e.g.:
pub async fn external(&self) -> Option<bool> { *self.external.read().await }let external = span_external.unwrap_or_else(|| {
stream.schema().metadata().get("external").map(|v| v == "true").unwrap_or(false)
});
if external { /* insert */ }(shim.rs callers would need .unwrap_or(false).) Low severity given how narrow the reachable case is, but the two adjacent blocks reading differently is the kind of thing that gets copied.
true
Check List
Description of Changes Made
This PR makes the SQL API return
lastRefreshTimefor queries whose plan has post-processing nodes over the cube scan (calculated projections overMEASURE(), sorts, filters) and for SQL pushdown queries, both of which silently dropped it. Related test is included.