akashchamp opened a new pull request, #68461:
URL: https://github.com/apache/doris/pull/68461
### What problem does this PR solve?
Issue Number: close #68074
Related PR: None
Problem Summary:
Insert a JSON boolean into a VARIANT column and read it back as JSON: it
comes back as the
number `1`/`0` instead of `true`/`false`, so `{"b": true}` does not
round-trip.
```sql
CREATE TABLE s (id INT, j VARIANT) DUPLICATE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 1 PROPERTIES('replication_num'='1');
INSERT INTO s VALUES (0, '{"b": true}');
SELECT CAST(j AS STRING) FROM s; -- was {"b":1}, now {"b":true}
```
The inferred/stored subcolumn type is already correctly `boolean` (with
`describe_extend_variant_column = true`, `DESC s` reports `j.b boolean`);
only the JSON
*output* was wrong. A path holding only booleans is the common case that
printed wrong:
mixing in any other JSON type (string, array, number) makes the path fall
back to JSONB
storage, whose writer already prints `true`/`false` correctly, which is why
the bug is easy
to miss. Nested booleans, `array<boolean>`, and a boolean path that overflows
`variant_max_subcolumns_count` into the sparse column are all affected the
same way.
Root cause: `ColumnVariant::Subcolumn::serialize_text_json()` reconstructs
each subcolumn's
JSON text by delegating to the inferred type's SerDe. For a BOOLEAN
subcolumn that reaches
`DataTypeNumberSerDe<TYPE_BOOLEAN>::serialize_one_cell_to_json()`, which
groups BOOLEAN with
the integer types (`is_int_or_bool`) and calls `write_number()` on the
underlying `UInt8`,
emitting `1`/`0`. That SerDe method is shared with every other
BOOLEAN-to-JSON caller in the
engine, so it could not simply be changed to always emit `true`/`false`.
A second, independent bypass affects `CAST(j['b'] AS STRING)` specifically:
`element_at` on
a scalar VARIANT root is treated by `cast_from_variant_impl()` as directly
castable, which
for a BOOLEAN root resolves to the ordinary SQL BOOLEAN-to-STRING caster
(`fmt::format_int`,
i.e. `0`/`1`) instead of going through the VARIANT JSON serializer above.
This is why
`CAST(j['b'] AS STRING)` stayed wrong even with only the first fix.
This is not the same defect as #68016, which changes VARIANT's
least-supertype merge so a
path holding both a boolean and a number resolves to JSONB instead of a
numeric type. A
boolean-only path never reaches that merge (its type is already `boolean`,
which is
correct); the defect here is downstream of typing, in how `boolean` is
written out as JSON.
The fix:
- `FormatOptions` already had an unused `is_bool_value_num` switch
documented for exactly
this (collection `true`/`false` vs `0`/`1` rendering) but nothing
consulted it. Wire it
into `DataTypeNumberSerDe<TYPE_BOOLEAN>::serialize_one_cell_to_json()`:
when false, write
the JSON literal `true`/`false`; the default stays `true` (`0`/`1`), so
every other
BOOLEAN-to-JSON caller (MySQL/CSV/text output, collections outside
VARIANT, etc.) keeps its
current behavior.
- `ColumnVariant::Subcolumn::serialize_text_json()` already takes its
`FormatOptions` by
value, so it is the single place every VARIANT JSON reconstruction path
funnels through
(dense, nested, array, doc-value snapshot and sparse-column reconstruction
all call it).
Set `is_bool_value_num = false` on that local copy so all of them request
JSON literals,
without touching the shared default used elsewhere.
- `cast_from_variant_impl()`: when a scalar VARIANT root is BOOLEAN and the
destination is a
string type, reuse the same structured-document JSON reconstruction
(`CastToStringFunction::execute_impl`, via the existing
`execute_on_finalized_input` helper)
instead of falling into the generic per-type wrapper. JSON booleans are
never quoted, so
this produces exactly the same text a full-tree reconstruction would.
### Release note
Fix a VARIANT correctness bug where a JSON boolean stored in a
homogeneous-boolean path
(scalar, nested, `array<boolean>`, or the sparse column) reconstructed as
the JSON number
`1`/`0` instead of the JSON boolean literal `true`/`false`, so `{"b": true}`
did not
round-trip through `CAST(... AS STRING)`, `CAST(j['b'] AS STRING)`, or any
JSON function
reading the reconstructed text. Plain (non-VARIANT) BOOLEAN output is
unchanged.
### Check List (For Author)
- Test <!-- At least one of them must be included. -->
- [x] Regression test
- [ ] Unit Test
- [ ] Manual test (add detailed scripts or steps below)
- [ ] No need to test or manual test. Explain why:
- [ ] This is a refactor/code format and no logic has been changed.
- [ ] Previous test can cover this change.
- [ ] No code files have been changed.
- [ ] Other reason <!-- Add your reason? -->
Added
`regression-test/suites/variant_p0/test_variant_boolean_json_output.groovy`,
covering: the scalar `{"b": true}`/`{"b": false}` round-trip, nested
`{"o":{"b":true}}`,
`array<boolean>` via `json_extract` on each element, `CAST(j['b'] AS
STRING)`
(element_at), `json_extract(CAST(j AS STRING), '$.b')`, the
pre-existing-correct
heterogeneous/JSONB control case (boolean sharing a path with a string),
and a plain
(non-VARIANT) `BOOLEAN` column as a compatibility guard that must keep
printing `1`/`0`.
I could not run this suite or reproduce against a live cluster myself:
this sandbox has
no Doris BE build toolchain (the pinned clang-16 LDB toolchain and
thirdparty libraries
aren't installed) and no running Doris cluster. What I did verify
directly: the exact
reconstruction and cast call paths by reading the source against the
reporter's
reproduction and the repository's own triage-bot analysis (both confirmed
the same root
cause and call chain independently), and `clang-format-16 --style=file -n
--Werror`
(installed for this check) reports no diff on any of the three changed C++
files. Please
run CI / the regression suite to confirm; I'm glad to address any failures
it turns up.
- Behavior changed:
- [ ] No.
- [x] Yes. `CAST(<a VARIANT with a homogeneous-boolean or array<boolean>
path> AS
STRING)` — and any other JSON-text reconstruction of that path,
including through the
sparse column and `element_at` — now emits `true`/`false` instead of
`1`/`0`. Every
other BOOLEAN-to-string/JSON path (plain `BOOLEAN` columns, and
VARIANT paths that were
already JSONB-backed and already printed correctly) is unchanged.
- Does this need documentation?
- [x] No.
- [ ] Yes. <!-- Add document PR link here. eg:
https://github.com/apache/doris-website/pull/1214 -->
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label <!-- Add branch pick label that this PR should
merge into -->
---
This PR targets `branch-4.1` directly rather than `master`, because master's
VARIANT JSON
serialization was already rewritten as part of the in-progress "Variant V2"
work (see
`be/src/core/data_type_serde/data_type_variant_v2_serde.cpp` /
`be/src/exprs/function/parse/variant_string_parse.h`, where
`Printer::write_primitive()`
already emits `true`/`false` for `TRUE_VALUE`/`FALSE_VALUE`), so this
specific defect does
not reproduce there. `branch-4.1` still uses the pre-V2 implementation
(`be/src/core/column/column_variant.cpp` / `data_type_variant_serde.cpp`,
selected via the
`enable_variant_v2` FE config) that this PR fixes, matching how the issue
was reported
(against 4.1.3). `branch-4.2` has the same pre-V2 implementation and is very
likely affected
too, but I've scoped this PR to the reported version; happy to open a
matching PR there if
maintainers want it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
--
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]