blob: ba34808a42bb8c9fc3e7fc5303323385c9b087ed [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 "iceberg/catalog/hive/hive_utils.h"
#include <gtest/gtest.h>
namespace iceberg::hive {
TEST(ValidateNamespaceTest, SingleLevelAccepted) {
EXPECT_TRUE(ValidateNamespace(Namespace{{"warehouse"}}).has_value());
}
TEST(ValidateNamespaceTest, MultiLevelRejected) {
auto status = ValidateNamespace(Namespace{{"a", "b"}});
ASSERT_FALSE(status.has_value());
EXPECT_EQ(status.error().kind, ErrorKind::kInvalidArgument);
}
TEST(ValidateNamespaceTest, EmptyLevelRejected) {
auto status = ValidateNamespace(Namespace{{""}});
ASSERT_FALSE(status.has_value());
EXPECT_EQ(status.error().kind, ErrorKind::kInvalidArgument);
}
TEST(ValidateOwnerSettingsTest, OwnerTypeRequiresOwner) {
auto status = ValidateOwnerSettings({{"hive.metastore.database.owner-type", "USER"}});
ASSERT_FALSE(status.has_value());
EXPECT_EQ(status.error().kind, ErrorKind::kInvalidArgument);
}
TEST(ValidateOwnerSettingsTest, OwnerOrOwnerPlusTypeAreOk) {
EXPECT_TRUE(
ValidateOwnerSettings({{"hive.metastore.database.owner", "alice"}}).has_value());
EXPECT_TRUE(ValidateOwnerSettings({{"hive.metastore.database.owner", "alice"},
{"hive.metastore.database.owner-type", "ROLE"}})
.has_value());
EXPECT_TRUE(ValidateOwnerSettings({}).has_value());
}
TEST(ValidateOwnerSettingsTest, UnknownOwnerTypeRejected) {
auto status = ValidateOwnerSettings({{"hive.metastore.database.owner", "alice"},
{"hive.metastore.database.owner-type", "ADMIN"}});
ASSERT_FALSE(status.has_value());
EXPECT_EQ(status.error().kind, ErrorKind::kInvalidArgument);
}
TEST(ValidateOwnerSettingsTest, BareOwnerKeysAreOrdinaryProperties) {
EXPECT_TRUE(ValidateOwnerSettings({{"owner-type", "nonsense"}}).has_value());
}
TEST(ConvertToHiveDatabaseTest, LiftsReservedKeysIntoDedicatedFields) {
auto db = ConvertToHiveDatabase(Namespace{{"warehouse"}},
{{"comment", "primary db"},
{"location", "s3://bucket/wh/"},
{"hive.metastore.database.owner", "alice"},
{"hive.metastore.database.owner-"
"type",
"USER"},
{"custom-prop", "value"}});
ASSERT_TRUE(db.has_value()) << db.error().message;
EXPECT_EQ(db->name, "warehouse");
EXPECT_EQ(db->description, "primary db");
EXPECT_EQ(db->location_uri, "s3://bucket/wh/");
EXPECT_EQ(db->owner_name, "alice");
EXPECT_EQ(db->owner_type, "USER");
ASSERT_EQ(db->parameters.size(), 1);
EXPECT_EQ(db->parameters.at("custom-prop"), "value");
}
TEST(ConvertToHiveDatabaseTest, RejectsHierarchicalNamespace) {
auto db = ConvertToHiveDatabase(Namespace{{"a", "b"}}, {});
ASSERT_FALSE(db.has_value());
EXPECT_EQ(db.error().kind, ErrorKind::kInvalidArgument);
}
TEST(ConvertToHiveDatabaseTest, RejectsNonCanonicalOwnerType) {
auto db = ConvertToHiveDatabase(Namespace{{"warehouse"}},
{{"hive.metastore.database.owner", "alice"},
{"hive.metastore.database.owner-type", "role"}});
ASSERT_FALSE(db.has_value());
EXPECT_EQ(db.error().kind, ErrorKind::kInvalidArgument);
}
TEST(ConvertFromHiveDatabaseTest, RoundTripWithCustomProperties) {
auto original = ConvertToHiveDatabase(Namespace{{"warehouse"}},
{{"comment", "primary"},
{"hive.metastore.database.owner", "alice"},
{"team", "data"}});
ASSERT_TRUE(original.has_value()) << original.error().message;
const auto round_tripped = ConvertFromHiveDatabase(*original);
EXPECT_EQ(round_tripped.ns.levels, std::vector<std::string>{"warehouse"});
EXPECT_EQ(round_tripped.properties.at("comment"), "primary");
EXPECT_EQ(round_tripped.properties.at("hive.metastore.database.owner"), "alice");
EXPECT_EQ(round_tripped.properties.at("team"), "data");
EXPECT_FALSE(round_tripped.properties.contains("hive.metastore.database.owner-type"));
}
TEST(ConvertToHiveTableTest, SetsIcebergMarkerParametersAndStorageDescriptor) {
TableIdentifier ident{.ns = Namespace{{"warehouse"}}, .name = "orders"};
const std::vector<HiveColumn> columns = {{.name = "id", .type_string = "bigint"},
{.name = "amount", .type_string = "double"}};
auto table = ConvertToHiveTable(ident, columns,
/*metadata_location=*/
"s3://bucket/wh/warehouse.db/"
"orders/metadata/v1.metadata.json",
/*location=*/"s3://bucket/wh/warehouse.db/orders",
/*table_properties=*/{{"format-version", "2"}});
ASSERT_TRUE(table.has_value()) << table.error().message;
EXPECT_EQ(table->db_name, "warehouse");
EXPECT_EQ(table->table_name, "orders");
EXPECT_EQ(table->table_type, "EXTERNAL_TABLE");
EXPECT_EQ(table->location, "s3://bucket/wh/warehouse.db/orders");
EXPECT_EQ(table->columns.size(), 2);
EXPECT_EQ(table->columns[0].name, "id");
EXPECT_EQ(table->columns[0].type_string, "bigint");
EXPECT_EQ(table->parameters.at("metadata_location"),
"s3://bucket/wh/warehouse.db/orders/metadata/v1.metadata.json");
EXPECT_EQ(table->parameters.at("table_type"), "ICEBERG");
EXPECT_EQ(table->parameters.at("EXTERNAL"), "TRUE");
EXPECT_EQ(table->parameters.at("format-version"), "2");
EXPECT_FALSE(table->parameters.contains("external.table.purge"));
EXPECT_EQ(table->serde, "org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe");
EXPECT_EQ(table->input_format, "org.apache.hadoop.mapred.FileInputFormat");
EXPECT_EQ(table->output_format, "org.apache.hadoop.mapred.FileOutputFormat");
}
TEST(ConvertToHiveTableTest, UsesQualifiedOwnerAndPreservesCommentAndBareOwner) {
TableIdentifier ident{.ns = Namespace{{"warehouse"}}, .name = "orders"};
auto table = ConvertToHiveTable(ident, /*columns=*/{}, /*metadata_location=*/"loc",
/*location=*/"root",
{{"comment", "table description"},
{"owner", "bob"},
{"hive.metastore.table.owner", "alice"},
{"location", "other root"}});
ASSERT_TRUE(table.has_value()) << table.error().message;
EXPECT_EQ(table->owner, "alice");
EXPECT_EQ(table->location, "root");
EXPECT_FALSE(table->parameters.contains("hive.metastore.table.owner"));
EXPECT_EQ(table->parameters.at("location"), "other root");
EXPECT_EQ(table->parameters.at("comment"), "table description");
EXPECT_EQ(table->parameters.at("owner"), "bob");
}
TEST(ConvertToHiveTableTest, TranslatesGcEnabled) {
TableIdentifier ident{.ns = Namespace{{"warehouse"}}, .name = "orders"};
for (const std::string value : {"true", "false"}) {
SCOPED_TRACE(value);
auto table = ConvertToHiveTable(ident, {}, "loc", "root", {{"gc.enabled", value}});
ASSERT_TRUE(table.has_value()) << table.error().message;
EXPECT_EQ(table->parameters.at("external.table.purge"), value);
EXPECT_FALSE(table->parameters.contains("gc.enabled"));
}
}
TEST(ConvertToHiveTableTest, ConflictingPurgePropertiesFollowIterationOrder) {
TableIdentifier ident{.ns = Namespace{{"warehouse"}}, .name = "orders"};
const std::unordered_map<std::string, std::string> properties = {
{"gc.enabled", "false"}, {"external.table.purge", "true"}};
std::string expected;
for (const auto& [key, value] : properties) {
expected = value;
}
auto table = ConvertToHiveTable(ident, {}, "loc", "root", properties);
ASSERT_TRUE(table.has_value()) << table.error().message;
EXPECT_EQ(table->parameters.at("external.table.purge"), expected);
}
TEST(ConvertToHiveTableTest, PreservesExplicitPurgePropertyWithoutGcEnabled) {
TableIdentifier ident{.ns = Namespace{{"warehouse"}}, .name = "orders"};
auto table =
ConvertToHiveTable(ident, {}, "loc", "root", {{"external.table.purge", "true"}});
ASSERT_TRUE(table.has_value()) << table.error().message;
EXPECT_EQ(table->parameters.at("external.table.purge"), "true");
}
TEST(ConvertToHiveTableTest, RejectsEmptyMetadataLocation) {
TableIdentifier ident{.ns = Namespace{{"warehouse"}}, .name = "orders"};
auto table = ConvertToHiveTable(ident, {}, "", "root", {});
ASSERT_FALSE(table.has_value());
EXPECT_EQ(table.error().kind, ErrorKind::kInvalidArgument);
}
TEST(ConvertToHiveTableTest, ProtectsIcebergMarkerParameters) {
TableIdentifier ident{.ns = Namespace{{"warehouse"}}, .name = "orders"};
auto table = ConvertToHiveTable(
ident, {}, "loc", "root",
{{"metadata_location", "other"}, {"table_type", "OTHER"}, {"EXTERNAL", "FALSE"}});
ASSERT_TRUE(table.has_value()) << table.error().message;
EXPECT_EQ(table->parameters.at("metadata_location"), "loc");
EXPECT_EQ(table->parameters.at("table_type"), "ICEBERG");
EXPECT_EQ(table->parameters.at("EXTERNAL"), "TRUE");
}
TEST(GetMetadataLocationTest, ExtractsKnownKey) {
auto loc = GetMetadataLocation({{"metadata_location", "s3://m.json"}});
ASSERT_TRUE(loc.has_value()) << loc.error().message;
EXPECT_EQ(*loc, "s3://m.json");
}
TEST(GetMetadataLocationTest, AbsenceMapsToNotFound) {
auto loc = GetMetadataLocation({{"EXTERNAL", "TRUE"}});
ASSERT_FALSE(loc.has_value());
EXPECT_EQ(loc.error().kind, ErrorKind::kNotFound);
}
TEST(GetMetadataLocationTest, EmptyLocationMapsToNotFound) {
auto loc = GetMetadataLocation({{"metadata_location", ""}});
ASSERT_FALSE(loc.has_value());
EXPECT_EQ(loc.error().kind, ErrorKind::kNotFound);
}
TEST(ValidateIcebergTableTest, AcceptsCaseInsensitiveIcebergMarker) {
const TableIdentifier id{.ns = Namespace{{"sales"}}, .name = "orders"};
EXPECT_TRUE(ValidateIcebergTable(id, {{"table_type", "ICEBERG"}}).has_value());
EXPECT_TRUE(ValidateIcebergTable(id, {{"table_type", "iceberg"}}).has_value());
EXPECT_TRUE(ValidateIcebergTable(id, {{"table_type", "Iceberg"}}).has_value());
}
TEST(ValidateIcebergTableTest, RejectsMissingMarker) {
const TableIdentifier id{.ns = Namespace{{"sales"}}, .name = "orders"};
auto status = ValidateIcebergTable(id, {{"EXTERNAL", "TRUE"}});
ASSERT_FALSE(status.has_value());
EXPECT_EQ(status.error().kind, ErrorKind::kNoSuchTable);
}
TEST(ValidateIcebergTableTest, RejectsForeignTableType) {
const TableIdentifier id{.ns = Namespace{{"sales"}}, .name = "orders"};
auto status =
ValidateIcebergTable(id, {{"table_type", "MANAGED_TABLE"}, {"EXTERNAL", "TRUE"}});
ASSERT_FALSE(status.has_value());
EXPECT_EQ(status.error().kind, ErrorKind::kNoSuchTable);
}
TEST(GetDefaultTableLocationTest, TrimsTrailingSlashAndAppendsDbAndTable) {
EXPECT_EQ(
GetDefaultTableLocation("s3://bucket/wh/", Namespace{{"warehouse"}}, "orders"),
"s3://bucket/wh/warehouse.db/orders");
EXPECT_EQ(GetDefaultTableLocation("hdfs://nn/data", Namespace{{"sales"}}, "orders"),
"hdfs://nn/data/sales.db/orders");
}
} // namespace iceberg::hive