blob: 383329a1b0af06c82415b31f9e7506462e265c62 [file]
// 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 <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "common/config.h"
#include "core/assert_cast.h"
#include "core/column/column_const.h"
#include "core/column/column_nullable.h"
#include "core/column/column_string.h"
#include "core/column/column_variant.h"
#include "core/column/variant_v2/column_variant_v2.h"
#include "core/data_type/data_type_nullable.h"
#include "core/data_type/data_type_string.h"
#include "core/data_type/data_type_variant.h"
#include "core/data_type/data_type_variant_v2.h"
#include "core/value/variant/variant_parquet_encoding.h"
#include "exprs/function/function_test_util.h"
#include "exprs/function/parse/variant_string_parse.h"
#include "exprs/function/simple_function_factory.h"
namespace doris {
namespace {
template <typename T>
class ScopedValue {
public:
ScopedValue(T& target, T value) : _target(target), _old(target) { _target = value; }
~ScopedValue() { _target = _old; }
private:
T& _target;
T _old;
};
ColumnPtr make_strings(const std::vector<std::string>& values) {
auto column = ColumnString::create();
for (const std::string& value : values) {
column->insert_data(value.data(), value.size());
}
return column;
}
ColumnPtr make_nullable_strings(const std::vector<std::optional<std::string>>& values) {
auto strings = ColumnString::create();
auto nulls = ColumnUInt8::create(values.size(), uint8_t {0});
for (size_t row = 0; row < values.size(); ++row) {
if (values[row].has_value()) {
strings->insert_data(values[row]->data(), values[row]->size());
} else {
strings->insert_default();
nulls->get_data()[row] = 1;
}
}
return ColumnNullable::create(std::move(strings), std::move(nulls));
}
struct ExecutionResult {
Status status;
ColumnPtr output;
DataTypePtr return_type;
};
ExecutionResult execute_parse(std::string_view function_name, ColumnPtr input,
const DataTypePtr& input_type, size_t rows,
DataTypePtr result_type = nullptr, bool use_variant_v2 = true) {
if (result_type == nullptr) {
if (use_variant_v2) {
result_type = std::make_shared<DataTypeVariantV2>();
} else {
result_type = std::make_shared<DataTypeVariant>();
}
if (function_name == "try_parse_to_variant" || input_type->is_nullable()) {
result_type = make_nullable(result_type);
}
}
Block block;
block.insert({std::move(input), input_type, "json"});
FunctionBasePtr function = SimpleFunctionFactory::instance().get_function(
std::string(function_name), block.get_columns_with_type_and_name(), result_type);
DORIS_CHECK(function != nullptr);
block.insert({nullptr, result_type, "result"});
FunctionUtils function_utils(result_type, {input_type}, false);
FunctionContext* context = function_utils.get_fn_ctx();
DORIS_CHECK(function->open(context, FunctionContext::FRAGMENT_LOCAL).ok());
DORIS_CHECK(function->open(context, FunctionContext::THREAD_LOCAL).ok());
Status status = function->execute(context, block, {0}, 1, rows);
static_cast<void>(function->close(context, FunctionContext::THREAD_LOCAL));
static_cast<void>(function->close(context, FunctionContext::FRAGMENT_LOCAL));
return {.status = std::move(status),
.output = block.get_by_position(1).column,
.return_type = function->get_return_type()};
}
ExecutionResult execute_parse(std::string_view function_name, ColumnPtr input,
const DataTypePtr& input_type, size_t rows, bool use_variant_v2) {
return execute_parse(function_name, std::move(input), input_type, rows, nullptr,
use_variant_v2);
}
const IColumn& physical_column(const ColumnPtr& output, size_t* row) {
DORIS_CHECK(static_cast<bool>(output));
if (const auto* constant = check_and_get_column<ColumnConst>(output.get())) {
*row = 0;
return constant->get_data_column();
}
return *output;
}
bool is_sql_null_at(const ColumnPtr& output, size_t row) {
const IColumn& physical = physical_column(output, &row);
const auto* nullable = check_and_get_column<ColumnNullable>(&physical);
return nullable != nullptr && nullable->is_null_at(row);
}
struct StringWriter {
void write(const char* data, size_t size) { value.append(data, size); }
std::string value;
};
std::string variant_json_at(const ColumnPtr& output, size_t row) {
const IColumn* physical = &physical_column(output, &row);
if (const auto* nullable = check_and_get_column<ColumnNullable>(physical)) {
DORIS_CHECK(!nullable->is_null_at(row));
physical = &nullable->get_nested_column();
}
const auto& variant = assert_cast<const ColumnVariantV2&>(*physical);
StringWriter writer;
to_json(variant.get_value_ref(row), writer, VariantJsonFormatOptions {});
return writer.value;
}
std::string nested_array_json(uint32_t depth) {
std::string json(depth, '[');
json.push_back('0');
json.append(depth, ']');
return json;
}
} // namespace
TEST(FunctionVariantParseTest, ExecutionTypeSelectsPhysicalColumn) {
const DataTypePtr string_type = std::make_shared<DataTypeString>();
ExecutionResult legacy =
execute_parse("parse_to_variant", make_strings({R"({"a":1})"}), string_type, 1, false);
ASSERT_TRUE(legacy.status.ok()) << legacy.status.to_string();
EXPECT_NE(check_and_get_column_with_const<ColumnVariant>(*legacy.output), nullptr);
ExecutionResult v2 =
execute_parse("parse_to_variant", make_strings({R"({"a":1})"}), string_type, 1, true);
ASSERT_TRUE(v2.status.ok()) << v2.status.to_string();
EXPECT_NE(check_and_get_column_with_const<ColumnVariantV2>(*v2.output), nullptr);
}
TEST(FunctionVariantParseTest, LegacyPathPreservesSqlNullAndErrorToNull) {
const DataTypePtr nullable_string_type = make_nullable(std::make_shared<DataTypeString>());
ExecutionResult nullable =
execute_parse("parse_to_variant",
make_nullable_strings({std::nullopt, std::string(R"({"value":1})")}),
nullable_string_type, 2, false);
ASSERT_TRUE(nullable.status.ok()) << nullable.status.to_string();
EXPECT_TRUE(is_sql_null_at(nullable.output, 0));
const auto& nullable_output = assert_cast<const ColumnNullable&>(*nullable.output);
EXPECT_NE(check_and_get_column<ColumnVariant>(&nullable_output.get_nested_column()), nullptr);
const DataTypePtr string_type = std::make_shared<DataTypeString>();
const std::string invalid_utf8(1, static_cast<char>(0xFF));
ScopedValue strict(config::variant_throw_exeception_on_invalid_json, true);
ExecutionResult failure = execute_parse(
"parse_to_variant", make_strings({R"({"before":1})", invalid_utf8, R"({"after":2})"}),
string_type, 3, false);
EXPECT_FALSE(failure.status.ok());
EXPECT_FALSE(static_cast<bool>(failure.output));
ExecutionResult recoverable =
execute_parse("try_parse_to_variant",
make_strings({R"({"before":1})", invalid_utf8, R"({"after":2})"}),
string_type, 3, false);
ASSERT_TRUE(recoverable.status.ok()) << recoverable.status.to_string();
EXPECT_FALSE(is_sql_null_at(recoverable.output, 0));
EXPECT_TRUE(is_sql_null_at(recoverable.output, 1));
EXPECT_FALSE(is_sql_null_at(recoverable.output, 2));
const auto& recoverable_output = assert_cast<const ColumnNullable&>(*recoverable.output);
EXPECT_NE(check_and_get_column<ColumnVariant>(&recoverable_output.get_nested_column()),
nullptr);
}
TEST(FunctionVariantParseTest, FunctionsAreRegistered) {
const DataTypePtr argument_type = make_nullable(std::make_shared<DataTypeString>());
const DataTypePtr result_type = make_nullable(std::make_shared<DataTypeVariant>());
const ColumnsWithTypeAndName arguments {
{argument_type->create_column(), argument_type, "json"}};
EXPECT_NE(SimpleFunctionFactory::instance().get_function("parse_to_variant", arguments,
result_type),
nullptr);
EXPECT_NE(SimpleFunctionFactory::instance().get_function("try_parse_to_variant", arguments,
result_type),
nullptr);
}
TEST(FunctionVariantParseTest, ConfiguredVariantReturnTypeBuildsAndExecutes) {
const DataTypePtr string_type = std::make_shared<DataTypeString>();
const DataTypePtr legacy_doc_mode_variant = std::make_shared<DataTypeVariant>(0, true);
ExecutionResult legacy_doc_mode =
execute_parse("parse_to_variant", make_strings({R"({"a":1})"}), string_type, 1,
legacy_doc_mode_variant);
ASSERT_TRUE(legacy_doc_mode.status.ok()) << legacy_doc_mode.status.to_string();
const auto& legacy_doc_mode_column = assert_cast<const ColumnVariant&>(*legacy_doc_mode.output);
EXPECT_TRUE(legacy_doc_mode_column.enable_doc_mode());
EXPECT_EQ(legacy_doc_mode_column.get_subcolumns().size(), 1);
ASSERT_EQ(legacy_doc_mode_column.serialized_doc_value_column_offsets().size(), 1);
EXPECT_EQ(legacy_doc_mode_column.serialized_doc_value_column_offsets().back(), 1);
const DataTypePtr max_subcolumns_variant = std::make_shared<DataTypeVariantV2>(2048, false);
ExecutionResult result = execute_parse("parse_to_variant", make_strings({R"({"a":1})"}),
string_type, 1, max_subcolumns_variant);
ASSERT_TRUE(result.status.ok()) << result.status.to_string();
ASSERT_TRUE(static_cast<bool>(result.output));
ASSERT_TRUE(static_cast<bool>(result.return_type));
EXPECT_TRUE(result.return_type->equals(*max_subcolumns_variant));
EXPECT_NE(check_and_get_column<ColumnVariantV2>(result.output.get()), nullptr);
EXPECT_EQ(variant_json_at(result.output, 0), R"({"a":1})");
const DataTypePtr nullable_string_type = make_nullable(std::make_shared<DataTypeString>());
const DataTypePtr nullable_doc_mode_variant =
make_nullable(std::make_shared<DataTypeVariantV2>(0, true));
ExecutionResult nullable = execute_parse(
"parse_to_variant", make_nullable_strings({std::nullopt, std::string(R"([1,2])")}),
nullable_string_type, 2, nullable_doc_mode_variant);
ASSERT_TRUE(nullable.status.ok()) << nullable.status.to_string();
ASSERT_TRUE(static_cast<bool>(nullable.return_type));
EXPECT_TRUE(nullable.return_type->equals(*nullable_doc_mode_variant));
EXPECT_TRUE(is_sql_null_at(nullable.output, 0));
EXPECT_EQ(variant_json_at(nullable.output, 1), "[1,2]");
const DataTypePtr nullable_max_subcolumns_variant =
make_nullable(std::make_shared<DataTypeVariantV2>(2048, false));
ExecutionResult error_to_null = execute_parse("try_parse_to_variant", make_strings({"true"}),
string_type, 1, nullable_max_subcolumns_variant);
ASSERT_TRUE(error_to_null.status.ok()) << error_to_null.status.to_string();
ASSERT_TRUE(static_cast<bool>(error_to_null.return_type));
EXPECT_TRUE(error_to_null.return_type->equals(*nullable_max_subcolumns_variant));
EXPECT_FALSE(is_sql_null_at(error_to_null.output, 0));
EXPECT_EQ(variant_json_at(error_to_null.output, 0), "true");
}
TEST(FunctionVariantParseTest, DistinguishesSqlNullJsonNullEmptyAndConst) {
const DataTypePtr string_type = std::make_shared<DataTypeString>();
ExecutionResult values = execute_parse(
"parse_to_variant", make_strings({R"({"a":1})", "null", ""}), string_type, 3);
ASSERT_TRUE(values.status.ok()) << values.status.to_string();
ASSERT_EQ(values.output->size(), 3);
EXPECT_FALSE(is_sql_null_at(values.output, 0));
EXPECT_FALSE(is_sql_null_at(values.output, 1));
EXPECT_EQ(variant_json_at(values.output, 0), R"({"a":1})");
EXPECT_EQ(variant_json_at(values.output, 1), "null");
EXPECT_EQ(variant_json_at(values.output, 2), "{}");
const DataTypePtr nullable_string_type = make_nullable(std::make_shared<DataTypeString>());
ExecutionResult nullable = execute_parse(
"parse_to_variant",
make_nullable_strings({std::nullopt, std::string("null"), std::string(R"({"b":2})")}),
nullable_string_type, 3);
ASSERT_TRUE(nullable.status.ok()) << nullable.status.to_string();
EXPECT_TRUE(is_sql_null_at(nullable.output, 0));
EXPECT_FALSE(is_sql_null_at(nullable.output, 1));
EXPECT_EQ(variant_json_at(nullable.output, 1), "null");
EXPECT_EQ(variant_json_at(nullable.output, 2), R"({"b":2})");
ColumnPtr constant = ColumnConst::create(make_strings({R"([1,2])"}), 4);
ExecutionResult constant_result =
execute_parse("parse_to_variant", std::move(constant), string_type, 4);
ASSERT_TRUE(constant_result.status.ok()) << constant_result.status.to_string();
ASSERT_TRUE(is_column_const(*constant_result.output));
ASSERT_EQ(constant_result.output->size(), 4);
EXPECT_EQ(variant_json_at(constant_result.output, 3), "[1,2]");
}
TEST(FunctionVariantParseTest, StrictFailureDoesNotPublishPartialBatch) {
ScopedValue strict(config::variant_throw_exeception_on_invalid_json, true);
const DataTypePtr string_type = std::make_shared<DataTypeString>();
ExecutionResult result =
execute_parse("parse_to_variant",
make_strings({R"({"before":1})", "{", R"({"after":2})"}), string_type, 3);
EXPECT_FALSE(result.status.ok());
EXPECT_EQ(result.status.code(), ErrorCode::INVALID_ARGUMENT) << result.status.to_string();
EXPECT_NE(result.status.to_string().find("Parse json document failed at row 1, error: "),
std::string::npos)
<< result.status.to_string();
EXPECT_NE(result.status.to_string().find("Failed to parse JSON as Variant"), std::string::npos)
<< result.status.to_string();
EXPECT_FALSE(static_cast<bool>(result.output));
}
TEST(FunctionVariantParseTest, ErrorToNullOnlyNullsRecoverableFailures) {
const DataTypePtr string_type = std::make_shared<DataTypeString>();
{
ScopedValue strict(config::variant_throw_exeception_on_invalid_json, true);
ExecutionResult result = execute_parse(
"try_parse_to_variant",
make_strings({R"({"before":1})", "{", "null", R"({"after":2})"}), string_type, 4);
ASSERT_TRUE(result.status.ok()) << result.status.to_string();
EXPECT_FALSE(is_sql_null_at(result.output, 0));
EXPECT_TRUE(is_sql_null_at(result.output, 1));
EXPECT_FALSE(is_sql_null_at(result.output, 2));
EXPECT_EQ(variant_json_at(result.output, 2), "null");
EXPECT_EQ(variant_json_at(result.output, 3), R"({"after":2})");
}
{
ScopedValue permissive(config::variant_throw_exeception_on_invalid_json, false);
ExecutionResult result =
execute_parse("try_parse_to_variant", make_strings({"{"}), string_type, 1);
ASSERT_TRUE(result.status.ok()) << result.status.to_string();
EXPECT_FALSE(is_sql_null_at(result.output, 0));
EXPECT_EQ(variant_json_at(result.output, 0), R"("{")");
}
}
TEST(FunctionVariantParseTest, ConfiguredInputValidationUsesFailOrOuterNull) {
const DataTypePtr string_type = std::make_shared<DataTypeString>();
{
ScopedValue key_limit(config::variant_max_json_key_length, 3);
ExecutionResult fail =
execute_parse("parse_to_variant", make_strings({R"({"abcd":1})"}), string_type, 1);
EXPECT_FALSE(fail.status.ok());
EXPECT_FALSE(static_cast<bool>(fail.output));
ExecutionResult null = execute_parse("try_parse_to_variant",
make_strings({R"({"abcd":1})"}), string_type, 1);
ASSERT_TRUE(null.status.ok()) << null.status.to_string();
EXPECT_TRUE(is_sql_null_at(null.output, 0));
}
{
ScopedValue reject_duplicates(config::variant_enable_duplicate_json_path_check, false);
ExecutionResult fail = execute_parse("parse_to_variant", make_strings({R"({"a":1,"a":2})"}),
string_type, 1);
EXPECT_FALSE(fail.status.ok());
ExecutionResult null = execute_parse("try_parse_to_variant",
make_strings({R"({"a":1,"a":2})"}), string_type, 1);
ASSERT_TRUE(null.status.ok()) << null.status.to_string();
EXPECT_TRUE(is_sql_null_at(null.output, 0));
}
{
ScopedValue keep_first(config::variant_enable_duplicate_json_path_check, true);
ExecutionResult result = execute_parse("try_parse_to_variant",
make_strings({R"({"a":1,"a":2})"}), string_type, 1);
ASSERT_TRUE(result.status.ok()) << result.status.to_string();
EXPECT_FALSE(is_sql_null_at(result.output, 0));
EXPECT_EQ(variant_json_at(result.output, 0), R"({"a":1})");
}
{
ScopedValue permissive(config::variant_throw_exeception_on_invalid_json, false);
const std::string invalid_utf8(1, static_cast<char>(0xFF));
ExecutionResult fail =
execute_parse("parse_to_variant", make_strings({invalid_utf8}), string_type, 1);
EXPECT_FALSE(fail.status.ok());
ExecutionResult null =
execute_parse("try_parse_to_variant", make_strings({invalid_utf8}), string_type, 1);
ASSERT_TRUE(null.status.ok()) << null.status.to_string();
EXPECT_TRUE(is_sql_null_at(null.output, 0));
}
{
const std::string too_deep = nested_array_json(VARIANT_MAX_NESTING_DEPTH + 1);
ExecutionResult fail =
execute_parse("parse_to_variant", make_strings({too_deep}), string_type, 1);
EXPECT_FALSE(fail.status.ok());
ExecutionResult null =
execute_parse("try_parse_to_variant", make_strings({too_deep}), string_type, 1);
ASSERT_TRUE(null.status.ok()) << null.status.to_string();
EXPECT_TRUE(is_sql_null_at(null.output, 0));
}
}
} // namespace doris