blob: dedb3bc94eaa4629a5e8b6fcb3f5f9b83619b4e7 [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 "paimon/core/schema/table_schema.h"
#include <utility>
#include "arrow/api.h"
#include "arrow/util/checked_cast.h"
#include "gtest/gtest.h"
#include "paimon/common/utils/date_time_utils.h"
#include "paimon/common/utils/string_utils.h"
#include "paimon/fs/local/local_file_system.h"
#include "paimon/status.h"
#include "paimon/testing/utils/testharness.h"
namespace paimon::test {
class TableSchemaTest : public ::testing::Test {
public:
arrow::FieldVector MakeArrowField(int32_t id) const {
arrow::FieldVector arrow_fields;
arrow_fields.reserve(5);
auto meta =
arrow::KeyValueMetadata::Make({std::string(DataField::FIELD_ID)}, {std::to_string(id)});
arrow_fields.push_back(std::make_shared<arrow::Field>("sub1", arrow::date32(),
/*nullable=*/true, meta));
meta = arrow::KeyValueMetadata::Make({std::string(DataField::FIELD_ID)},
{std::to_string(id + 1)});
arrow_fields.push_back(
std::make_shared<arrow::Field>("sub2", arrow::timestamp(arrow::TimeUnit::type::NANO),
/*nullable=*/true, meta));
meta = arrow::KeyValueMetadata::Make({std::string(DataField::FIELD_ID)},
{std::to_string(id + 2)});
arrow_fields.push_back(std::make_shared<arrow::Field>("sub3", arrow::decimal128(23, 5),
/*nullable=*/true, meta));
meta = arrow::KeyValueMetadata::Make({std::string(DataField::FIELD_ID)},
{std::to_string(id + 3)});
arrow_fields.push_back(std::make_shared<arrow::Field>("sub4", arrow::binary(),
/*nullable=*/true, meta));
meta = arrow::KeyValueMetadata::Make({std::string(DataField::FIELD_ID)},
{std::to_string(id + 4)});
arrow_fields.push_back(std::make_shared<arrow::Field>("sub5", arrow::binary(),
/*nullable=*/true, meta));
return arrow_fields;
}
std::string ReplaceAll(const std::string& str) {
std::string replaced_str = StringUtils::Replace(str, " ", "");
replaced_str = StringUtils::Replace(replaced_str, "\t", "");
replaced_str = StringUtils::Replace(replaced_str, "\n", "");
return replaced_str;
}
};
TEST_F(TableSchemaTest, TestCreateWithAllFieldsNotHaveFieldId) {
arrow::FieldVector fields = {
arrow::field("f0", arrow::boolean()), arrow::field("f1", arrow::int8()),
arrow::field("f2", arrow::int8()), arrow::field("f3", arrow::int16()),
arrow::field("f4", arrow::int16()), arrow::field("f5", arrow::int32()),
arrow::field("f6", arrow::int32()), arrow::field("f7", arrow::int64()),
arrow::field("f8", arrow::int64()), arrow::field("f9", arrow::float32()),
arrow::field("f10", arrow::float64()), arrow::field("f11", arrow::utf8()),
arrow::field("f12", arrow::binary()), arrow::field("non-partition-field", arrow::int32())};
auto schema = arrow::schema(fields);
std::vector<std::string> partition_keys = {"f1", "f2"};
std::vector<std::string> primary_keys = {"f3", "f4"};
std::map<std::string, std::string> options;
ASSERT_OK_AND_ASSIGN(
auto table_schema,
TableSchema::Create(/*schema_id=*/0, schema, partition_keys, primary_keys, options));
ASSERT_TRUE(table_schema);
std::vector<DataField> expected_data_fields = {
DataField(0, arrow::field("f0", arrow::boolean())),
DataField(1, arrow::field("f1", arrow::int8())),
DataField(2, arrow::field("f2", arrow::int8())),
DataField(3, arrow::field("f3", arrow::int16(), /*nullable=*/false)),
DataField(4, arrow::field("f4", arrow::int16(), /*nullable=*/false)),
DataField(5, arrow::field("f5", arrow::int32())),
DataField(6, arrow::field("f6", arrow::int32())),
DataField(7, arrow::field("f7", arrow::int64())),
DataField(8, arrow::field("f8", arrow::int64())),
DataField(9, arrow::field("f9", arrow::float32())),
DataField(10, arrow::field("f10", arrow::float64())),
DataField(11, arrow::field("f11", arrow::utf8())),
DataField(12, arrow::field("f12", arrow::binary())),
DataField(13, arrow::field("non-partition-field", arrow::int32()))};
ASSERT_EQ(table_schema->Fields(), expected_data_fields);
ASSERT_EQ(table_schema->Id(), 0);
ASSERT_EQ(table_schema->HighestFieldId(), 13);
ASSERT_EQ(table_schema->PrimaryKeys(), primary_keys);
ASSERT_EQ(table_schema->PartitionKeys(), partition_keys);
ASSERT_EQ(table_schema->Options(), options);
}
TEST_F(TableSchemaTest, TestCreateWithAllFieldsHaveFieldId) {
arrow::FieldVector fields = MakeArrowField(0);
auto schema = arrow::schema(fields);
std::vector<std::string> partition_keys = {"sub1", "sub2"};
std::vector<std::string> primary_keys = {"sub3", "sub4"};
std::map<std::string, std::string> options;
ASSERT_OK_AND_ASSIGN(
auto table_schema,
TableSchema::Create(/*schema_id=*/0, schema, partition_keys, primary_keys, options));
ASSERT_TRUE(table_schema);
std::vector<DataField> expected_data_fields = {
DataField(0, arrow::field("sub1", arrow::date32())),
DataField(1, arrow::field("sub2", arrow::timestamp(arrow::TimeUnit::type::NANO))),
DataField(2, arrow::field("sub3", arrow::decimal128(23, 5), /*nullable=*/false)),
DataField(3, arrow::field("sub4", arrow::binary(), /*nullable=*/false)),
DataField(4, arrow::field("sub5", arrow::binary()))};
ASSERT_EQ(table_schema->Fields(), expected_data_fields);
ASSERT_EQ(table_schema->Id(), 0);
ASSERT_EQ(table_schema->HighestFieldId(), 4);
ASSERT_EQ(table_schema->PrimaryKeys(), primary_keys);
ASSERT_EQ(table_schema->PartitionKeys(), partition_keys);
ASSERT_EQ(table_schema->Options(), options);
}
TEST_F(TableSchemaTest, TestInvalidCreate) {
// partial fields have field id
arrow::FieldVector fields = MakeArrowField(3);
fields.push_back(arrow::field("f0", arrow::boolean()));
auto schema = arrow::schema(fields);
std::vector<std::string> partition_keys = {"sub1", "sub2"};
std::vector<std::string> primary_keys = {"sub3", "sub4"};
std::map<std::string, std::string> options;
ASSERT_NOK(TableSchema::Create(/*schema_id=*/1, schema, partition_keys, primary_keys, options));
}
TEST_F(TableSchemaTest, TestDeserializeAppendTableSchema) {
auto fs = std::make_shared<LocalFileSystem>();
std::string path = paimon::test::GetDataDir() + "/orc/append_09.db/append_09/schema/schema-0";
std::string content;
ASSERT_OK(fs->ReadFile(path, &content));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableSchema> result_schema,
TableSchema::CreateFromJson(content));
TableSchema expected_schema;
expected_schema.version_ = 3;
expected_schema.id_ = 0;
auto field1 = DataField(0, arrow::field("f0", arrow::utf8()));
auto field2 = DataField(1, arrow::field("f1", arrow::int32()));
auto field3 = DataField(2, arrow::field("f2", arrow::int32()));
auto field4 = DataField(3, arrow::field("f3", arrow::float64()));
expected_schema.fields_ = {field1, field2, field3, field4};
expected_schema.highest_field_id_ = 3;
expected_schema.partition_keys_ = {"f1"};
expected_schema.primary_keys_ = {};
expected_schema.options_ = {
{"bucket", "2"}, {"bucket-key", "f2"}, {"manifest.format", "orc"}, {"file.format", "orc"}};
expected_schema.time_millis_ = 1721614341162;
ASSERT_EQ(*result_schema, expected_schema);
ASSERT_EQ(std::vector<std::string>({"f0", "f1", "f2", "f3"}), result_schema->FieldNames());
ASSERT_EQ(field2, result_schema->GetField("f1").value());
ASSERT_EQ(field2, result_schema->GetField(1).value());
ASSERT_NOK_WITH_MSG(result_schema->GetField("non-exist"),
"Get field non-exist failed: not exist in table schema");
ASSERT_NOK_WITH_MSG(result_schema->GetField(100),
"Get field with id 100 failed: not exist in table schema");
}
TEST_F(TableSchemaTest, TestDeserializePkTableSchema) {
auto fs = std::make_shared<LocalFileSystem>();
std::string path = paimon::test::GetDataDir() + "/orc/pk_09.db/pk_09/schema/schema-0";
std::string content;
ASSERT_OK(fs->ReadFile(path, &content));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableSchema> result_schema,
TableSchema::CreateFromJson(content));
TableSchema expected_schema;
expected_schema.version_ = 3;
expected_schema.id_ = 0;
auto field1 = DataField(0, arrow::field("f0", arrow::utf8(), /*nullable=*/false));
auto field2 = DataField(1, arrow::field("f1", arrow::int32(), /*nullable=*/false));
auto field3 = DataField(2, arrow::field("f2", arrow::int32(), /*nullable=*/false));
auto field4 = DataField(3, arrow::field("f3", arrow::float64(), /*nullable=*/true));
expected_schema.fields_ = {field1, field2, field3, field4};
expected_schema.highest_field_id_ = 3;
expected_schema.partition_keys_ = {"f1"};
expected_schema.primary_keys_ = {"f0", "f1", "f2"};
expected_schema.options_ = {{"bucket", "2"},
{"bucket-key", "f2"},
{"manifest.format", "orc"},
{"file.format", "orc"},
{"deletion-vectors.enabled", "true"},
{"commit.force-compact", "true"}};
expected_schema.time_millis_ = 1725534144802;
ASSERT_EQ(*result_schema, expected_schema);
ASSERT_EQ(std::vector<std::string>({"f0", "f1", "f2", "f3"}), result_schema->FieldNames());
ASSERT_OK_AND_ASSIGN(auto trimmed_primary_keys, result_schema->TrimmedPrimaryKeys());
ASSERT_EQ(trimmed_primary_keys, std::vector<std::string>({"f0", "f2"}));
ASSERT_OK_AND_ASSIGN(std::vector<DataField> trimmed_primary_key_fields,
result_schema->GetFields(trimmed_primary_keys));
ASSERT_EQ(2, trimmed_primary_key_fields.size());
ASSERT_EQ(trimmed_primary_key_fields[0], field1);
ASSERT_EQ(trimmed_primary_key_fields[1], field3);
}
TEST_F(TableSchemaTest, TestTrimmedPrimaryKeyInterfaces) {
arrow::FieldVector fields = {
arrow::field("pt", arrow::utf8()),
arrow::field("id", arrow::int64()),
arrow::field("val", arrow::int32()),
};
auto schema = arrow::schema(fields);
ASSERT_OK_AND_ASSIGN(auto table_schema, TableSchema::Create(/*schema_id=*/0, schema,
/*partition_keys=*/{"pt"},
/*primary_keys=*/{"pt", "id"},
/*options=*/{}));
ASSERT_OK_AND_ASSIGN(std::vector<std::string> trimmed_primary_keys,
table_schema->TrimmedPrimaryKeys());
ASSERT_EQ(trimmed_primary_keys, std::vector<std::string>({"id"}));
ASSERT_OK_AND_ASSIGN(std::vector<DataField> trimmed_primary_key_fields,
table_schema->TrimmedPrimaryKeyFields());
ASSERT_EQ(trimmed_primary_key_fields.size(), 1);
ASSERT_EQ(trimmed_primary_key_fields[0].Name(), "id");
ASSERT_FALSE(trimmed_primary_key_fields[0].Nullable());
ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Schema> trimmed_primary_key_schema,
table_schema->TrimmedPrimaryKeySchema());
ASSERT_EQ(trimmed_primary_key_schema->num_fields(), 1);
ASSERT_EQ(trimmed_primary_key_schema->field(0)->name(), "id");
ASSERT_FALSE(trimmed_primary_key_schema->field(0)->nullable());
}
TEST_F(TableSchemaTest, TestTrimmedPrimaryKeyInterfacesWithoutPrimaryKeys) {
arrow::FieldVector fields = {
arrow::field("pt", arrow::utf8()),
arrow::field("val", arrow::int32()),
};
auto schema = arrow::schema(fields);
ASSERT_OK_AND_ASSIGN(auto table_schema, TableSchema::Create(/*schema_id=*/0, schema,
/*partition_keys=*/{"pt"},
/*primary_keys=*/{},
/*options=*/{}));
ASSERT_OK_AND_ASSIGN(std::vector<std::string> trimmed_primary_keys,
table_schema->TrimmedPrimaryKeys());
ASSERT_TRUE(trimmed_primary_keys.empty());
ASSERT_OK_AND_ASSIGN(std::vector<DataField> trimmed_primary_key_fields,
table_schema->TrimmedPrimaryKeyFields());
ASSERT_TRUE(trimmed_primary_key_fields.empty());
ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Schema> trimmed_primary_key_schema,
table_schema->TrimmedPrimaryKeySchema());
ASSERT_EQ(trimmed_primary_key_schema->num_fields(), 0);
}
TEST_F(TableSchemaTest, TestDeserializeAppendTableSchemaWithTimestamp) {
auto fs = std::make_shared<LocalFileSystem>();
std::string path = paimon::test::GetDataDir() +
"/orc/append_with_multiple_ts_precision_and_timezone.db/"
"append_with_multiple_ts_precision_and_timezone/schema/schema-0";
std::string content;
ASSERT_OK(fs->ReadFile(path, &content));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableSchema> result_schema,
TableSchema::CreateFromJson(content));
TableSchema expected_schema;
expected_schema.version_ = 3;
expected_schema.id_ = 0;
auto timezone = DateTimeUtils::GetLocalTimezoneName();
std::vector<DataField> fields = {
DataField(0, arrow::field("ts_sec", arrow::timestamp(arrow::TimeUnit::SECOND))),
DataField(1, arrow::field("ts_milli", arrow::timestamp(arrow::TimeUnit::MILLI))),
DataField(2, arrow::field("ts_micro", arrow::timestamp(arrow::TimeUnit::MICRO))),
DataField(3, arrow::field("ts_nano", arrow::timestamp(arrow::TimeUnit::NANO))),
DataField(4,
arrow::field("ts_tz_sec", arrow::timestamp(arrow::TimeUnit::SECOND, timezone))),
DataField(5,
arrow::field("ts_tz_milli", arrow::timestamp(arrow::TimeUnit::MILLI, timezone))),
DataField(6,
arrow::field("ts_tz_micro", arrow::timestamp(arrow::TimeUnit::MICRO, timezone))),
DataField(7,
arrow::field("ts_tz_nano", arrow::timestamp(arrow::TimeUnit::NANO, timezone)))};
expected_schema.fields_ = fields;
expected_schema.highest_field_id_ = 7;
expected_schema.partition_keys_ = {};
expected_schema.primary_keys_ = {};
expected_schema.options_ = {{"bucket", "-1"},
{"manifest.format", "orc"},
{"file.format", "orc"},
{"orc.timestamp-ltz.legacy.type", "false"}};
expected_schema.time_millis_ = 1757927622210;
ASSERT_EQ(*result_schema, expected_schema);
ASSERT_EQ(std::vector<std::string>({"ts_sec", "ts_milli", "ts_micro", "ts_nano", "ts_tz_sec",
"ts_tz_milli", "ts_tz_micro", "ts_tz_nano"}),
result_schema->FieldNames());
}
TEST_F(TableSchemaTest, TestDeserializeWithNestedDataType) {
auto fs = std::make_shared<LocalFileSystem>();
std::string path = paimon::test::GetDataDir() +
"/orc/append_table_with_nested_type.db/append_table_with_nested_type/schema/"
"schema-0";
std::string content;
ASSERT_OK(fs->ReadFile(path, &content));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableSchema> result_schema,
TableSchema::CreateFromJson(content));
TableSchema expected_schema;
expected_schema.version_ = 3;
expected_schema.id_ = 0;
auto field1 = DataField(0, arrow::field("f0", arrow::struct_(MakeArrowField(1))));
auto field2 = DataField(6, arrow::field("f1", arrow::list(arrow::struct_(MakeArrowField(7)))));
auto field3 = DataField(12, arrow::field("f2", arrow::map(arrow::struct_(MakeArrowField(13)),
arrow::struct_(MakeArrowField(18)))));
expected_schema.fields_ = {field1, field2, field3};
expected_schema.highest_field_id_ = 22;
expected_schema.partition_keys_ = {};
expected_schema.primary_keys_ = {};
expected_schema.options_ = {{"manifest.format", "orc"}, {"file.format", "orc"}};
expected_schema.time_millis_ = 1729759141146;
ASSERT_EQ(*result_schema, expected_schema);
ASSERT_EQ(std::vector<std::string>({"f0", "f1", "f2"}), result_schema->FieldNames());
}
TEST_F(TableSchemaTest, TestEmptyBucketKey) {
// empty bucket key, will use trimmed pk as bucket key
{
std::string table_schema_str = R"({
"version" : 3,
"id" : 0,
"fields" : [ {
"id" : 0,
"name" : "f0",
"type" : "STRING"
}, {
"id" : 1,
"name" : "f1",
"type" : "INT"
}, {
"id" : 2,
"name" : "f2",
"type" : "INT"
}, {
"id" : 3,
"name" : "f3",
"type" : "DOUBLE"
} ],
"highestFieldId" : 3,
"partitionKeys" : [ "f1"],
"primaryKeys" : [ "f1", "f2" ],
"options" : {
"bucket" : "2",
"bucket-key" : "",
"manifest.format" : "orc",
"file.format" : "orc"
},
"timeMillis" : 1721614341162
})";
ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableSchema> schema_result,
TableSchema::CreateFromJson(table_schema_str));
ASSERT_EQ(std::vector<std::string>({"f2"}), schema_result->bucket_keys_);
}
{
std::string table_schema_str = R"({
"version" : 3,
"id" : 0,
"fields" : [ {
"id" : 0,
"name" : "f0",
"type" : "STRING"
}, {
"id" : 1,
"name" : "f1",
"type" : "INT"
}, {
"id" : 2,
"name" : "f2",
"type" : "INT"
}, {
"id" : 3,
"name" : "f3",
"type" : "DOUBLE"
} ],
"highestFieldId" : 3,
"partitionKeys" : [ "f1"],
"primaryKeys" : [ "f1", "f2" ],
"options" : {
"bucket" : "2",
"manifest.format" : "orc",
"file.format" : "orc"
},
"timeMillis" : 1721614341162
})";
ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableSchema> schema_result,
TableSchema::CreateFromJson(table_schema_str));
ASSERT_EQ(std::vector<std::string>({"f2"}), schema_result->bucket_keys_);
}
}
TEST_F(TableSchemaTest, TestToJson) {
auto field1 = DataField(0, arrow::field("f0", arrow::utf8()));
auto field2 = DataField(1, arrow::field("f1", arrow::int32()));
auto field3 = DataField(2, arrow::field("f2", arrow::int32()));
auto field4 = DataField(3, arrow::field("f3", arrow::float64()));
auto data_fields = {field1, field2, field3, field4};
auto arrow_schema = DataField::ConvertDataFieldsToArrowSchema(data_fields);
ASSERT_OK_AND_ASSIGN(
auto schema, TableSchema::Create(/*schema_id=*/0, arrow_schema, /*partition_keys=*/{"f1"},
/*primary_keys=*/{},
{{"manifest.format", "orc"}, {"file.format", "orc"}}));
schema->time_millis_ = 1721614341162;
ASSERT_OK_AND_ASSIGN(std::string json_str, schema->ToJsonString());
std::string table_schema_str = R"({
"version": 3,
"id": 0,
"fields": [
{
"id": 0,
"name": "f0",
"type": "STRING"
},
{
"id": 1,
"name": "f1",
"type": "INT"
},
{
"id": 2,
"name": "f2",
"type": "INT"
},
{
"id": 3,
"name": "f3",
"type": "DOUBLE"
}
],
"highestFieldId": 3,
"partitionKeys": [
"f1"
],
"primaryKeys": [],
"options": {
"file.format": "orc",
"manifest.format": "orc"
},
"timeMillis": 1721614341162
})";
ASSERT_EQ(ReplaceAll(table_schema_str), ReplaceAll(json_str));
}
TEST_F(TableSchemaTest, TestToJson2) {
std::string table_schema_str = R"({
"version" : 3,
"id" : 0,
"fields" : [ {
"id" : 0,
"name" : "f0",
"type" : "STRING"
}, {
"id" : 1,
"name" : "f1",
"type" : "INT"
}, {
"id" : 2,
"name" : "f2",
"type" : "INT"
}, {
"id" : 3,
"name" : "f3",
"type" : "DOUBLE"
} ],
"highestFieldId" : 3,
"partitionKeys" : [ "f1"],
"primaryKeys" : [ "f1", "f2" ],
"options" : {
"bucket" : "2",
"file.format" : "orc",
"manifest.format" : "orc"
},
"comment" : "this is a comment",
"timeMillis" : 1721614341162
})";
ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableSchema> schema_result,
TableSchema::CreateFromJson(table_schema_str));
ASSERT_OK_AND_ASSIGN(std::string json_str, schema_result->ToJsonString());
ASSERT_EQ(ReplaceAll(table_schema_str), ReplaceAll(json_str));
}
TEST_F(TableSchemaTest, TestToJson3) {
auto fs = std::make_shared<LocalFileSystem>();
std::string path =
paimon::test::GetDataDir() +
"/orc/append_complex_build_in_fieldid.db/append_complex_build_in_fieldid/schema/"
"schema-0";
std::string content;
ASSERT_OK(fs->ReadFile(path, &content));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableSchema> st, TableSchema::CreateFromJson(content));
ASSERT_OK_AND_ASSIGN(std::string json_str, st->ToJsonString());
std::string expected_str = R"==({
"version": 3,
"id": 0,
"fields": [
{
"id": 0,
"name": "f1",
"type": {
"type": "MAP",
"key": "TINYINT NOT NULL",
"value": "SMALLINT"
}
},
{
"id": 1,
"name": "f2",
"type": {
"type": "ARRAY",
"element": "FLOAT"
}
},
{
"id": 2,
"name": "f3",
"type": {
"type": "ROW",
"fields": [
{
"id": 3,
"name": "f0",
"type": "BOOLEAN"
},
{
"id": 4,
"name": "f1",
"type": "BIGINT"
}
]
}
},
{
"id": 5,
"name": "f4",
"type": "TIMESTAMP(9)"
},
{
"id": 6,
"name": "f5",
"type": "DATE"
},
{
"id": 7,
"name": "f6",
"type": "DECIMAL(2, 2)"
}
],
"highestFieldId": 7,
"partitionKeys": [],
"primaryKeys": [],
"options": {
"file.format": "orc",
"manifest.format": "orc"
},
"timeMillis": 1732079677062
})==";
ASSERT_EQ(ReplaceAll(expected_str), ReplaceAll(json_str));
}
TEST_F(TableSchemaTest, TestToJsonWithNestedField0) {
auto map_type = arrow::map(arrow::int8(), arrow::int16());
auto list_type = arrow::list(DataField::ConvertDataFieldToArrowField(
DataField(536871936, arrow::field("item", arrow::float32()))));
std::vector<DataField> struct_fields = {DataField(3, arrow::field("f0", arrow::boolean())),
DataField(4, arrow::field("f1", arrow::int64()))};
auto struct_type = DataField::ConvertDataFieldsToArrowStructType(struct_fields);
auto data_fields = {DataField(0, arrow::field("f1", map_type)),
DataField(1, arrow::field("f2", list_type)),
DataField(2, arrow::field("f3", struct_type)),
DataField(5, arrow::field("f4", arrow::timestamp(arrow::TimeUnit::NANO))),
DataField(6, arrow::field("f5", arrow::date32())),
DataField(7, arrow::field("f6", arrow::decimal128(2, 2)))};
auto arrow_schema = DataField::ConvertDataFieldsToArrowSchema(data_fields);
ASSERT_OK_AND_ASSIGN(auto schema,
TableSchema::Create(/*schema_id=*/0, arrow_schema, /*partition_keys=*/{},
/*primary_keys=*/{},
{{"manifest.format", "orc"}, {"file.format", "orc"}}));
schema->time_millis_ = 1721614341162;
ASSERT_OK_AND_ASSIGN(std::string json_str, schema->ToJsonString());
std::string expected_schema = R"==({
"version": 3,
"id": 0,
"fields": [
{
"id": 0,
"name": "f1",
"type": {
"type": "MAP",
"key": "TINYINT NOT NULL",
"value": "SMALLINT"
}
},
{
"id": 1,
"name": "f2",
"type": {
"type": "ARRAY",
"element": "FLOAT"
}
},
{
"id": 2,
"name": "f3",
"type": {
"type": "ROW",
"fields": [
{
"id": 3,
"name": "f0",
"type": "BOOLEAN"
},
{
"id": 4,
"name": "f1",
"type": "BIGINT"
}
]
}
},
{
"id": 5,
"name": "f4",
"type": "TIMESTAMP(9)"
},
{
"id": 6,
"name": "f5",
"type": "DATE"
},
{
"id": 7,
"name": "f6",
"type": "DECIMAL(2, 2)"
}
],
"highestFieldId": 7,
"partitionKeys": [],
"primaryKeys": [],
"options": {
"file.format": "orc",
"manifest.format": "orc"
},
"timeMillis": 1721614341162
})==";
ASSERT_EQ(ReplaceAll(expected_schema), ReplaceAll(json_str));
}
TEST_F(TableSchemaTest, TestToJsonWithNestedField1) {
auto fs = std::make_shared<LocalFileSystem>();
std::string path = paimon::test::GetDataDir() +
"/orc/append_table_with_nested_type.db/append_table_with_nested_type/schema/"
"schema-0";
std::string content;
ASSERT_OK(fs->ReadFile(path, &content));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableSchema> st, TableSchema::CreateFromJson(content));
ASSERT_OK_AND_ASSIGN(std::string json_str, st->ToJsonString());
std::string expected_str = R"==({
"version": 3,
"id": 0,
"fields": [
{
"id": 0,
"name": "f0",
"type": {
"type": "ROW",
"fields": [
{
"id": 1,
"name": "sub1",
"type": "DATE"
},
{
"id": 2,
"name": "sub2",
"type": "TIMESTAMP(9)"
},
{
"id": 3,
"name": "sub3",
"type": "DECIMAL(23, 5)"
},
{
"id": 4,
"name": "sub4",
"type": "BYTES"
},
{
"id": 5,
"name": "sub5",
"type": "BYTES"
}
]
}
},
{
"id": 6,
"name": "f1",
"type": {
"type": "ARRAY",
"element": {
"type": "ROW",
"fields": [
{
"id": 7,
"name": "sub1",
"type": "DATE"
},
{
"id": 8,
"name": "sub2",
"type": "TIMESTAMP(9)"
},
{
"id": 9,
"name": "sub3",
"type": "DECIMAL(23, 5)"
},
{
"id": 10,
"name": "sub4",
"type": "BYTES"
},
{
"id": 11,
"name": "sub5",
"type": "BYTES"
}
]
}
}
},
{
"id": 12,
"name": "f2",
"type": {
"type": "MAP",
"key": {
"type": "ROW NOT NULL",
"fields": [
{
"id": 13,
"name": "sub1",
"type": "DATE"
},
{
"id": 14,
"name": "sub2",
"type": "TIMESTAMP(9)"
},
{
"id": 15,
"name": "sub3",
"type": "DECIMAL(23, 5)"
},
{
"id": 16,
"name": "sub4",
"type": "BYTES"
},
{
"id": 17,
"name": "sub5",
"type": "BYTES"
}
]
},
"value": {
"type": "ROW",
"fields": [
{
"id": 18,
"name": "sub1",
"type": "DATE"
},
{
"id": 19,
"name": "sub2",
"type": "TIMESTAMP(9)"
},
{
"id": 20,
"name": "sub3",
"type": "DECIMAL(23, 5)"
},
{
"id": 21,
"name": "sub4",
"type": "BYTES"
},
{
"id": 22,
"name": "sub5",
"type": "BYTES"
}
]
}
}
}
],
"highestFieldId": 22,
"partitionKeys": [],
"primaryKeys": [],
"options": {
"file.format": "orc",
"manifest.format": "orc"
},
"timeMillis": 1729759141146
})==";
ASSERT_EQ(ReplaceAll(expected_str), ReplaceAll(json_str));
// test create from arrow schema, test set field id
auto f0 = arrow::field(
"f0", arrow::struct_({arrow::field("sub1", arrow::date32()),
arrow::field("sub2", arrow::timestamp(arrow::TimeUnit::NANO)),
arrow::field("sub3", arrow::decimal128(23, 5)),
arrow::field("sub4", arrow::binary()),
arrow::field("sub5", arrow::binary())}));
auto f1 = arrow::field("f1", arrow::list(f0));
auto f2 = arrow::field("f2", arrow::map(f0->type(), f0->type()));
arrow::FieldVector fields = {f0, f1, f2};
auto arrow_schema = arrow::schema(fields);
ASSERT_OK_AND_ASSIGN(
auto new_table_schema,
TableSchema::Create(0, arrow_schema, /*partition_keys=*/{}, /*primary_keys=*/{},
/*options=*/{{"file.format", "orc"}, {"manifest.format", "orc"}}));
new_table_schema->time_millis_ = 1729759141146;
ASSERT_OK_AND_ASSIGN(std::string new_json_str, new_table_schema->ToJsonString());
ASSERT_EQ(expected_str, new_json_str) << new_json_str;
}
TEST_F(TableSchemaTest, TestInvalidSchema) {
{
// partition key same as primary key
std::string table_schema_str = R"({
"version" : 3,
"id" : 0,
"fields" : [ {
"id" : 0,
"name" : "f0",
"type" : "STRING"
}, {
"id" : 1,
"name" : "f1",
"type" : "INT"
}, {
"id" : 2,
"name" : "f2",
"type" : "INT"
}, {
"id" : 3,
"name" : "f3",
"type" : "DOUBLE"
} ],
"highestFieldId" : 3,
"partitionKeys" : [ "f1", "f2" ],
"primaryKeys" : [ "f1", "f2" ],
"options" : {
"bucket" : "2",
"bucket-key" : "f2",
"manifest.format" : "orc",
"file.format" : "orc"
},
"timeMillis" : 1721614341162
})";
ASSERT_NOK_WITH_MSG(TableSchema::CreateFromJson(table_schema_str),
"this will result in only one record in a partition");
}
{
// bucket key is non-exist in fields
std::string table_schema_str = R"({
"version" : 3,
"id" : 0,
"fields" : [ {
"id" : 0,
"name" : "f0",
"type" : "STRING"
}, {
"id" : 1,
"name" : "f1",
"type" : "INT"
}, {
"id" : 2,
"name" : "f2",
"type" : "INT"
}, {
"id" : 3,
"name" : "f3",
"type" : "DOUBLE"
} ],
"highestFieldId" : 3,
"partitionKeys" : [ "f1"],
"primaryKeys" : [ "f1", "f2" ],
"options" : {
"bucket" : "2",
"bucket-key" : "non-exist",
"manifest.format" : "orc",
"file.format" : "orc"
},
"timeMillis" : 1721614341162
})";
ASSERT_NOK_WITH_MSG(TableSchema::CreateFromJson(table_schema_str),
"should contain all bucket keys");
}
{
// bucket key in partition key
std::string table_schema_str = R"({
"version" : 3,
"id" : 0,
"fields" : [ {
"id" : 0,
"name" : "f0",
"type" : "STRING"
}, {
"id" : 1,
"name" : "f1",
"type" : "INT"
}, {
"id" : 2,
"name" : "f2",
"type" : "INT"
}, {
"id" : 3,
"name" : "f3",
"type" : "DOUBLE"
} ],
"highestFieldId" : 3,
"partitionKeys" : [ "f1"],
"primaryKeys" : [ "f1", "f2" ],
"options" : {
"bucket" : "2",
"bucket-key" : "f1,f3",
"manifest.format" : "orc",
"file.format" : "orc"
},
"timeMillis" : 1721614341162
})";
ASSERT_NOK_WITH_MSG(TableSchema::CreateFromJson(table_schema_str),
"should not in partition keys");
}
{
// bucket key is not a subset of primary key
std::string table_schema_str = R"({
"version" : 3,
"id" : 0,
"fields" : [ {
"id" : 0,
"name" : "f0",
"type" : "STRING"
}, {
"id" : 1,
"name" : "f1",
"type" : "INT"
}, {
"id" : 2,
"name" : "f2",
"type" : "INT"
}, {
"id" : 3,
"name" : "f3",
"type" : "DOUBLE"
} ],
"highestFieldId" : 3,
"partitionKeys" : [ "f1"],
"primaryKeys" : [ "f1", "f2" ],
"options" : {
"bucket" : "2",
"bucket-key" : "f3",
"manifest.format" : "orc",
"file.format" : "orc"
},
"timeMillis" : 1721614341162
})";
ASSERT_NOK_WITH_MSG(TableSchema::CreateFromJson(table_schema_str),
"should contain all bucket keys");
}
}
TEST_F(TableSchemaTest, SetFieldIdBasicType) {
auto field = arrow::field("column1", arrow::int32());
{
int32_t field_id = 0;
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<arrow::Field> new_field,
TableSchema::AssignFieldIdsRecursively(field, /*set_field_id=*/true, &field_id));
ASSERT_TRUE(new_field->metadata());
ASSERT_EQ(new_field->metadata()->Get(DataField::FIELD_ID).ValueOrDie(), "0");
ASSERT_EQ(field_id, 1);
}
{
int32_t field_id = 0;
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<arrow::Field> new_field,
TableSchema::AssignFieldIdsRecursively(field, /*set_field_id=*/false, &field_id));
ASSERT_FALSE(new_field->metadata());
ASSERT_EQ(field_id, 0);
}
}
TEST_F(TableSchemaTest, SetFieldIdStructType) {
auto child1 = arrow::field("child1", arrow::int32());
auto child2 = arrow::field("child2", arrow::float64());
auto struct_field = arrow::field("parent", arrow::struct_({child1, child2}));
{
int32_t field_id = 0;
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<arrow::Field> new_field,
TableSchema::AssignFieldIdsRecursively(struct_field, /*set_field_id=*/true, &field_id));
auto struct_type = std::static_pointer_cast<arrow::StructType>(new_field->type());
ASSERT_EQ(struct_type->num_fields(), 2);
ASSERT_EQ(new_field->metadata()->Get(DataField::FIELD_ID).ValueOrDie(), "0");
ASSERT_EQ(struct_type->field(0)->metadata()->Get(DataField::FIELD_ID).ValueOrDie(), "1");
ASSERT_EQ(struct_type->field(1)->metadata()->Get(DataField::FIELD_ID).ValueOrDie(), "2");
ASSERT_EQ(field_id, 3);
}
{
int32_t field_id = 0;
ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Field> new_field,
TableSchema::AssignFieldIdsRecursively(
struct_field, /*set_field_id=*/false, &field_id));
auto struct_type = std::static_pointer_cast<arrow::StructType>(new_field->type());
ASSERT_EQ(struct_type->num_fields(), 2);
ASSERT_FALSE(new_field->metadata());
ASSERT_EQ(struct_type->field(0)->metadata()->Get(DataField::FIELD_ID).ValueOrDie(), "0");
ASSERT_EQ(struct_type->field(1)->metadata()->Get(DataField::FIELD_ID).ValueOrDie(), "1");
ASSERT_EQ(field_id, 2);
}
}
TEST_F(TableSchemaTest, SetFieldIdListType) {
auto value_field = arrow::field("values", arrow::int32());
auto list_field = arrow::field("list_column", arrow::list(value_field));
{
int32_t field_id = 0;
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<arrow::Field> new_field,
TableSchema::AssignFieldIdsRecursively(list_field, /*set_field_id=*/true, &field_id));
ASSERT_EQ(new_field->metadata()->Get(DataField::FIELD_ID).ValueOrDie(), "0");
auto list_type = arrow::internal::checked_pointer_cast<arrow::ListType>(new_field->type());
ASSERT_FALSE(list_type->value_field()->metadata());
ASSERT_EQ(field_id, 1);
}
{
int32_t field_id = 0;
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<arrow::Field> new_field,
TableSchema::AssignFieldIdsRecursively(list_field, /*set_field_id=*/false, &field_id));
ASSERT_FALSE(new_field->metadata());
auto list_type = arrow::internal::checked_pointer_cast<arrow::ListType>(new_field->type());
ASSERT_FALSE(list_type->value_field()->metadata());
ASSERT_EQ(field_id, 0);
}
}
TEST_F(TableSchemaTest, SetFieldIdMapType) {
auto map_field = arrow::field("map_column", arrow::map(arrow::utf8(), arrow::int32()));
{
int32_t field_id = 0;
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<arrow::Field> new_field,
TableSchema::AssignFieldIdsRecursively(map_field, /*set_field_id=*/true, &field_id));
ASSERT_EQ(new_field->metadata()->Get(DataField::FIELD_ID).ValueOrDie(), "0");
auto map_type = arrow::internal::checked_pointer_cast<arrow::MapType>(new_field->type());
std::shared_ptr<arrow::Field> key_field = map_type->key_field();
std::shared_ptr<arrow::Field> value_field = map_type->item_field();
ASSERT_FALSE(key_field->metadata());
ASSERT_FALSE(value_field->metadata());
ASSERT_EQ(field_id, 1);
}
{
int32_t field_id = 0;
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<arrow::Field> new_field,
TableSchema::AssignFieldIdsRecursively(map_field, /*set_field_id=*/false, &field_id));
ASSERT_FALSE(new_field->metadata());
auto map_type = arrow::internal::checked_pointer_cast<arrow::MapType>(new_field->type());
std::shared_ptr<arrow::Field> key_field = map_type->key_field();
std::shared_ptr<arrow::Field> value_field = map_type->item_field();
ASSERT_FALSE(key_field->metadata());
ASSERT_FALSE(value_field->metadata());
ASSERT_EQ(field_id, 0);
}
}
TEST_F(TableSchemaTest, SetFieldIdMapWithStruct) {
auto inner_child1 = arrow::field("inner1", arrow::int32());
auto inner_child2 = arrow::field("inner2", arrow::float64());
auto map_field =
arrow::field("map_column", arrow::map(arrow::struct_({inner_child1, inner_child2}),
arrow::struct_({inner_child1, inner_child2})));
{
int32_t field_id = 0;
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<arrow::Field> new_field,
TableSchema::AssignFieldIdsRecursively(map_field, /*set_field_id=*/true, &field_id));
ASSERT_EQ(new_field->metadata()->Get(DataField::FIELD_ID).ValueOrDie(), "0");
auto map_type = arrow::internal::checked_pointer_cast<arrow::MapType>(new_field->type());
std::shared_ptr<arrow::Field> key_field = map_type->key_field();
std::shared_ptr<arrow::Field> value_field = map_type->item_field();
ASSERT_FALSE(key_field->metadata());
auto key_inner_type = std::static_pointer_cast<arrow::StructType>(key_field->type());
ASSERT_EQ(key_inner_type->field(0)->metadata()->Get(DataField::FIELD_ID).ValueOrDie(), "1");
ASSERT_EQ(key_inner_type->field(1)->metadata()->Get(DataField::FIELD_ID).ValueOrDie(), "2");
ASSERT_FALSE(value_field->metadata());
auto value_inner_type = std::static_pointer_cast<arrow::StructType>(value_field->type());
ASSERT_EQ(value_inner_type->field(0)->metadata()->Get(DataField::FIELD_ID).ValueOrDie(),
"3");
ASSERT_EQ(value_inner_type->field(1)->metadata()->Get(DataField::FIELD_ID).ValueOrDie(),
"4");
ASSERT_EQ(field_id, 5);
}
{
int32_t field_id = 0;
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<arrow::Field> new_field,
TableSchema::AssignFieldIdsRecursively(map_field, /*set_field_id=*/false, &field_id));
ASSERT_FALSE(new_field->metadata());
auto map_type = arrow::internal::checked_pointer_cast<arrow::MapType>(new_field->type());
std::shared_ptr<arrow::Field> key_field = map_type->key_field();
std::shared_ptr<arrow::Field> value_field = map_type->item_field();
ASSERT_FALSE(key_field->metadata());
auto key_inner_type = std::static_pointer_cast<arrow::StructType>(key_field->type());
ASSERT_EQ(key_inner_type->field(0)->metadata()->Get(DataField::FIELD_ID).ValueOrDie(), "0");
ASSERT_EQ(key_inner_type->field(1)->metadata()->Get(DataField::FIELD_ID).ValueOrDie(), "1");
ASSERT_FALSE(value_field->metadata());
auto value_inner_type = std::static_pointer_cast<arrow::StructType>(value_field->type());
ASSERT_EQ(value_inner_type->field(0)->metadata()->Get(DataField::FIELD_ID).ValueOrDie(),
"2");
ASSERT_EQ(value_inner_type->field(1)->metadata()->Get(DataField::FIELD_ID).ValueOrDie(),
"3");
ASSERT_EQ(field_id, 4);
}
}
TEST_F(TableSchemaTest, SetFieldIdNestedStruct) {
auto inner_child1 = arrow::field("inner1", arrow::int32());
auto inner_child2 = arrow::field("inner2", arrow::float64());
auto inner_struct = arrow::field("inner_struct", arrow::struct_({inner_child1, inner_child2}));
auto outer_struct = arrow::field("outer_struct", arrow::struct_({inner_struct}));
{
int32_t field_id = 0;
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<arrow::Field> new_field,
TableSchema::AssignFieldIdsRecursively(outer_struct, /*set_field_id=*/true, &field_id));
ASSERT_EQ(new_field->metadata()->Get(DataField::FIELD_ID).ValueOrDie(), "0");
auto outer_type = std::static_pointer_cast<arrow::StructType>(new_field->type());
ASSERT_EQ(outer_type->field(0)->metadata()->Get(DataField::FIELD_ID).ValueOrDie(), "1");
auto inner_type = std::static_pointer_cast<arrow::StructType>(outer_type->field(0)->type());
ASSERT_EQ(inner_type->field(0)->metadata()->Get(DataField::FIELD_ID).ValueOrDie(), "2");
ASSERT_EQ(inner_type->field(1)->metadata()->Get(DataField::FIELD_ID).ValueOrDie(), "3");
ASSERT_EQ(field_id, 4);
}
{
int32_t field_id = 0;
ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Field> new_field,
TableSchema::AssignFieldIdsRecursively(
outer_struct, /*set_field_id=*/false, &field_id));
ASSERT_FALSE(new_field->metadata());
auto outer_type = std::static_pointer_cast<arrow::StructType>(new_field->type());
ASSERT_EQ(outer_type->field(0)->metadata()->Get(DataField::FIELD_ID).ValueOrDie(), "0");
auto inner_type = std::static_pointer_cast<arrow::StructType>(outer_type->field(0)->type());
ASSERT_EQ(inner_type->field(0)->metadata()->Get(DataField::FIELD_ID).ValueOrDie(), "1");
ASSERT_EQ(inner_type->field(1)->metadata()->Get(DataField::FIELD_ID).ValueOrDie(), "2");
ASSERT_EQ(field_id, 3);
}
}
TEST_F(TableSchemaTest, SetFieldIdNestedListInStruct) {
auto list_value_field = arrow::field("list_values", arrow::int32());
auto list_field = arrow::field("list_column", arrow::list(list_value_field));
auto struct_field = arrow::field("struct_with_list", arrow::struct_({list_field}));
{
int32_t field_id = 0;
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<arrow::Field> new_field,
TableSchema::AssignFieldIdsRecursively(struct_field, /*set_field_id=*/true, &field_id));
auto struct_type = std::static_pointer_cast<arrow::StructType>(new_field->type());
ASSERT_EQ(new_field->metadata()->Get(DataField::FIELD_ID).ValueOrDie(), "0");
ASSERT_EQ(struct_type->field(0)->metadata()->Get(DataField::FIELD_ID).ValueOrDie(), "1");
auto list_type =
arrow::internal::checked_pointer_cast<arrow::ListType>(struct_type->field(0)->type());
ASSERT_FALSE(list_type->value_field()->metadata());
ASSERT_EQ(field_id, 2);
}
{
int32_t field_id = 0;
ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Field> new_field,
TableSchema::AssignFieldIdsRecursively(
struct_field, /*set_field_id=*/false, &field_id));
auto struct_type = std::static_pointer_cast<arrow::StructType>(new_field->type());
ASSERT_FALSE(new_field->metadata());
ASSERT_EQ(struct_type->field(0)->metadata()->Get(DataField::FIELD_ID).ValueOrDie(), "0");
auto list_type =
arrow::internal::checked_pointer_cast<arrow::ListType>(struct_type->field(0)->type());
ASSERT_FALSE(list_type->value_field()->metadata());
ASSERT_EQ(field_id, 1);
}
}
TEST_F(TableSchemaTest, MapKeyMustBeNotNull) {
std::string table_schema_str = R"({
"version" : 3,
"id" : 0,
"fields" : [ {
"id" : 0,
"name" : "f0",
"type" : {
"type": "MAP",
"key": "TINYINT",
"value": "SMALLINT"
}
} ],
"highestFieldId" : 0,
"partitionKeys" : [],
"primaryKeys" : [],
"options" : {},
"timeMillis" : 1721614341162
})";
ASSERT_NOK_WITH_MSG(TableSchema::CreateFromJson(table_schema_str),
"Map field 'f0' has a nullable key.");
}
TEST_F(TableSchemaTest, CrossPartitionUpdate) {
arrow::FieldVector fields = {
arrow::field("f0", arrow::boolean()), arrow::field("f1", arrow::int8()),
arrow::field("f2", arrow::int8()), arrow::field("f3", arrow::int16()),
arrow::field("f4", arrow::int16()), arrow::field("f5", arrow::int32())};
auto schema = arrow::schema(fields);
std::vector<std::string> primary_keys = {"f1", "f2"};
std::vector<std::string> partition_keys = {"f2", "f3"};
std::map<std::string, std::string> options;
ASSERT_OK_AND_ASSIGN(
auto table_schema,
TableSchema::Create(/*schema_id=*/0, schema, partition_keys, primary_keys, options));
ASSERT_TRUE(table_schema);
ASSERT_TRUE(table_schema->CrossPartitionUpdate());
}
TEST_F(TableSchemaTest, CrossPartitionUpdate2) {
arrow::FieldVector fields = {
arrow::field("f0", arrow::boolean()), arrow::field("f1", arrow::int8()),
arrow::field("f2", arrow::int8()), arrow::field("f3", arrow::int16()),
arrow::field("f4", arrow::int16()), arrow::field("f5", arrow::int32())};
auto schema = arrow::schema(fields);
std::vector<std::string> primary_keys = {"f1", "f2", "f3"};
std::vector<std::string> partition_keys = {"f2", "f3"};
std::map<std::string, std::string> options;
ASSERT_OK_AND_ASSIGN(
auto table_schema,
TableSchema::Create(/*schema_id=*/0, schema, partition_keys, primary_keys, options));
ASSERT_TRUE(table_schema);
ASSERT_FALSE(table_schema->CrossPartitionUpdate());
}
} // namespace paimon::test