feat(cubeorchestrator): Split numeric query result into typed variants#11036
feat(cubeorchestrator): Split numeric query result into typed variants#11036ovr wants to merge 1 commit into
Conversation
|
Claude finished @ovr's task in 2m 11s —— View job ReviewTasks & detailed findings (click to expand) — 1 high, 2 medium, 2 low
SummaryThe split into typed numeric variants is the right direction —
Notes
|
fbf6637 to
86eb368
Compare
|
Claude finished @ovr's task in 1m 17s —— View job Code Review — 3 medium concerns (all pre-existing inline threads still apply)Full reviewSummaryThis PR replaces a single Re-affirmed 3 prior threads still apply:
Other notes
|
| match self { | ||
| DBResponsePrimitive::Null => serializer.serialize_none(), | ||
| DBResponsePrimitive::Boolean(b) => serializer.serialize_bool(*b), | ||
| DBResponsePrimitive::Int64(n) => serializer.collect_str(n), | ||
| DBResponsePrimitive::UInt64(n) => serializer.collect_str(n), | ||
| DBResponsePrimitive::Float64(n) => serializer.collect_str(n), | ||
| DBResponsePrimitive::Int128(s) => serializer.serialize_str(s), | ||
| DBResponsePrimitive::UInt128(s) => serializer.serialize_str(s), | ||
| DBResponsePrimitive::String(s) => serializer.serialize_str(s), | ||
| DBResponsePrimitive::Uncommon(v) => v.serialize(serializer), | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Wire format change + asymmetric round-trip.
Every numeric variant now serializes as a JSON string (collect_str), but Deserialize only accepts JSON numbers. Two consequences:
- The on-wire JSON payload for every numeric cell changes from
42/2.0to"42"/"2". JS consumers readingresult.dataand any external client (BI tools, snapshot tests) will see strings where they used to see numbers. - A round-trip through this type is lossy:
Int64(5)→ serializes to"5"→ deserializes viavisit_str→ ends up asDBResponsePrimitive::String("5"), notInt64(5). Any internal code that re-parses an already-serialized result silently loses the numeric type.
Consider:
- Emitting JSON numbers when the value fits in JS's safe integer range (
|n| <= 2^53), and strings only beyond that, OR - Always emitting strings but also accepting strings on the deserialize side so the type is its own inverse.
A test that explicitly round-trips serde_json::to_string → from_str and asserts variant equality would lock this in.
| (FieldValue::Int64(number), builder) => builder.append_value(number as i16)?, | ||
| (FieldValue::UInt64(number), builder) => builder.append_value(number as i16)?, |
There was a problem hiding this comment.
Integer cast semantics change: saturate → truncate/wrap.
Previously, this column path went through FieldValue::Number(f64) → number.round() as i16. Rust's f64 as i16 saturates (since Rust 1.45), so e.g. 1_000_000.0 → i16::MAX.
With Int64(n) as i16 / UInt64(n) as i16, the cast truncates/wraps: 1_000_000_i64 as i16 == 16960. Same concern for the Int32 branch below, and for Int64 when fed UInt64 ≥ i64::MAX.
If saturation is the intended semantics for "value too wide for column type" (it usually is, otherwise the warning row produced by the String arm gets silently better treatment than typed-int rows), replace as with a checked path, e.g.:
| (FieldValue::Int64(number), builder) => builder.append_value(number as i16)?, | |
| (FieldValue::UInt64(number), builder) => builder.append_value(number as i16)?, | |
| (FieldValue::Int64(number), builder) => builder.append_value(i16::try_from(number).unwrap_or(if number > 0 { i16::MAX } else { i16::MIN }))?, | |
| (FieldValue::UInt64(number), builder) => builder.append_value(i16::try_from(number).unwrap_or(i16::MAX))?, |
| Ok(FieldValue::String(Cow::Owned(s.value(&mut self.cx)))) | ||
| } else if let Ok(n) = value.downcast::<JsNumber, _>(&mut self.cx) { | ||
| Ok(FieldValue::Number(n.value(&mut self.cx))) | ||
| Ok(FieldValue::Float64(n.value(&mut self.cx))) |
There was a problem hiding this comment.
The JS streaming path collapses every JsNumber to Float64, so the new Int64/UInt64 variants don't help here even when the JS value is integer-valued. If the goal is to preserve int-vs-float through the whole result path, this should detect integer-valued doubles:
let f = n.value(&mut self.cx);
if f.is_finite() && f.fract() == 0.0
&& f >= i64::MIN as f64 && f <= i64::MAX as f64 {
Ok(FieldValue::Int64(f as i64))
} else {
Ok(FieldValue::Float64(f))
}(Note: integers > 2^53 from JS already arrive as floats with lost precision — this only matters for values inside JS's safe integer range, which is most real-world cases.)
If keeping this path Float64-only is intentional, a comment explaining why would help future readers.
| Ok(DBResponsePrimitive::Int64(v)) | ||
| } | ||
|
|
||
| fn visit_i128<E: de::Error>(self, v: i128) -> Result<Self::Value, E> { | ||
| Ok(DBResponsePrimitive::Number(v as f64)) | ||
| Ok(DBResponsePrimitive::Int128(v.to_string())) | ||
| } | ||
|
|
||
| fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> { | ||
| Ok(DBResponsePrimitive::Number(v as f64)) | ||
| Ok(DBResponsePrimitive::UInt64(v)) |
There was a problem hiding this comment.
visit_i128 / visit_u128 are likely unreachable from serde_json by default.
serde_json parses all numbers through visit_u64 / visit_i64 / visit_f64 unless the arbitrary_precision feature is enabled. With the current setup, the Int128 / UInt128 variants will essentially never be produced from JSON input — only from hand-constructed values.
If 128-bit JSON input is a real concern (e.g. bigint measures from CubeStore), consider either enabling serde_json/arbitrary_precision (and handling Number accordingly) or adding a visit_str fallback that tries i128::from_str / u128::from_str for digit-only strings before falling back to String. Otherwise it's worth a comment noting the limitation.
86eb368 to
cc5cad1
Compare
|
Claude finished @ovr's task in 49s —— View job Code Review — no changes since prior reviewVerdict: 1 high, 3 medium, 1 low — all from the prior pass; the diff is unchanged on this push, so no new inline comments. Re-affirmed 4 prior threads still apply. Tasks & re-affirmed findings (click to expand)
Re-affirmed (still unresolved, still applicable)
Other note (not posted inline)
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #11036 +/- ##
==========================================
+ Coverage 78.78% 83.32% +4.53%
==========================================
Files 471 255 -216
Lines 94509 77274 -17235
Branches 3520 0 -3520
==========================================
- Hits 74463 64391 -10072
+ Misses 19538 12883 -6655
+ Partials 508 0 -508
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:
|
Replace the single f64 `DBResponsePrimitive::Number` / `FieldValue::Number` variant with precise `Int64`/`UInt64`/`Float64` variants so integer-vs-float type is preserved through the query result path and large 64-bit integers no longer lose precision via f64.
cc5cad1 to
9a3f471
Compare
|
Claude finished @ovr's task in 33s —— View job Code Review — no changes since prior reviewVerdict: 1 high, 3 medium, 1 low — all from prior passes; the diff is unchanged on this trigger, so no new inline comments. Re-affirmed 4 prior threads still apply. Tasks & re-affirmed findings (click to expand)
Re-affirmed (still unresolved, still applicable)
Other note (not posted inline)
— branch: |
Replace the single f64
DBResponsePrimitive::Number/FieldValue::Numbervariant with preciseInt64/UInt64/Float64variants so integer-vs-float type is preserved through the query result path and large 64-bit integers no longer lose precision via f64.