This is an automated email from the ASF dual-hosted git repository.

wgtmac pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-cpp.git


The following commit(s) were added to refs/heads/main by this push:
     new 70b2b6a1 feat(hive): add Iceberg-to-HMS schema and record conversion 
helpers (#904)
70b2b6a1 is described below

commit 70b2b6a1a8b256d15469760d077ebede631ce689
Author: liuxiaoyu <[email protected]>
AuthorDate: Fri Oct 9 22:25:35 2026 +0800

    feat(hive): add Iceberg-to-HMS schema and record conversion helpers (#904)
---
 cmake_modules/IcebergThirdpartyToolchain.cmake |   5 +-
 src/iceberg/catalog/hive/CMakeLists.txt        |  18 +-
 src/iceberg/catalog/hive/hive_schema.cc        | 128 ++++++++++++
 src/iceberg/catalog/hive/hive_schema.h         |  51 +++++
 src/iceberg/catalog/hive/hive_utils.cc         | 214 ++++++++++++++++++++
 src/iceberg/catalog/hive/hive_utils.h          | 131 +++++++++++++
 src/iceberg/cpp20_compatibility_internal.h     |   2 +
 src/iceberg/test/CMakeLists.txt                |   6 +-
 src/iceberg/test/hive_schema_test.cc           | 209 ++++++++++++++++++++
 src/iceberg/test/hive_utils_test.cc            | 262 +++++++++++++++++++++++++
 10 files changed, 1015 insertions(+), 11 deletions(-)

diff --git a/cmake_modules/IcebergThirdpartyToolchain.cmake 
b/cmake_modules/IcebergThirdpartyToolchain.cmake
index 7631f0e0..9e0d3d3f 100644
--- a/cmake_modules/IcebergThirdpartyToolchain.cmake
+++ b/cmake_modules/IcebergThirdpartyToolchain.cmake
@@ -906,10 +906,7 @@ function(resolve_thrift_dependency)
     return()
   endif()
   if(ICEBERG_THRIFT_BUNDLED)
-    # Arrow's bundled build creates the Thrift C++ runtime as a `thrift` target
-    # scoped to its FetchContent directory, where iceberg_hive cannot see it.
-    # Promote it to a global `thrift::thrift` alias so iceberg_hive can link 
the
-    # generated Hive Metastore bindings against it.
+    # Expose bundled Thrift and its include paths to Hive.
     if(TARGET thrift AND NOT TARGET thrift::thrift)
       add_library(thrift::thrift INTERFACE IMPORTED GLOBAL)
       target_link_libraries(thrift::thrift INTERFACE thrift)
diff --git a/src/iceberg/catalog/hive/CMakeLists.txt 
b/src/iceberg/catalog/hive/CMakeLists.txt
index 5ec191d3..ff9f7a8a 100644
--- a/src/iceberg/catalog/hive/CMakeLists.txt
+++ b/src/iceberg/catalog/hive/CMakeLists.txt
@@ -54,8 +54,13 @@ if(NOT TARGET thrift::thrift)
                       "-DICEBERG_BUNDLE_THRIFT=OFF against a system Thrift 
install.")
 endif()
 
-set(ICEBERG_HIVE_SOURCES hive_catalog.cc hive_catalog_properties.cc 
hms_client.cc
-                         ${ICEBERG_HIVE_THRIFT_GEN_SOURCES})
+set(ICEBERG_HIVE_SOURCES
+    hive_catalog.cc
+    hive_catalog_properties.cc
+    hive_schema.cc
+    hive_utils.cc
+    hms_client.cc
+    ${ICEBERG_HIVE_THRIFT_GEN_SOURCES})
 
 set(ICEBERG_HIVE_STATIC_BUILD_INTERFACE_LIBS)
 set(ICEBERG_HIVE_SHARED_BUILD_INTERFACE_LIBS)
@@ -72,10 +77,11 @@ set(ICEBERG_HIVE_SHARED_INSTALL_INTERFACE_LIBS)
 #  * System: downstream find_package(Iceberg) re-creates `thrift::thrift` via
 #    find_dependency(ThriftAlt) (FindThriftAlt.cmake is shipped next to the
 #    config), so it must be propagated.
-list(APPEND ICEBERG_HIVE_STATIC_BUILD_INTERFACE_LIBS
-     "$<IF:$<TARGET_EXISTS:iceberg_static>,iceberg_static,iceberg_shared>" 
thrift::thrift)
-list(APPEND ICEBERG_HIVE_SHARED_BUILD_INTERFACE_LIBS
-     "$<IF:$<TARGET_EXISTS:iceberg_shared>,iceberg_shared,iceberg_static>" 
thrift::thrift)
+# Prefer Thrift headers over any system Thrift found through core.
+list(APPEND ICEBERG_HIVE_STATIC_BUILD_INTERFACE_LIBS thrift::thrift
+     "$<IF:$<TARGET_EXISTS:iceberg_static>,iceberg_static,iceberg_shared>")
+list(APPEND ICEBERG_HIVE_SHARED_BUILD_INTERFACE_LIBS thrift::thrift
+     "$<IF:$<TARGET_EXISTS:iceberg_shared>,iceberg_shared,iceberg_static>")
 list(APPEND
      ICEBERG_HIVE_STATIC_INSTALL_INTERFACE_LIBS
      
"$<IF:$<TARGET_EXISTS:iceberg::iceberg_static>,iceberg::iceberg_static,iceberg::iceberg_shared>"
diff --git a/src/iceberg/catalog/hive/hive_schema.cc 
b/src/iceberg/catalog/hive/hive_schema.cc
new file mode 100644
index 00000000..8311923a
--- /dev/null
+++ b/src/iceberg/catalog/hive/hive_schema.cc
@@ -0,0 +1,128 @@
+/*
+ * 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_schema.h"
+
+#include <format>
+#include <string>
+#include <vector>
+
+#include "iceberg/schema.h"
+#include "iceberg/schema_field.h"
+#include "iceberg/type.h"
+#include "iceberg/util/checked_cast.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg::hive {
+
+namespace {
+
+Result<std::string> PrimitiveToHive(const Type& type) {
+  switch (type.type_id()) {
+    case TypeId::kBoolean:
+      return std::string("boolean");
+    case TypeId::kInt:
+      return std::string("int");
+    case TypeId::kLong:
+      return std::string("bigint");
+    case TypeId::kFloat:
+      return std::string("float");
+    case TypeId::kDouble:
+      return std::string("double");
+    case TypeId::kDate:
+      return std::string("date");
+    case TypeId::kTimestamp:
+      return std::string("timestamp");
+    case TypeId::kTimestampTz:
+      return std::string("timestamp with local time zone");
+    case TypeId::kTime:
+    case TypeId::kString:
+    case TypeId::kUuid:
+      return std::string("string");
+    case TypeId::kBinary:
+    case TypeId::kFixed:
+      return std::string("binary");
+    case TypeId::kVariant:
+      return std::string("unknown");
+    case TypeId::kDecimal: {
+      const auto& decimal = internal::checked_cast<const DecimalType&>(type);
+      return std::format("decimal({},{})", decimal.precision(), 
decimal.scale());
+    }
+    default:
+      return NotImplemented("Unhandled primitive Iceberg type id: {}",
+                            static_cast<int>(type.type_id()));
+  }
+}
+
+Result<std::string> StructToHive(const StructType& type) {
+  std::string out = "struct<";
+  bool first = true;
+  for (const auto& field : type.fields()) {
+    if (!first) {
+      out += ",";
+    }
+    ICEBERG_ASSIGN_OR_RAISE(auto field_type, TypeToHiveString(*field.type()));
+    out += std::string(field.name());
+    out += ":";
+    out += field_type;
+    first = false;
+  }
+  out += ">";
+  return out;
+}
+
+Result<std::string> ListToHive(const ListType& type) {
+  ICEBERG_ASSIGN_OR_RAISE(auto inner, 
TypeToHiveString(*type.element().type()));
+  return std::format("array<{}>", inner);
+}
+
+Result<std::string> MapToHive(const MapType& type) {
+  ICEBERG_ASSIGN_OR_RAISE(auto key_type, TypeToHiveString(*type.key().type()));
+  ICEBERG_ASSIGN_OR_RAISE(auto value_type, 
TypeToHiveString(*type.value().type()));
+  return std::format("map<{},{}>", key_type, value_type);
+}
+
+}  // namespace
+
+Result<std::string> TypeToHiveString(const Type& type) {
+  switch (type.type_id()) {
+    case TypeId::kStruct:
+      return StructToHive(internal::checked_cast<const StructType&>(type));
+    case TypeId::kList:
+      return ListToHive(internal::checked_cast<const ListType&>(type));
+    case TypeId::kMap:
+      return MapToHive(internal::checked_cast<const MapType&>(type));
+    default:
+      return PrimitiveToHive(type);
+  }
+}
+
+Result<std::vector<HiveColumn>> SchemaToHiveColumns(const Schema& schema) {
+  std::vector<HiveColumn> columns;
+  columns.reserve(schema.fields().size());
+  for (const auto& field : schema.fields()) {
+    ICEBERG_ASSIGN_OR_RAISE(auto type_string, TypeToHiveString(*field.type()));
+    columns.push_back(HiveColumn{.name = std::string(field.name()),
+                                 .type_string = std::move(type_string),
+                                 .comment = std::string(field.doc())});
+  }
+  return columns;
+}
+
+}  // namespace iceberg::hive
diff --git a/src/iceberg/catalog/hive/hive_schema.h 
b/src/iceberg/catalog/hive/hive_schema.h
new file mode 100644
index 00000000..8a093125
--- /dev/null
+++ b/src/iceberg/catalog/hive/hive_schema.h
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include "iceberg/catalog/hive/iceberg_hive_export.h"
+#include "iceberg/result.h"
+#include "iceberg/type_fwd.h"
+
+/// \file iceberg/catalog/hive/hive_schema.h
+/// \brief Render Iceberg schemas as Hive type strings.
+
+namespace iceberg::hive {
+
+/// \brief Column metadata passed to HMS.
+struct ICEBERG_HIVE_EXPORT HiveColumn {
+  std::string name;
+  std::string type_string;
+  std::string comment;
+};
+
+/// \brief Render a Hive type string, including nested types.
+/// Follow Java: variant -> unknown, time/uuid -> string, fixed -> binary.
+/// Unsupported types return kNotImplemented. Zoned timestamps require Hive 3+.
+ICEBERG_HIVE_EXPORT Result<std::string> TypeToHiveString(const Type& type);
+
+/// \brief Convert top-level fields to HMS columns. Docs become comments.
+/// Field IDs and requiredness are not represented in HMS.
+ICEBERG_HIVE_EXPORT Result<std::vector<HiveColumn>> SchemaToHiveColumns(
+    const Schema& schema);
+
+}  // namespace iceberg::hive
diff --git a/src/iceberg/catalog/hive/hive_utils.cc 
b/src/iceberg/catalog/hive/hive_utils.cc
new file mode 100644
index 00000000..6a7d573d
--- /dev/null
+++ b/src/iceberg/catalog/hive/hive_utils.cc
@@ -0,0 +1,214 @@
+/*
+ * 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 <algorithm>
+#include <array>
+#include <format>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "iceberg/table_properties.h"
+#include "iceberg/util/macros.h"
+#include "iceberg/util/string_util.h"
+
+namespace iceberg::hive {
+
+namespace {
+
+// Keys lifted into dedicated Database fields.
+constexpr std::array<std::string_view, 4> kDatabaseReservedKeys = {
+    kCommentProperty, kLocationProperty, kHmsDbOwnerProperty, 
kHmsDbOwnerTypeProperty};
+
+constexpr std::string_view kExternalTablePurgeProperty = 
"external.table.purge";
+
+// HMS `PrincipalType` names accepted for a database owner.
+constexpr std::array<std::string_view, 3> kPrincipalTypes = {"USER", "GROUP", 
"ROLE"};
+
+bool IsDatabaseReservedKey(std::string_view key) {
+  return std::ranges::find(kDatabaseReservedKeys, key) != 
kDatabaseReservedKeys.end();
+}
+
+std::string GetOrEmpty(const std::unordered_map<std::string, std::string>& 
properties,
+                       std::string_view key) {
+  auto it = properties.find(std::string(key));
+  return it == properties.end() ? std::string() : it->second;
+}
+
+std::string TrimTrailingSlash(std::string_view path) {
+  while (path.size() > 1 && path.back() == '/') {
+    path.remove_suffix(1);
+  }
+  return std::string(path);
+}
+
+}  // namespace
+
+Status ValidateNamespace(const Namespace& ns) {
+  if (ns.levels.size() != 1) {
+    return InvalidArgument(
+        "Hive Metastore only supports single-level namespaces; got {} 
level(s).",
+        ns.levels.size());
+  }
+  if (ns.levels[0].empty()) {
+    return InvalidArgument("Hive namespace cannot have an empty name.");
+  }
+  return {};
+}
+
+Status ValidateOwnerSettings(
+    const std::unordered_map<std::string, std::string>& properties) {
+  auto owner_type = properties.find(std::string(kHmsDbOwnerTypeProperty));
+  if (owner_type == properties.end()) {
+    return {};
+  }
+  if (!properties.contains(std::string(kHmsDbOwnerProperty))) {
+    return InvalidArgument("Hive namespace property '{}' requires '{}' to also 
be set.",
+                           kHmsDbOwnerTypeProperty, kHmsDbOwnerProperty);
+  }
+  const bool known =
+      std::ranges::find(kPrincipalTypes, owner_type->second) != 
kPrincipalTypes.end();
+  if (!known) {
+    return InvalidArgument(
+        "Hive namespace property '{}' has value '{}'; expected one of "
+        "USER, GROUP or ROLE.",
+        kHmsDbOwnerTypeProperty, owner_type->second);
+  }
+  return {};
+}
+
+Result<HiveDatabase> ConvertToHiveDatabase(
+    const Namespace& ns, const std::unordered_map<std::string, std::string>& 
properties) {
+  ICEBERG_RETURN_UNEXPECTED(ValidateNamespace(ns));
+  ICEBERG_RETURN_UNEXPECTED(ValidateOwnerSettings(properties));
+
+  HiveDatabase database;
+  database.name = ns.levels[0];
+  database.description = GetOrEmpty(properties, kCommentProperty);
+  database.location_uri = GetOrEmpty(properties, kLocationProperty);
+  database.owner_name = GetOrEmpty(properties, kHmsDbOwnerProperty);
+  database.owner_type = GetOrEmpty(properties, kHmsDbOwnerTypeProperty);
+
+  for (const auto& [key, value] : properties) {
+    if (!IsDatabaseReservedKey(key)) {
+      database.parameters.emplace(key, value);
+    }
+  }
+  return database;
+}
+
+HiveNamespace ConvertFromHiveDatabase(const HiveDatabase& database) {
+  HiveNamespace result;
+  result.ns.levels = {database.name};
+  result.properties = database.parameters;
+  if (!database.description.empty()) {
+    result.properties[std::string(kCommentProperty)] = database.description;
+  }
+  if (!database.location_uri.empty()) {
+    result.properties[std::string(kLocationProperty)] = database.location_uri;
+  }
+  if (!database.owner_name.empty()) {
+    result.properties[std::string(kHmsDbOwnerProperty)] = database.owner_name;
+  }
+  if (!database.owner_type.empty()) {
+    result.properties[std::string(kHmsDbOwnerTypeProperty)] = 
database.owner_type;
+  }
+  return result;
+}
+
+Result<HiveTable> ConvertToHiveTable(
+    const TableIdentifier& identifier, const std::vector<HiveColumn>& columns,
+    std::string_view metadata_location, std::string_view location,
+    const std::unordered_map<std::string, std::string>& table_properties) {
+  ICEBERG_RETURN_UNEXPECTED(ValidateNamespace(identifier.ns));
+  ICEBERG_RETURN_UNEXPECTED(identifier.Validate());
+  if (metadata_location.empty()) {
+    return InvalidArgument("Hive table metadata location cannot be empty.");
+  }
+
+  HiveTable table;
+  table.db_name = identifier.ns.levels[0];
+  table.table_name = identifier.name;
+  table.owner = GetOrEmpty(table_properties, kHmsTableOwnerProperty);
+  table.table_type = "EXTERNAL_TABLE";
+  table.location = std::string(location);
+  table.columns = columns;
+  table.serde = std::string(kLazySimpleSerDe);
+  table.input_format = std::string(kFileInputFormat);
+  table.output_format = std::string(kFileOutputFormat);
+
+  for (const auto& [key, value] : table_properties) {
+    if (StringUtils::EqualsIgnoreCase(key, kHmsTableOwnerProperty)) {
+      continue;
+    }
+    const std::string hms_key = key == TableProperties::kGcEnabled.key()
+                                    ? std::string(kExternalTablePurgeProperty)
+                                    : key;
+    table.parameters.insert_or_assign(hms_key, value);
+  }
+  // The catalog markers take precedence over user properties.
+  table.parameters[std::string(kMetadataLocationKey)] = 
std::string(metadata_location);
+  table.parameters[std::string(kTableTypeKey)] = 
std::string(kTableTypeIceberg);
+  table.parameters[std::string(kExternalKey)] = std::string(kExternalTrue);
+  return table;
+}
+
+Result<std::string> GetMetadataLocation(
+    const std::unordered_map<std::string, std::string>& table_parameters) {
+  auto it = table_parameters.find(std::string(kMetadataLocationKey));
+  if (it == table_parameters.end() || it->second.empty()) {
+    return NotFound("HMS table is missing '{}' parameter; not an Iceberg 
table.",
+                    kMetadataLocationKey);
+  }
+  return it->second;
+}
+
+Status ValidateIcebergTable(
+    const TableIdentifier& identifier,
+    const std::unordered_map<std::string, std::string>& table_parameters) {
+  auto it = table_parameters.find(std::string(kTableTypeKey));
+  if (it == table_parameters.end() || it->second.empty()) {
+    return NoSuchTable(
+        "HMS table {} has no '{}' parameter; refusing to treat it as an 
Iceberg table.",
+        identifier.ToString(), kTableTypeKey);
+  }
+  if (!StringUtils::EqualsIgnoreCase(it->second, kTableTypeIceberg)) {
+    return NoSuchTable("HMS table {} has '{}={}'; expected '{}' 
(case-insensitive).",
+                       identifier.ToString(), kTableTypeKey, it->second,
+                       kTableTypeIceberg);
+  }
+  return {};
+}
+
+std::string GetDefaultTableLocation(std::string_view warehouse, const 
Namespace& ns,
+                                    std::string_view table_name) {
+  std::string base = TrimTrailingSlash(warehouse);
+  std::string ns_part;
+  for (std::size_t i = 0; i < ns.levels.size(); ++i) {
+    if (i > 0) {
+      ns_part += ".";
+    }
+    ns_part += ns.levels[i];
+  }
+  return std::format("{}/{}.db/{}", base, ns_part, table_name);
+}
+
+}  // namespace iceberg::hive
diff --git a/src/iceberg/catalog/hive/hive_utils.h 
b/src/iceberg/catalog/hive/hive_utils.h
new file mode 100644
index 00000000..ad41af72
--- /dev/null
+++ b/src/iceberg/catalog/hive/hive_utils.h
@@ -0,0 +1,131 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <vector>
+
+#include "iceberg/catalog/hive/hive_schema.h"
+#include "iceberg/catalog/hive/iceberg_hive_export.h"
+#include "iceberg/result.h"
+#include "iceberg/table_identifier.h"
+
+/// \file iceberg/catalog/hive/hive_utils.h
+/// \brief Convert Iceberg catalog objects to HMS records.
+
+namespace iceberg::hive {
+
+/// \brief Iceberg-on-HMS parameter keys.
+inline constexpr std::string_view kMetadataLocationKey = "metadata_location";
+inline constexpr std::string_view kTableTypeKey = "table_type";
+inline constexpr std::string_view kTableTypeIceberg = "ICEBERG";
+inline constexpr std::string_view kExternalKey = "EXTERNAL";
+inline constexpr std::string_view kExternalTrue = "TRUE";
+
+/// \brief Property keys lifted into HMS fields.
+inline constexpr std::string_view kCommentProperty = "comment";
+inline constexpr std::string_view kLocationProperty = "location";
+
+/// \brief HMS table owner key.
+inline constexpr std::string_view kHmsTableOwnerProperty = 
"hive.metastore.table.owner";
+
+/// \brief HMS database owner keys.
+inline constexpr std::string_view kHmsDbOwnerProperty = 
"hive.metastore.database.owner";
+inline constexpr std::string_view kHmsDbOwnerTypeProperty =
+    "hive.metastore.database.owner-type";
+
+/// \brief Fallback storage descriptor classes.
+inline constexpr std::string_view kLazySimpleSerDe =
+    "org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe";
+inline constexpr std::string_view kFileInputFormat =
+    "org.apache.hadoop.mapred.FileInputFormat";
+inline constexpr std::string_view kFileOutputFormat =
+    "org.apache.hadoop.mapred.FileOutputFormat";
+
+/// \brief Subset of HMS Database used by HiveCatalog.
+struct ICEBERG_HIVE_EXPORT HiveDatabase {
+  std::string name;
+  std::string description;
+  std::string location_uri;
+  std::unordered_map<std::string, std::string> parameters;
+  std::string owner_name;
+  std::string owner_type;  // "USER", "GROUP" or "ROLE"; empty means 
HMS-default
+};
+
+/// \brief Subset of HMS Table used by HiveCatalog.
+struct ICEBERG_HIVE_EXPORT HiveTable {
+  std::string db_name;
+  std::string table_name;
+  std::string owner;
+  std::string table_type;  // "EXTERNAL_TABLE" for Iceberg tables
+  std::string location;
+  std::vector<HiveColumn> columns;
+  std::unordered_map<std::string, std::string> parameters;
+  std::string serde;
+  std::string input_format;
+  std::string output_format;
+};
+
+/// \brief Namespace and properties read from HMS.
+struct ICEBERG_HIVE_EXPORT HiveNamespace {
+  Namespace ns;
+  std::unordered_map<std::string, std::string> properties;
+};
+
+/// \brief Lift reserved properties into HMS fields. Caller resolves defaults.
+ICEBERG_HIVE_EXPORT Result<HiveDatabase> ConvertToHiveDatabase(
+    const Namespace& ns, const std::unordered_map<std::string, std::string>& 
properties);
+
+/// \brief Extract namespace properties from an HMS database.
+ICEBERG_HIVE_EXPORT HiveNamespace ConvertFromHiveDatabase(const HiveDatabase& 
database);
+
+/// \brief Build an external Iceberg table with a LazySimpleSerDe descriptor.
+/// Translate gc.enabled to external.table.purge. Catalog markers override user
+/// properties. metadata_location must be non-empty; caller resolves the owner.
+ICEBERG_HIVE_EXPORT Result<HiveTable> ConvertToHiveTable(
+    const TableIdentifier& identifier, const std::vector<HiveColumn>& columns,
+    std::string_view metadata_location, std::string_view location,
+    const std::unordered_map<std::string, std::string>& table_properties);
+
+/// \brief Return metadata_location, or kNotFound if absent or empty.
+ICEBERG_HIVE_EXPORT Result<std::string> GetMetadataLocation(
+    const std::unordered_map<std::string, std::string>& table_parameters);
+
+/// \brief Require table_type=ICEBERG (case-insensitive).
+ICEBERG_HIVE_EXPORT Status ValidateIcebergTable(
+    const TableIdentifier& identifier,
+    const std::unordered_map<std::string, std::string>& table_parameters);
+
+/// \brief Return <warehouse>/<namespace>.db/<table_name>.
+ICEBERG_HIVE_EXPORT std::string GetDefaultTableLocation(std::string_view 
warehouse,
+                                                        const Namespace& ns,
+                                                        std::string_view 
table_name);
+
+/// \brief Require a single-level namespace.
+ICEBERG_HIVE_EXPORT Status ValidateNamespace(const Namespace& ns);
+
+/// \brief Require owner when owner-type is set.
+/// owner-type must be USER, GROUP or ROLE.
+ICEBERG_HIVE_EXPORT Status
+ValidateOwnerSettings(const std::unordered_map<std::string, std::string>& 
properties);
+
+}  // namespace iceberg::hive
diff --git a/src/iceberg/cpp20_compatibility_internal.h 
b/src/iceberg/cpp20_compatibility_internal.h
index 71a5ccce..5c54b544 100644
--- a/src/iceberg/cpp20_compatibility_internal.h
+++ b/src/iceberg/cpp20_compatibility_internal.h
@@ -213,6 +213,8 @@
 #ifdef ICEBERG_PUBLIC_HEADERS_HIVE
 #  include "iceberg/catalog/hive/hive_catalog.h"
 #  include "iceberg/catalog/hive/hive_catalog_properties.h"
+#  include "iceberg/catalog/hive/hive_schema.h"
+#  include "iceberg/catalog/hive/hive_utils.h"
 #  include "iceberg/catalog/hive/hms_client.h"
 #  include "iceberg/catalog/hive/iceberg_hive_export.h"
 #endif  // ICEBERG_PUBLIC_HEADERS_HIVE
diff --git a/src/iceberg/test/CMakeLists.txt b/src/iceberg/test/CMakeLists.txt
index 46b1982c..956a0cbd 100644
--- a/src/iceberg/test/CMakeLists.txt
+++ b/src/iceberg/test/CMakeLists.txt
@@ -362,7 +362,11 @@ if(ICEBERG_BUILD_HIVE)
     add_test(NAME ${test_name} COMMAND ${test_name})
   endfunction()
 
-  add_hive_iceberg_test(hive_catalog_test SOURCES hms_client_test.cc)
+  add_hive_iceberg_test(hive_catalog_test
+                        SOURCES
+                        hive_schema_test.cc
+                        hive_utils_test.cc
+                        hms_client_test.cc)
 endif()
 
 if(ICEBERG_CXX20_COMPAT)
diff --git a/src/iceberg/test/hive_schema_test.cc 
b/src/iceberg/test/hive_schema_test.cc
new file mode 100644
index 00000000..3eb88a30
--- /dev/null
+++ b/src/iceberg/test/hive_schema_test.cc
@@ -0,0 +1,209 @@
+/*
+ * 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_schema.h"
+
+#include <memory>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+#include "iceberg/schema.h"
+#include "iceberg/schema_field.h"
+#include "iceberg/type.h"
+
+namespace iceberg::hive {
+
+namespace {
+
+// Assert successful conversion.
+std::string ToHive(const std::shared_ptr<Type>& type) {
+  auto result = TypeToHiveString(*type);
+  EXPECT_TRUE(result.has_value())
+      << "TypeToHiveString failed: " << (result ? "" : result.error().message);
+  return result.value_or("");
+}
+
+}  // namespace
+
+TEST(TypeToHiveStringTest, PrimitivesMapToHiveScalarTypes) {
+  EXPECT_EQ(ToHive(boolean()), "boolean");
+  EXPECT_EQ(ToHive(int32()), "int");
+  EXPECT_EQ(ToHive(int64()), "bigint");
+  EXPECT_EQ(ToHive(float32()), "float");
+  EXPECT_EQ(ToHive(float64()), "double");
+  EXPECT_EQ(ToHive(date()), "date");
+  EXPECT_EQ(ToHive(timestamp()), "timestamp");
+}
+
+TEST(TypeToHiveStringTest, TimeStringUuidAllMapToString) {
+  EXPECT_EQ(ToHive(time()), "string");
+  EXPECT_EQ(ToHive(string()), "string");
+  EXPECT_EQ(ToHive(uuid()), "string");
+}
+
+TEST(TypeToHiveStringTest, BinaryAndFixedMapToBinary) {
+  EXPECT_EQ(ToHive(binary()), "binary");
+  EXPECT_EQ(ToHive(fixed(16)), "binary");
+}
+
+TEST(SchemaToHiveColumnsTest, VariantUsesHiveMetadataPlaceholder) {
+  Schema schema({SchemaField(1, "payload", variant(), true)});
+  auto columns = SchemaToHiveColumns(schema);
+  ASSERT_TRUE(columns.has_value()) << columns.error().message;
+  ASSERT_EQ(columns->size(), 1);
+  EXPECT_EQ(columns->front().name, "payload");
+  EXPECT_EQ(columns->front().type_string, "unknown");
+}
+
+TEST(TypeToHiveStringTest, UnsupportedTypesReturnNotImplemented) {
+  const std::vector<std::shared_ptr<Type>> types = {timestamp_ns(), 
timestamptz_ns(),
+                                                    unknown(), geometry(), 
geography()};
+  for (const auto& type : types) {
+    SCOPED_TRACE(type->ToString());
+    auto result = TypeToHiveString(*type);
+    ASSERT_FALSE(result.has_value());
+    EXPECT_EQ(result.error().kind, ErrorKind::kNotImplemented);
+  }
+}
+
+TEST(SchemaToHiveColumnsTest, PropagatesUnsupportedNestedType) {
+  auto list_type = std::make_shared<ListType>(1, timestamp_ns(), true);
+  Schema schema({SchemaField(2, "values", list_type, true)});
+  auto columns = SchemaToHiveColumns(schema);
+  ASSERT_FALSE(columns.has_value());
+  EXPECT_EQ(columns.error().kind, ErrorKind::kNotImplemented);
+}
+
+TEST(TypeToHiveStringTest, DecimalIncludesPrecisionAndScale) {
+  EXPECT_EQ(ToHive(decimal(10, 2)), "decimal(10,2)");
+  EXPECT_EQ(ToHive(decimal(38, 9)), "decimal(38,9)");
+}
+
+TEST(TypeToHiveStringTest, TimestampTzMapsToTimestampWithLocalTimeZone) {
+  EXPECT_EQ(ToHive(timestamp_tz()), "timestamp with local time zone");
+}
+
+TEST(TypeToHiveStringTest, StructFlattensFieldList) {
+  StructType struct_type(
+      {SchemaField(1, "a", int32(), true), SchemaField(2, "b", string(), 
true)});
+  auto result = TypeToHiveString(struct_type);
+  ASSERT_TRUE(result.has_value()) << result.error().message;
+  EXPECT_EQ(*result, "struct<a:int,b:string>");
+}
+
+TEST(TypeToHiveStringTest, ListWrapsElement) {
+  ListType list_type(1, int32(), true);
+  auto result = TypeToHiveString(list_type);
+  ASSERT_TRUE(result.has_value()) << result.error().message;
+  EXPECT_EQ(*result, "array<int>");
+}
+
+TEST(TypeToHiveStringTest, MapEmitsKeyAndValue) {
+  MapType map_type(SchemaField(1, "key", string(), false),
+                   SchemaField(2, "value", int32(), true));
+  auto result = TypeToHiveString(map_type);
+  ASSERT_TRUE(result.has_value()) << result.error().message;
+  EXPECT_EQ(*result, "map<string,int>");
+}
+
+TEST(TypeToHiveStringTest, NestedMapInsideMap) {
+  // map<string, map<string, int>>
+  auto inner_map = std::make_shared<MapType>(SchemaField(3, "key", string(), 
false),
+                                             SchemaField(4, "value", int32(), 
true));
+  MapType outer_map(SchemaField(1, "key", string(), false),
+                    SchemaField(2, "value", inner_map, true));
+  auto result = TypeToHiveString(outer_map);
+  ASSERT_TRUE(result.has_value()) << result.error().message;
+  EXPECT_EQ(*result, "map<string,map<string,int>>");
+}
+
+TEST(TypeToHiveStringTest, StructInsideListEmitsArrayOfStruct) {
+  auto struct_type = std::make_shared<StructType>(std::vector<SchemaField>{
+      SchemaField(2, "name", string(), true), SchemaField(3, "age", int32(), 
true)});
+  ListType list_type(SchemaField(1, "element", struct_type, true));
+  auto result = TypeToHiveString(list_type);
+  ASSERT_TRUE(result.has_value()) << result.error().message;
+  EXPECT_EQ(*result, "array<struct<name:string,age:int>>");
+}
+
+TEST(TypeToHiveStringTest, NestedTimestampTzKeepsTheMultiWordTypeName) {
+  StructType struct_type(
+      {SchemaField(1, "a", int32(), true), SchemaField(2, "b", timestamp_tz(), 
true)});
+  auto result = TypeToHiveString(struct_type);
+  ASSERT_TRUE(result.has_value()) << result.error().message;
+  EXPECT_EQ(*result, "struct<a:int,b:timestamp with local time zone>");
+}
+
+TEST(SchemaToHiveColumnsTest, FlatSchemaProducesOneColumnPerField) {
+  Schema schema(
+      {SchemaField(1, "id", int64(), true), SchemaField(2, "name", string(), 
true)},
+      /*schema_id=*/100);
+
+  auto columns = SchemaToHiveColumns(schema);
+  ASSERT_TRUE(columns.has_value()) << columns.error().message;
+  ASSERT_EQ(columns->size(), 2);
+
+  EXPECT_EQ((*columns)[0].name, "id");
+  EXPECT_EQ((*columns)[0].type_string, "bigint");
+  EXPECT_TRUE((*columns)[0].comment.empty());
+
+  EXPECT_EQ((*columns)[1].name, "name");
+  EXPECT_EQ((*columns)[1].type_string, "string");
+}
+
+TEST(SchemaToHiveColumnsTest, NestedFieldRendersAsFlattenedTypeString) {
+  auto address = std::make_shared<StructType>(std::vector<SchemaField>{
+      SchemaField(10, "street", string(), true), SchemaField(11, "zip", 
int32(), true)});
+  Schema schema(
+      {SchemaField(1, "id", int64(), true), SchemaField(2, "address", address, 
true)},
+      /*schema_id=*/200);
+
+  auto columns = SchemaToHiveColumns(schema);
+  ASSERT_TRUE(columns.has_value()) << columns.error().message;
+  ASSERT_EQ(columns->size(), 2);
+  EXPECT_EQ((*columns)[1].name, "address");
+  EXPECT_EQ((*columns)[1].type_string, "struct<street:string,zip:int>");
+}
+
+TEST(SchemaToHiveColumnsTest, FieldDocBecomesColumnComment) {
+  SchemaField id_field(1, "id", int64(), true, "Synthetic primary key.");
+  Schema schema({std::move(id_field)}, /*schema_id=*/1);
+
+  auto columns = SchemaToHiveColumns(schema);
+  ASSERT_TRUE(columns.has_value()) << columns.error().message;
+  ASSERT_EQ(columns->size(), 1);
+  EXPECT_EQ((*columns)[0].comment, "Synthetic primary key.");
+}
+
+TEST(SchemaToHiveColumnsTest, TimestampTzColumnIsTzAdjustedInTheDdl) {
+  // Both timestamp types must stay distinct.
+  Schema schema({SchemaField(1, "ts", timestamp_tz(), true),
+                 SchemaField(2, "naive", timestamp(), true)},
+                /*schema_id=*/1);
+  auto columns = SchemaToHiveColumns(schema);
+  ASSERT_TRUE(columns.has_value()) << columns.error().message;
+  ASSERT_EQ(columns->size(), 2);
+  EXPECT_EQ((*columns)[0].name, "ts");
+  EXPECT_EQ((*columns)[0].type_string, "timestamp with local time zone");
+  EXPECT_EQ((*columns)[1].name, "naive");
+  EXPECT_EQ((*columns)[1].type_string, "timestamp");
+}
+
+}  // namespace iceberg::hive
diff --git a/src/iceberg/test/hive_utils_test.cc 
b/src/iceberg/test/hive_utils_test.cc
new file mode 100644
index 00000000..ba34808a
--- /dev/null
+++ b/src/iceberg/test/hive_utils_test.cc
@@ -0,0 +1,262 @@
+/*
+ * 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

Reply via email to