-
Notifications
You must be signed in to change notification settings - Fork 3.9k
[feature](jsonb) add json_object_flatten scalar function #62825
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| // Licensed to the Apache Software Foundation (ASF) under one | ||
| // or more contributor license agreements. See the NOTICE file | ||
| // distributed with this work for additional information | ||
| // regarding copyright ownership. The ASF licenses this file | ||
| // to you under the Apache License, Version 2.0 (the | ||
| // "License"); you may not use this file except in compliance | ||
| // with the License. You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, | ||
| // software distributed under the License is distributed on an | ||
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| // KIND, either express or implied. See the License for the | ||
| // specific language governing permissions and limitations | ||
| // under the License. | ||
|
|
||
| #include <gtest/gtest.h> | ||
|
|
||
| #include <string> | ||
| #include <utility> | ||
| #include <vector> | ||
|
|
||
| #include "exprs/function/function_jsonb_transform.cpp" | ||
| #include "util/jsonb_document.h" | ||
| #include "util/jsonb_parser_simd.h" | ||
| #include "util/jsonb_utils.h" | ||
| #include "util/jsonb_writer.h" | ||
|
|
||
| namespace doris { | ||
|
|
||
| namespace { | ||
|
|
||
| // Parse JSON text into JSONB bytes via the standard simdjson-backed parser. | ||
| std::string json_to_jsonb(const std::string& json) { | ||
| JsonbWriter writer; | ||
| auto status = JsonbParser::parse(json.data(), json.size(), writer); | ||
| EXPECT_TRUE(status.ok()) << "parse failed: " << json << " -> " << status.to_string(); | ||
| return std::string(writer.getOutput()->getBuffer(), writer.getOutput()->getSize()); | ||
| } | ||
|
|
||
| // Run flatten_json_object end-to-end starting from a JSON text input and | ||
| // returning the flattened result rendered back to JSON text. The parse → | ||
| // flatten → re-render trip exercises the same code path the SQL function | ||
| // follows: ColumnString(JSONB) -> flatten_json_object -> ColumnString(JSONB). | ||
| std::string flatten(const std::string& json_in) { | ||
| const std::string in_bytes = json_to_jsonb(json_in); | ||
| const JsonbDocument* doc = nullptr; | ||
| auto status = JsonbDocument::checkAndCreateDocument(in_bytes.data(), in_bytes.size(), &doc); | ||
| EXPECT_TRUE(status.ok()) << status.to_string(); | ||
| EXPECT_NE(doc, nullptr); | ||
|
|
||
| JsonbWriter writer; | ||
| flatten_json_object(writer, doc->getValue()); | ||
| return JsonbToJson::jsonb_to_json_string(writer.getOutput()->getBuffer(), | ||
| writer.getOutput()->getSize()); | ||
| } | ||
|
|
||
| void check(const std::string& input, const std::string& expected) { | ||
| EXPECT_EQ(flatten(input), expected) << "input: " << input; | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| TEST(function_json_object_flatten_test, two_level) { | ||
| check(R"({"a":{"b":2}})", R"({"a.b":2})"); | ||
| } | ||
|
|
||
| TEST(function_json_object_flatten_test, three_level) { | ||
| check(R"({"a":{"b":{"c":3}}})", R"({"a.b.c":3})"); | ||
| } | ||
|
|
||
| TEST(function_json_object_flatten_test, already_flat) { | ||
| check(R"({"a":1,"b":"hi"})", R"({"a":1,"b":"hi"})"); | ||
| } | ||
|
|
||
| TEST(function_json_object_flatten_test, empty_top_level_object) { | ||
| check("{}", "{}"); | ||
| } | ||
|
|
||
| TEST(function_json_object_flatten_test, empty_nested_object_is_leaf) { | ||
| check(R"({"a":{}})", R"({"a":{}})"); | ||
| } | ||
|
|
||
| TEST(function_json_object_flatten_test, deep_nesting) { | ||
| check(R"({"a":{"b":{"c":{"d":{"e":{"f":{"g":{"h":{"i":{"j":{"k":1}}}}}}}}}}})", | ||
| R"({"a.b.c.d.e.f.g.h.i.j.k":1})"); | ||
| } | ||
|
|
||
| TEST(function_json_object_flatten_test, prefix_buffer_is_reset_across_siblings) { | ||
| check(R"({"a":{"x":1},"b":{"y":2}})", R"({"a.x":1,"b.y":2})"); | ||
| } | ||
|
|
||
| TEST(function_json_object_flatten_test, array_of_scalars_under_nested_path_stays_opaque) { | ||
| check(R"({"a":{"b":[1,2,3]}})", R"({"a.b":[1,2,3]})"); | ||
| } | ||
|
|
||
| TEST(function_json_object_flatten_test, array_of_objects_under_nested_path_stays_opaque) { | ||
| // keep-arrays semantics: the array is a leaf value under "a.b"; the | ||
| // inner object's key "d" must NOT show up at the flat level. | ||
| check(R"({"a":{"b":[{"d":1},{"d":2}]}})", R"({"a.b":[{"d":1},{"d":2}]})"); | ||
| } | ||
|
|
||
| TEST(function_json_object_flatten_test, mixed_object_scalar_and_array_leaves) { | ||
| check(R"({"x":{"s":1,"a":[1,2],"o":{"k":"v"}}})", R"({"x.s":1,"x.a":[1,2],"x.o.k":"v"})"); | ||
| } | ||
|
|
||
| TEST(function_json_object_flatten_test, null_leaf_at_top) { | ||
| check(R"({"a":null})", R"({"a":null})"); | ||
| } | ||
|
|
||
| TEST(function_json_object_flatten_test, null_leaf_nested) { | ||
| check(R"({"a":{"b":null}})", R"({"a.b":null})"); | ||
| } | ||
|
|
||
| TEST(function_json_object_flatten_test, top_level_scalar_pass_through) { | ||
| check("42", "42"); | ||
| check("\"hello\"", "\"hello\""); | ||
| check("null", "null"); | ||
| check("true", "true"); | ||
| } | ||
|
|
||
| TEST(function_json_object_flatten_test, top_level_array_pass_through) { | ||
| check(R"([1,2,3])", R"([1,2,3])"); | ||
| check(R"([{"x":1}])", R"([{"x":1}])"); | ||
| } | ||
|
|
||
| TEST(function_json_object_flatten_test, literal_dotted_key_round_trips) { | ||
| // A literal '.' inside a key collapses with real nesting at the flat layer | ||
| // — the same documented-lossy collision NiFi FlattenJson accepts. | ||
| check(R"({"a.b":2})", R"({"a.b":2})"); | ||
| } | ||
|
|
||
| } // namespace doris |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| // Licensed to the Apache Software Foundation (ASF) under one | ||
| // or more contributor license agreements. See the NOTICE file | ||
| // distributed with this work for additional information | ||
| // regarding copyright ownership. The ASF licenses this file | ||
| // to you under the Apache License, Version 2.0 (the | ||
| // "License"); you may not use this file except in compliance | ||
| // with the License. You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, | ||
| // software distributed under the License is distributed on an | ||
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| // KIND, either express or implied. See the License for the | ||
| // specific language governing permissions and limitations | ||
| // under the License. | ||
|
|
||
| package org.apache.doris.nereids.trees.expressions.functions.scalar; | ||
|
|
||
| import org.apache.doris.catalog.FunctionSignature; | ||
| import org.apache.doris.nereids.trees.expressions.Expression; | ||
| import org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature; | ||
| import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable; | ||
| import org.apache.doris.nereids.trees.expressions.shape.UnaryExpression; | ||
| import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor; | ||
| import org.apache.doris.nereids.types.JsonType; | ||
|
|
||
| import com.google.common.base.Preconditions; | ||
| import com.google.common.collect.ImmutableList; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| /** | ||
| * ScalarFunction 'json_object_flatten'. Turn a nested JSONB object into a | ||
| * single-level JSONB object whose keys are dot-joined paths to each leaf | ||
| * (NiFi FlattenJson "keep-arrays" semantics — arrays stay as opaque values): | ||
| * {"a":{"b":2}} -> {"a.b":2} | ||
| * {"a":[{"b":1}]} -> {"a":[{"b":1}]} | ||
| * To flatten a VARIANT, wrap it with `to_json`: json_object_flatten(to_json(v)). | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This usage example does not match the actual FE signature rules in this snapshot. |
||
| */ | ||
| public class JsonObjectFlatten extends ScalarFunction | ||
| implements UnaryExpression, ExplicitlyCastableSignature, PropagateNullable { | ||
|
|
||
| public static final List<FunctionSignature> SIGNATURES = ImmutableList.of( | ||
|
csun5285 marked this conversation as resolved.
|
||
| FunctionSignature.ret(JsonType.INSTANCE).args(JsonType.INSTANCE)); | ||
|
|
||
| public JsonObjectFlatten(Expression arg0) { | ||
| super("json_object_flatten", arg0); | ||
| } | ||
|
|
||
| private JsonObjectFlatten(ScalarFunctionParams functionParams) { | ||
| super(functionParams); | ||
| } | ||
|
|
||
| @Override | ||
| public JsonObjectFlatten withChildren(List<Expression> children) { | ||
| Preconditions.checkArgument(children.size() == 1); | ||
| return new JsonObjectFlatten(getFunctionParams(children)); | ||
| } | ||
|
|
||
| @Override | ||
| public List<FunctionSignature> getSignatures() { | ||
| return SIGNATURES; | ||
| } | ||
|
|
||
| @Override | ||
| public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) { | ||
| return visitor.visitJsonObjectFlatten(this, context); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| -- This file is automatically generated. You should know what you did if you want to edit this | ||
| -- !sql_jsonb -- | ||
| 1 {"a.b":2} | ||
| 2 {"a.b.c":3} | ||
| 3 {"a":1,"b":"hi"} | ||
| 4 {} | ||
| 5 {"a":{}} | ||
| 6 {"a":null} | ||
| 7 {"a.b":null} | ||
| 8 {"a.b.c.d.e.f.g.h.i.j.k.l":1} | ||
| 9 \N | ||
| 10 42 | ||
| 11 "hello" | ||
| 12 {"a":[1,2,3]} | ||
| 13 {"a":[{"b":1},{"b":2}]} | ||
| 14 {"a.b":[1,2,3]} | ||
| 15 {"a.b":[{"c":1},{"c":2}]} | ||
| 16 [1,2,{"x":3}] | ||
| 17 {"x.s":1,"x.a":[1,2],"x.o.k":"v"} | ||
|
|
||
| -- !sql_variant -- | ||
| 1 {"a.b":2} | ||
| 2 {"a.b.c":3} | ||
| 3 {"a":1,"b":"hi"} | ||
| 4 {} | ||
| 5 {} | ||
| 6 {} | ||
| 7 {} | ||
| 8 {"a.b.c.d.e.f.g.h.i.j.k.l":1} | ||
| 9 \N | ||
| 10 42 | ||
| 11 "hello" | ||
| 12 {"a":[1,2,3]} | ||
| 13 {"a":[{"b":1},{"b":2}]} | ||
| 14 {"a.b":[1,2,3]} | ||
| 15 {"a.b":[{"c":1},{"c":2}]} | ||
| 16 [1,2,{"x":3}] | ||
| 17 {"x.a":[1,2],"x.o.k":"v","x.s":1} | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This write path is missing the checked key-length handling Doris already uses when building JSONB from parsed JSON.
prefixis a synthesized dotted path, so it can exceed 255 bytes even when every input segment is individually valid under the default limit: for example, two nested 128-byte keys produce a 257-byte flat key. Withstatic_cast<uint8_t>that silently wraps and only the truncated prefix bytes are written, sojson_object_flattenreturns a corrupted object instead of preserving or rejecting the key. Please validate the flattened key length here and use the checked cast path rather than a rawstatic_cast, and add a BE test for a >255-byte dotted key.