mirror of
https://github.com/hanzoai/datastore.git
synced 2026-09-26 13:55:07 +00:00
1443 lines
63 KiB
C++
1443 lines
63 KiB
C++
#include <Storages/ObjectStorage/DataLakes/Iceberg/SnapshotSummary.h>
|
|
#include <base/defines.h>
|
|
#include <DataTypes/DataTypeString.h>
|
|
#include <base/sleep.h>
|
|
#include "config.h"
|
|
|
|
#if USE_AVRO
|
|
|
|
#include <cstddef>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <Columns/ColumnConst.h>
|
|
#include <Columns/ColumnSet.h>
|
|
#include <Core/UUID.h>
|
|
#include <DataTypes/DataTypeSet.h>
|
|
#include <DataTypes/DataTypesNumber.h>
|
|
#include <Formats/FormatFilterInfo.h>
|
|
#include <Formats/FormatParserSharedResources.h>
|
|
#include <Formats/ReadSchemaUtils.h>
|
|
#include <Functions/FunctionFactory.h>
|
|
#include <Functions/IFunctionAdaptors.h>
|
|
#include <Functions/tuple.h>
|
|
#include <Processors/Formats/ISchemaReader.h>
|
|
#include <Processors/Sources/SourceFromSingleChunk.h>
|
|
#include <Processors/Formats/Impl/ParquetV3BlockInputFormat.h>
|
|
#include <Processors/Transforms/FilterTransform.h>
|
|
#include <QueryPipeline/QueryPipelineBuilder.h>
|
|
#include <Storages/ObjectStorage/StorageObjectStorageConfiguration.h>
|
|
#include <fmt/format.h>
|
|
#include <Poco/JSON/Array.h>
|
|
#include <Poco/JSON/Object.h>
|
|
#include <Poco/JSON/Stringifier.h>
|
|
#include <Common/Exception.h>
|
|
|
|
#include <Interpreters/PreparedSets.h>
|
|
#include <Storages/ObjectStorage/Utils.h>
|
|
|
|
#include <Core/ColumnsWithTypeAndName.h>
|
|
#include <Core/ServerSettings.h>
|
|
#include <Core/Settings.h>
|
|
#include <Core/NamesAndTypes.h>
|
|
#include <Databases/DataLake/Common.h>
|
|
#include <Databases/DataLake/ICatalog.h>
|
|
#include <Disks/DiskObjectStorage/ObjectStorages/StoredObject.h>
|
|
#include <Disks/DiskType.h>
|
|
#include <Formats/FormatFactory.h>
|
|
#include <IO/ReadBufferFromFileBase.h>
|
|
#include <IO/ReadBufferFromString.h>
|
|
#include <IO/ReadHelpers.h>
|
|
#include <Parsers/ASTLiteral.h>
|
|
#include <IO/VarInt.h>
|
|
#include <Interpreters/Context.h>
|
|
#include <Interpreters/IcebergMetadataLog.h>
|
|
#include <Interpreters/StorageID.h>
|
|
|
|
#include <Storages/ObjectStorage/DataLakes/Common/Common.h>
|
|
#include <Storages/ObjectStorage/StorageObjectStorageSource.h>
|
|
#include <IO/CompressedReadBufferWrapper.h>
|
|
#include <Interpreters/ExpressionActions.h>
|
|
#include <Storages/ObjectStorage/DataLakes/DataLakeStorageSettings.h>
|
|
#include <Storages/ObjectStorage/DataLakes/Iceberg/IcebergMetadataFilesCache.h>
|
|
|
|
#include <Disks/DiskObjectStorage/ObjectStorages/IObjectStorage.h>
|
|
#include <Storages/ColumnsDescription.h>
|
|
#include <Storages/ObjectStorage/DataLakes/Common/AvroForIcebergDeserializer.h>
|
|
#include <Storages/ObjectStorage/DataLakes/Iceberg/Constant.h>
|
|
#include <Storages/ObjectStorage/DataLakes/Iceberg/ExpireSnapshotsExecute.h>
|
|
#include <Storages/ObjectStorage/DataLakes/Iceberg/RemoveOrphanFilesExecute.h>
|
|
#include <Storages/ObjectStorage/DataLakes/Iceberg/IcebergDataObjectInfo.h>
|
|
#include <Storages/ObjectStorage/DataLakes/Iceberg/IcebergIterator.h>
|
|
#include <Storages/ObjectStorage/DataLakes/Iceberg/IcebergMetadata.h>
|
|
#include <Storages/ObjectStorage/DataLakes/Iceberg/IcebergTableStateSnapshot.h>
|
|
#include <Storages/ObjectStorage/DataLakes/Iceberg/IcebergWrites.h>
|
|
#include <Storages/ObjectStorage/DataLakes/Iceberg/ManifestFile.h>
|
|
#include <Storages/ObjectStorage/DataLakes/Iceberg/Compaction.h>
|
|
#include <Storages/ObjectStorage/DataLakes/Iceberg/ManifestFilesPruning.h>
|
|
#include <Storages/ObjectStorage/DataLakes/Iceberg/Mutations.h>
|
|
#include <Storages/ObjectStorage/DataLakes/Iceberg/PositionDeleteTransform.h>
|
|
#include <Storages/ObjectStorage/DataLakes/Iceberg/Snapshot.h>
|
|
#include <Storages/ObjectStorage/DataLakes/Iceberg/StatelessMetadataFileGetter.h>
|
|
#include <Storages/ObjectStorage/DataLakes/Iceberg/Utils.h>
|
|
|
|
#include <Storages/IStorage.h>
|
|
#include <Common/FieldVisitorToString.h>
|
|
|
|
#include <Common/ProfileEvents.h>
|
|
#include <Common/SharedLockGuard.h>
|
|
#include <Common/logger_useful.h>
|
|
|
|
#include <IO/WriteHelpers.h>
|
|
#include <Storages/ObjectStorage/DataLakes/Iceberg/SchemaProcessor.h>
|
|
|
|
namespace ProfileEvents
|
|
{
|
|
extern const Event IcebergIteratorInitializationMicroseconds;
|
|
extern const Event IcebergMetadataUpdateMicroseconds;
|
|
extern const Event IcebergTrivialCountOptimizationApplied;
|
|
}
|
|
|
|
namespace DB
|
|
{
|
|
|
|
namespace DataLakeStorageSetting
|
|
{
|
|
extern const DataLakeStorageSettingsString iceberg_metadata_file_path;
|
|
extern const DataLakeStorageSettingsString iceberg_metadata_table_uuid;
|
|
extern const DataLakeStorageSettingsUInt32 iceberg_metadata_async_prefetch_period_ms;
|
|
extern const DataLakeStorageSettingsBool iceberg_recent_metadata_file_by_last_updated_ms_field;
|
|
extern const DataLakeStorageSettingsBool iceberg_use_version_hint;
|
|
extern const DataLakeStorageSettingsNonZeroUInt64 iceberg_format_version;
|
|
}
|
|
|
|
namespace ErrorCodes
|
|
{
|
|
extern const int BAD_ARGUMENTS;
|
|
extern const int LOGICAL_ERROR;
|
|
extern const int NOT_IMPLEMENTED;
|
|
extern const int ICEBERG_SPECIFICATION_VIOLATION;
|
|
extern const int S3_ERROR;
|
|
extern const int TABLE_ALREADY_EXISTS;
|
|
extern const int SUPPORT_IS_DISABLED;
|
|
}
|
|
|
|
namespace Setting
|
|
{
|
|
extern const SettingsNonZeroUInt64 max_block_size;
|
|
extern const SettingsUInt64 max_bytes_in_set;
|
|
extern const SettingsUInt64 max_rows_in_set;
|
|
extern const SettingsOverflowMode set_overflow_mode;
|
|
extern const SettingsInt64 iceberg_timestamp_ms;
|
|
extern const SettingsInt64 iceberg_snapshot_id;
|
|
extern const SettingsBool use_iceberg_metadata_files_cache;
|
|
extern const SettingsBool use_iceberg_partition_pruning;
|
|
extern const SettingsBool write_full_path_in_iceberg_metadata;
|
|
extern const SettingsBool use_roaring_bitmap_iceberg_positional_deletes;
|
|
extern const SettingsString iceberg_metadata_compression_method;
|
|
extern const SettingsBool allow_insert_into_iceberg;
|
|
extern const SettingsBool allow_experimental_iceberg_compaction;
|
|
extern const SettingsBool allow_experimental_geo_types_in_iceberg;
|
|
extern const SettingsBool allow_iceberg_remove_orphan_files;
|
|
extern const SettingsBool allow_experimental_expire_snapshots;
|
|
extern const SettingsBool iceberg_delete_data_on_drop;
|
|
extern const SettingsSeconds lock_acquire_timeout;
|
|
extern const SettingsSeconds iceberg_compaction_delay_bias;
|
|
extern const SettingsSeconds iceberg_compaction_data_cleanup;
|
|
extern const SettingsBool allow_experimental_cleanup_old_data_files_compaction;
|
|
extern const SettingsUInt64 iceberg_insert_max_rows_in_data_file;
|
|
extern const SettingsUInt64 iceberg_insert_max_bytes_in_data_file;
|
|
extern const SettingsUInt64 iceberg_max_number_datafiles_to_compact;
|
|
extern const SettingsUInt64 iceberg_data_file_size_lower_threshold_compaction;
|
|
extern const SettingsUInt64 iceberg_data_file_size_upper_threshold_compaction;
|
|
}
|
|
|
|
namespace
|
|
{
|
|
String dumpMetadataObjectToString(const Poco::JSON::Object::Ptr & metadata_object)
|
|
{
|
|
return stringifyJSON(metadata_object);
|
|
}
|
|
}
|
|
|
|
|
|
using namespace Iceberg;
|
|
|
|
namespace
|
|
{
|
|
Iceberg::TableStateSnapshotPtr extractIcebergSnapshotIdFromMetadataObject(StorageMetadataPtr storage_metadata)
|
|
{
|
|
if (!storage_metadata || !storage_metadata->datalake_table_state.has_value())
|
|
return nullptr;
|
|
chassert(std::holds_alternative<TableStateSnapshot>(storage_metadata->datalake_table_state.value()));
|
|
return std::make_shared<TableStateSnapshot>(std::get<TableStateSnapshot>(storage_metadata->datalake_table_state.value()));
|
|
}
|
|
}
|
|
|
|
Iceberg::PersistentTableComponents IcebergMetadata::initializePersistentTableComponents(
|
|
ObjectStoragePtr object_storage,
|
|
StorageObjectStorageConfigurationPtr configuration,
|
|
IcebergMetadataFilesCachePtr cache_ptr,
|
|
ContextPtr context_,
|
|
LoggerPtr log)
|
|
{
|
|
const auto [metadata_version, metadata_file_path, compression_method]
|
|
= getLatestOrExplicitMetadataFileAndVersion(object_storage, configuration->getPathForRead().path, configuration->getDataLakeSettings(), cache_ptr, context_, log.get(), std::nullopt, CompressionMethod::None, true);
|
|
LOG_DEBUG(log, "Latest metadata file path is {}, version {}", metadata_file_path, metadata_version);
|
|
auto metadata_object
|
|
= getMetadataJSONObject(metadata_file_path, object_storage, cache_ptr, context_, log, compression_method, std::nullopt);
|
|
Int32 format_version = metadata_object->getValue<Int32>(f_format_version);
|
|
String table_location = metadata_object->getValue<String>(f_location);
|
|
std::optional<String> table_uuid = std::nullopt;
|
|
if (metadata_object->has(Iceberg::f_table_uuid))
|
|
{
|
|
table_uuid = normalizeUuid(metadata_object->getValue<String>(f_table_uuid));
|
|
}
|
|
else
|
|
{
|
|
if (format_version >= 2)
|
|
{
|
|
throw Exception(
|
|
ErrorCodes::ICEBERG_SPECIFICATION_VIOLATION,
|
|
"Iceberg table metadata file '{}' doesn't contain required field '{}'",
|
|
metadata_file_path,
|
|
Iceberg::f_table_uuid);
|
|
}
|
|
}
|
|
auto table_path = configuration->getPathForRead().path;
|
|
return PersistentTableComponents{
|
|
.schema_processor = std::make_shared<IcebergSchemaProcessor>(context_->getSettingsRef()[Setting::allow_experimental_geo_types_in_iceberg]),
|
|
.metadata_cache = cache_ptr,
|
|
.format_version = format_version,
|
|
.table_location = table_location,
|
|
.metadata_compression_method = compression_method,
|
|
.table_path = table_path,
|
|
.table_uuid = table_uuid,
|
|
.path_resolver = IcebergPathResolver(table_location, table_path, configuration->getTypeName(), configuration->getNamespace()),
|
|
};
|
|
}
|
|
|
|
std::pair<IcebergDataSnapshotPtr, TableStateSnapshot> IcebergMetadata::getRelevantState(const ContextPtr & context, bool force_fetch_latest_metadata) const
|
|
{
|
|
const auto [metadata_version, metadata_file_path, compression_method] = getLatestOrExplicitMetadataFileAndVersion(
|
|
object_storage,
|
|
persistent_components.table_path,
|
|
data_lake_settings,
|
|
persistent_components.metadata_cache,
|
|
context,
|
|
log.get(),
|
|
persistent_components.table_uuid,
|
|
persistent_components.metadata_compression_method,
|
|
force_fetch_latest_metadata);
|
|
return getState(context, metadata_file_path, metadata_version);
|
|
}
|
|
|
|
IcebergMetadata::IcebergMetadata(
|
|
ObjectStoragePtr object_storage_,
|
|
StorageObjectStorageConfigurationPtr configuration_,
|
|
Iceberg::PersistentTableComponents && persistent_components_,
|
|
ContextPtr context_)
|
|
: log(getLogger("IcebergMetadata"))
|
|
, object_storage(std::move(object_storage_))
|
|
, persistent_components(std::move(persistent_components_))
|
|
, data_lake_settings(configuration_->getDataLakeSettings())
|
|
, write_format(configuration_->format)
|
|
{
|
|
/// TODO: for now it's okay to start/stop the task via constructor/destructor. Once refactored, we'd need to plumb startup/shutdown and schedule the task from there
|
|
if (persistent_components.metadata_cache && data_lake_settings[DataLakeStorageSetting::iceberg_metadata_async_prefetch_period_ms] != 0)
|
|
{
|
|
background_metadata_prefetch_task = context_->getIcebergSchedulePool().createTask(
|
|
StorageID("", persistent_components.table_uuid ? *persistent_components.table_uuid : persistent_components.table_path),
|
|
"backgroundMetadataPrefetcherThread",
|
|
[this]
|
|
{
|
|
this->backgroundMetadataPrefetcherThread();
|
|
}
|
|
);
|
|
background_metadata_prefetch_task->activateAndSchedule();
|
|
}
|
|
}
|
|
|
|
IcebergMetadata::~IcebergMetadata()
|
|
{
|
|
if (background_metadata_prefetch_task)
|
|
background_metadata_prefetch_task->deactivate();
|
|
}
|
|
|
|
void IcebergMetadata::backgroundMetadataPrefetcherThread()
|
|
{
|
|
size_t interval = data_lake_settings[DataLakeStorageSetting::iceberg_metadata_async_prefetch_period_ms];
|
|
SCOPE_EXIT({
|
|
background_metadata_prefetch_task->scheduleAfter(interval);
|
|
});
|
|
|
|
try
|
|
{
|
|
if (!Context::getGlobalContextInstance())
|
|
{
|
|
/// Should never happen, but if seen, this is clear indicator that the task should be started/stopped via startup/shutdown mechanism (check TODOs above)
|
|
LOG_DEBUG(log, "backgroundMetadataPrefetcherThread: no global context - skipping");
|
|
return;
|
|
}
|
|
|
|
Stopwatch watch;
|
|
|
|
/// TODO: also we'd want to run all these download operations as separate scheduled tasks - to parallelize it and
|
|
/// to prevent running a heavy multi-step operation as: IO > deserialization > parsing > IO > deserialization > parsing > ...
|
|
/// We'll be able to achieve that after getting asyncIterator refactoring
|
|
|
|
/// first, we fetch the latest metadata version and cache it;
|
|
/// as a part of the same method, we download metadata.json of the latest metadata version
|
|
/// and after parsing it, we fetch manifest lists, parse and cache them
|
|
auto ctx = Context::getGlobalContextInstance()->getBackgroundContext();
|
|
auto [actual_data_snapshot, actual_table_state_snapshot] = getRelevantState(ctx, true);
|
|
if (actual_data_snapshot)
|
|
{
|
|
for (const auto & entry : actual_data_snapshot->manifest_list_entries)
|
|
{
|
|
/// second, we fetch, parse and cache each manifest file
|
|
auto manifest_file_ptr = getManifestFileEntriesHandle(
|
|
object_storage, persistent_components, ctx, log, entry, actual_table_state_snapshot.schema_id);
|
|
}
|
|
}
|
|
|
|
LOG_TRACE(log, "backgroundMetadataPrefetcherThread: interval={} prefetch_time_ms={} table_path={}/{} latest_metadata={}/{}",
|
|
interval,
|
|
watch.elapsedMilliseconds(),
|
|
persistent_components.table_path,
|
|
persistent_components.table_uuid ? *(persistent_components.table_uuid) : "no_uuid",
|
|
actual_table_state_snapshot.metadata_version,
|
|
actual_table_state_snapshot.metadata_file_path);
|
|
}
|
|
catch (...)
|
|
{
|
|
DB::tryLogCurrentException(log);
|
|
}
|
|
}
|
|
|
|
Int32 IcebergMetadata::parseTableSchema(
|
|
const Poco::JSON::Object::Ptr & metadata_object,
|
|
IcebergSchemaProcessor & schema_processor,
|
|
LoggerPtr metadata_logger)
|
|
{
|
|
const auto format_version = metadata_object->getValue<Int32>(f_format_version);
|
|
|
|
if (format_version == 2)
|
|
{
|
|
auto [schema, current_schema_id] = parseTableSchemaV2Method(metadata_object);
|
|
schema_processor.addIcebergTableSchema(schema);
|
|
return current_schema_id;
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
auto [schema, current_schema_id] = parseTableSchemaV1Method(metadata_object);
|
|
schema_processor.addIcebergTableSchema(schema);
|
|
return current_schema_id;
|
|
}
|
|
catch (const Exception & first_error)
|
|
{
|
|
if (first_error.code() != ErrorCodes::BAD_ARGUMENTS)
|
|
throw;
|
|
try
|
|
{
|
|
auto [schema, current_schema_id] = parseTableSchemaV2Method(metadata_object);
|
|
schema_processor.addIcebergTableSchema(schema);
|
|
LOG_WARNING(
|
|
metadata_logger,
|
|
"Iceberg table schema was parsed using v2 specification, but it was impossible to parse it using v1 "
|
|
"specification. Be "
|
|
"aware that you Iceberg writing engine violates Iceberg specification. Error during parsing {}",
|
|
first_error.displayText());
|
|
return current_schema_id;
|
|
}
|
|
catch (const Exception & second_error)
|
|
{
|
|
if (first_error.code() != ErrorCodes::BAD_ARGUMENTS)
|
|
throw;
|
|
throw Exception(
|
|
ErrorCodes::BAD_ARGUMENTS,
|
|
"Cannot parse Iceberg table schema both with v1 and v2 methods. Old method error: {}. New method error: {}",
|
|
first_error.displayText(),
|
|
second_error.displayText());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static Poco::JSON::Object::Ptr traverseMetadataAndFindNecessarySnapshotObject(
|
|
Poco::JSON::Object::Ptr metadata_object, Int64 snapshot_id, IcebergSchemaProcessorPtr schema_processor)
|
|
{
|
|
if (!metadata_object->has(f_snapshots))
|
|
throw Exception(ErrorCodes::ICEBERG_SPECIFICATION_VIOLATION, "No snapshot set found in metadata for iceberg file");
|
|
auto schemas = metadata_object->get(f_schemas).extract<Poco::JSON::Array::Ptr>();
|
|
for (UInt32 j = 0; j < schemas->size(); ++j)
|
|
{
|
|
auto schema = schemas->getObject(j);
|
|
schema_processor->addIcebergTableSchema(schema);
|
|
}
|
|
Poco::JSON::Object::Ptr current_snapshot = nullptr;
|
|
auto snapshots = metadata_object->get(f_snapshots).extract<Poco::JSON::Array::Ptr>();
|
|
for (size_t i = 0; i < snapshots->size(); ++i)
|
|
{
|
|
const auto snapshot = snapshots->getObject(static_cast<UInt32>(i));
|
|
auto current_snapshot_id = snapshot->getValue<Int64>(f_metadata_snapshot_id);
|
|
auto current_schema_id = snapshot->getValue<Int32>(f_schema_id);
|
|
schema_processor->registerSnapshotWithSchemaId(current_snapshot_id, current_schema_id);
|
|
if (snapshot->getValue<Int64>(f_metadata_snapshot_id) == snapshot_id)
|
|
{
|
|
current_snapshot = snapshot;
|
|
}
|
|
}
|
|
return current_snapshot;
|
|
}
|
|
|
|
IcebergDataSnapshotPtr IcebergMetadata::createIcebergDataSnapshotFromSnapshotJSON(
|
|
Poco::JSON::Object::Ptr snapshot_object, Int64 snapshot_id, ContextPtr local_context) const
|
|
{
|
|
if (!snapshot_object->has(f_manifest_list))
|
|
throw Exception(
|
|
ErrorCodes::ICEBERG_SPECIFICATION_VIOLATION,
|
|
"Snapshot object doesn't contain a manifest list path for snapshot with id `{}`",
|
|
snapshot_id);
|
|
IcebergPathFromMetadata manifest_list_file_path = IcebergPathFromMetadata::deserialize(snapshot_object->getValue<String>(f_manifest_list));
|
|
std::optional<size_t> total_rows;
|
|
std::optional<size_t> total_bytes;
|
|
std::optional<size_t> total_position_deletes;
|
|
|
|
if (snapshot_object->has(f_summary))
|
|
{
|
|
auto summary_object = snapshot_object->get(f_summary).extract<Poco::JSON::Object::Ptr>();
|
|
if (summary_object->has(f_total_records))
|
|
total_rows = summary_object->getValue<Int64>(f_total_records);
|
|
|
|
if (summary_object->has(f_total_files_size))
|
|
total_bytes = summary_object->getValue<Int64>(f_total_files_size);
|
|
|
|
if (summary_object->has(f_total_position_deletes))
|
|
{
|
|
total_position_deletes = summary_object->getValue<Int64>(f_total_position_deletes);
|
|
}
|
|
}
|
|
|
|
if (!snapshot_object->has(f_schema_id))
|
|
throw Exception(ErrorCodes::ICEBERG_SPECIFICATION_VIOLATION, "No schema id found for snapshot id `{}`", snapshot_id);
|
|
Int32 schema_id = snapshot_object->getValue<Int32>(f_schema_id);
|
|
|
|
|
|
return std::make_shared<IcebergDataSnapshot>(
|
|
getManifestList(object_storage, persistent_components, local_context, manifest_list_file_path, log),
|
|
snapshot_id,
|
|
schema_id,
|
|
total_rows,
|
|
total_bytes,
|
|
total_position_deletes);
|
|
}
|
|
|
|
IcebergDataSnapshotPtr
|
|
IcebergMetadata::getIcebergDataSnapshot(Poco::JSON::Object::Ptr metadata_object, Int64 snapshot_id, ContextPtr local_context) const
|
|
{
|
|
auto object = traverseMetadataAndFindNecessarySnapshotObject(metadata_object, snapshot_id, persistent_components.schema_processor);
|
|
if (!object)
|
|
throw Exception(ErrorCodes::ICEBERG_SPECIFICATION_VIOLATION, "No snapshot found for id `{}`", snapshot_id);
|
|
|
|
return createIcebergDataSnapshotFromSnapshotJSON(object, snapshot_id, local_context);
|
|
}
|
|
|
|
bool IcebergMetadata::optimize(
|
|
[[maybe_unused]] const StorageMetadataPtr & metadata_snapshot,
|
|
[[maybe_unused]] ContextPtr context,
|
|
[[maybe_unused]] const std::optional<FormatSettings> & format_settings)
|
|
{
|
|
#if CLICKHOUSE_CLOUD
|
|
if (!compaction_enabled)
|
|
throw Exception(
|
|
ErrorCodes::BAD_ARGUMENTS, "Enable `allow_experimental_iceberg_compaction` setting to call OPTIMIZE for Iceberg tables.");
|
|
|
|
if (!iceberg_compaction_metadata_generator)
|
|
throw Exception(
|
|
ErrorCodes::LOGICAL_ERROR, "Background compaction is not initialized. This is a bug.");
|
|
|
|
iceberg_compaction_metadata_generator->waitUntilUpdated();
|
|
return true;
|
|
#else
|
|
if (context->getSettingsRef()[Setting::allow_experimental_iceberg_compaction])
|
|
{
|
|
const auto sample_block = std::make_shared<const Block>(metadata_snapshot->getSampleBlock());
|
|
auto snapshots_info = getHistory(context);
|
|
compactIcebergTable(
|
|
snapshots_info,
|
|
persistent_components,
|
|
object_storage,
|
|
data_lake_settings,
|
|
format_settings,
|
|
sample_block,
|
|
context,
|
|
write_format);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
throw Exception(
|
|
ErrorCodes::BAD_ARGUMENTS, "Enable 'allow_experimental_iceberg_compaction' setting to call optimize for iceberg tables.");
|
|
}
|
|
#endif
|
|
}
|
|
|
|
std::pair<IcebergDataSnapshotPtr, Int32>
|
|
IcebergMetadata::getStateImpl(const ContextPtr & local_context, Poco::JSON::Object::Ptr metadata_object) const
|
|
{
|
|
std::optional<String> manifest_list_file;
|
|
|
|
bool timestamp_changed = local_context->getSettingsRef()[Setting::iceberg_timestamp_ms].changed;
|
|
bool snapshot_id_changed = local_context->getSettingsRef()[Setting::iceberg_snapshot_id].changed;
|
|
if (timestamp_changed && snapshot_id_changed)
|
|
{
|
|
throw Exception(
|
|
ErrorCodes::BAD_ARGUMENTS,
|
|
"Time travel with timestamp and snapshot id for iceberg table by path {} cannot be changed simultaneously",
|
|
persistent_components.table_path);
|
|
}
|
|
if (timestamp_changed)
|
|
{
|
|
if (!metadata_object->has(f_snapshot_log))
|
|
throw Exception(
|
|
ErrorCodes::BAD_ARGUMENTS,
|
|
"No snapshot log found in metadata for iceberg table {} so it is impossible to get relevant snapshot id using timestamp",
|
|
persistent_components.table_path);
|
|
std::optional<Int64> current_snapshot_id = std::nullopt;
|
|
{
|
|
Int64 closest_timestamp = 0;
|
|
Int64 query_timestamp = local_context->getSettingsRef()[Setting::iceberg_timestamp_ms];
|
|
auto snapshot_log = metadata_object->get(f_snapshot_log).extract<Poco::JSON::Array::Ptr>();
|
|
for (size_t i = 0; i < snapshot_log->size(); ++i)
|
|
{
|
|
const auto snapshot = snapshot_log->getObject(static_cast<UInt32>(i));
|
|
Int64 snapshot_timestamp = snapshot->getValue<Int64>(f_timestamp_ms);
|
|
if (snapshot_timestamp <= query_timestamp && snapshot_timestamp > closest_timestamp)
|
|
{
|
|
closest_timestamp = snapshot_timestamp;
|
|
current_snapshot_id = snapshot->getValue<Int64>(f_metadata_snapshot_id);
|
|
}
|
|
}
|
|
}
|
|
if (!current_snapshot_id)
|
|
throw Exception(
|
|
ErrorCodes::BAD_ARGUMENTS,
|
|
"No snapshot found in snapshot log before requested timestamp for iceberg table {}",
|
|
persistent_components.table_path);
|
|
auto data_snapshot = getIcebergDataSnapshot(metadata_object, *current_snapshot_id, local_context);
|
|
return {data_snapshot, static_cast<Int32>(data_snapshot->schema_id_on_snapshot_commit)};
|
|
}
|
|
else if (snapshot_id_changed)
|
|
{
|
|
Int64 current_snapshot_id = local_context->getSettingsRef()[Setting::iceberg_snapshot_id];
|
|
auto data_snapshot = getIcebergDataSnapshot(metadata_object, current_snapshot_id, local_context);
|
|
return {data_snapshot, static_cast<Int32>(data_snapshot->schema_id_on_snapshot_commit)};
|
|
}
|
|
else
|
|
{
|
|
auto schema_id = parseTableSchema(metadata_object, *persistent_components.schema_processor, log);
|
|
if (!metadata_object->has(f_current_snapshot_id))
|
|
{
|
|
return {nullptr, schema_id};
|
|
}
|
|
Int64 current_snapshot_id = metadata_object->isNull(f_current_snapshot_id)
|
|
? -1
|
|
: metadata_object->getValue<Int64>(f_current_snapshot_id);
|
|
if (current_snapshot_id < 0)
|
|
{
|
|
return {nullptr, schema_id};
|
|
}
|
|
auto data_snapshot = getIcebergDataSnapshot(metadata_object, current_snapshot_id, local_context);
|
|
return {data_snapshot, schema_id};
|
|
}
|
|
}
|
|
|
|
std::pair<IcebergDataSnapshotPtr, TableStateSnapshot>
|
|
IcebergMetadata::getState(const ContextPtr & local_context, const String & metadata_path, Int32 metadata_version) const
|
|
{
|
|
IcebergDataSnapshotPtr data_snapshot;
|
|
TableStateSnapshot table_state_snapshot;
|
|
auto metadata_object = getMetadataJSONObject(
|
|
metadata_path, object_storage, persistent_components.metadata_cache, local_context, log, persistent_components.metadata_compression_method, persistent_components.table_uuid);
|
|
|
|
insertRowToLogTable(
|
|
local_context,
|
|
dumpMetadataObjectToString(metadata_object),
|
|
DB::IcebergMetadataLogLevel::Metadata,
|
|
persistent_components.path_resolver.getTableRoot(),
|
|
Iceberg::IcebergPathFromMetadata::deserialize(metadata_path),
|
|
std::nullopt,
|
|
std::nullopt);
|
|
|
|
std::tie(data_snapshot, table_state_snapshot.schema_id) = getStateImpl(local_context, metadata_object);
|
|
table_state_snapshot.snapshot_id = data_snapshot ? std::optional{data_snapshot->snapshot_id} : std::nullopt;
|
|
table_state_snapshot.metadata_version = metadata_version;
|
|
table_state_snapshot.metadata_file_path = metadata_path;
|
|
return {data_snapshot, table_state_snapshot};
|
|
}
|
|
|
|
std::shared_ptr<NamesAndTypesList> IcebergMetadata::getInitialSchemaByPath(ContextPtr, ObjectInfoPtr object_info) const
|
|
{
|
|
IcebergDataObjectInfo * iceberg_object_info = dynamic_cast<IcebergDataObjectInfo *>(object_info.get());
|
|
if (!iceberg_object_info)
|
|
return nullptr;
|
|
/// if we need schema evolution or have equality deletes files, we need to read all the columns.
|
|
return (iceberg_object_info->info.underlying_format_read_schema_id != iceberg_object_info->info.schema_id_relevant_to_iterator
|
|
|| (!iceberg_object_info->info.equality_deletes_objects.empty()))
|
|
? persistent_components.schema_processor->getClickhouseTableSchemaById(iceberg_object_info->info.underlying_format_read_schema_id)
|
|
: nullptr;
|
|
}
|
|
|
|
std::shared_ptr<const ActionsDAG> IcebergMetadata::getSchemaTransformer(ContextPtr, ObjectInfoPtr object_info) const
|
|
{
|
|
IcebergDataObjectInfo * iceberg_object_info = dynamic_cast<IcebergDataObjectInfo *>(object_info.get());
|
|
if (!iceberg_object_info)
|
|
return nullptr;
|
|
return (iceberg_object_info->info.underlying_format_read_schema_id != iceberg_object_info->info.schema_id_relevant_to_iterator)
|
|
? persistent_components.schema_processor->getSchemaTransformationDagByIds(
|
|
iceberg_object_info->info.underlying_format_read_schema_id, iceberg_object_info->info.schema_id_relevant_to_iterator)
|
|
: nullptr;
|
|
}
|
|
|
|
void IcebergMetadata::mutate(
|
|
const MutationCommands & commands,
|
|
StoragePtr storage_ptr,
|
|
ContextPtr context,
|
|
const StorageID & storage_id,
|
|
StorageMetadataPtr metadata_snapshot,
|
|
std::shared_ptr<DataLake::ICatalog> catalog,
|
|
const std::optional<FormatSettings> & format_settings)
|
|
{
|
|
if (!context->getSettingsRef()[Setting::allow_insert_into_iceberg].value)
|
|
{
|
|
throw Exception(
|
|
ErrorCodes::SUPPORT_IS_DISABLED,
|
|
"Iceberg mutations is experimental. "
|
|
"To allow its usage, enable setting allow_insert_into_iceberg");
|
|
}
|
|
|
|
DB::Iceberg::mutate(
|
|
commands,
|
|
context,
|
|
storage_ptr,
|
|
metadata_snapshot,
|
|
storage_id,
|
|
object_storage,
|
|
data_lake_settings,
|
|
persistent_components,
|
|
write_format,
|
|
format_settings,
|
|
catalog);
|
|
}
|
|
|
|
void IcebergMetadata::checkMutationIsPossible(const MutationCommands & commands)
|
|
{
|
|
for (const auto & command : commands)
|
|
if (command.type != MutationCommand::DELETE && command.type != MutationCommand::UPDATE)
|
|
throw Exception(ErrorCodes::NOT_IMPLEMENTED, "Iceberg supports only DELETE and UPDATE mutations");
|
|
}
|
|
|
|
void IcebergMetadata::checkAlterIsPossible(const AlterCommands & commands)
|
|
{
|
|
for (const auto & command : commands)
|
|
{
|
|
if (command.type != AlterCommand::Type::ADD_COLUMN && command.type != AlterCommand::Type::DROP_COLUMN
|
|
&& command.type != AlterCommand::Type::MODIFY_COLUMN && command.type != AlterCommand::Type::RENAME_COLUMN)
|
|
throw Exception(ErrorCodes::NOT_IMPLEMENTED, "Alter of type '{}' is not supported by Iceberg storage", command.type);
|
|
|
|
if (command.type == AlterCommand::Type::MODIFY_COLUMN && command.to_remove != AlterCommand::RemoveProperty::NO_PROPERTY)
|
|
throw Exception(
|
|
ErrorCodes::NOT_IMPLEMENTED,
|
|
"Removing column property '{}' from column '{}' is not supported by Iceberg storage", command.to_remove, command.column_name);
|
|
|
|
if (command.type == AlterCommand::Type::MODIFY_COLUMN && !command.data_type)
|
|
throw Exception(
|
|
ErrorCodes::NOT_IMPLEMENTED,
|
|
"Modifying column '{}' without changing its type is not supported by Iceberg storage", command.column_name);
|
|
}
|
|
}
|
|
|
|
void IcebergMetadata::alter(
|
|
const AlterCommands & params,
|
|
ContextPtr context,
|
|
const StorageID & storage_id,
|
|
std::shared_ptr<DataLake::ICatalog> catalog)
|
|
{
|
|
if (!context->getSettingsRef()[Setting::allow_insert_into_iceberg].value)
|
|
{
|
|
throw Exception(
|
|
ErrorCodes::SUPPORT_IS_DISABLED,
|
|
"Alter iceberg is experimental. "
|
|
"To allow its usage, enable setting allow_insert_into_iceberg");
|
|
}
|
|
|
|
Iceberg::alter(params, context, storage_id, object_storage, data_lake_settings, persistent_components, write_format, catalog);
|
|
}
|
|
|
|
Pipe IcebergMetadata::executeCommand(
|
|
const String & command_name,
|
|
const ASTPtr & args,
|
|
ObjectStoragePtr object_storage_,
|
|
StorageObjectStorageConfigurationPtr /*configuration*/,
|
|
std::shared_ptr<DataLake::ICatalog> catalog_,
|
|
ContextPtr context,
|
|
const StorageID & storage_id)
|
|
{
|
|
if (!context->getSettingsRef()[Setting::allow_insert_into_iceberg].value)
|
|
{
|
|
throw Exception(
|
|
ErrorCodes::SUPPORT_IS_DISABLED,
|
|
"Iceberg EXECUTE commands are experimental. "
|
|
"To allow their usage, enable setting allow_insert_into_iceberg");
|
|
}
|
|
|
|
if (command_name == "expire_snapshots")
|
|
{
|
|
if (!context->getSettingsRef()[Setting::allow_experimental_expire_snapshots].value)
|
|
{
|
|
throw Exception(
|
|
ErrorCodes::SUPPORT_IS_DISABLED,
|
|
"Iceberg expire_snapshots is experimental. "
|
|
"To allow its usage, enable setting allow_experimental_expire_snapshots");
|
|
}
|
|
|
|
return Iceberg::executeExpireSnapshots(
|
|
args, context, object_storage_, data_lake_settings, persistent_components,
|
|
write_format, catalog_, storage_id.getTableName());
|
|
}
|
|
else if (command_name == "remove_orphan_files")
|
|
{
|
|
if (!context->getSettingsRef()[Setting::allow_iceberg_remove_orphan_files].value)
|
|
{
|
|
throw Exception(
|
|
ErrorCodes::SUPPORT_IS_DISABLED,
|
|
"remove_orphan_files is experimental. "
|
|
"To allow its usage, enable setting allow_iceberg_remove_orphan_files");
|
|
}
|
|
|
|
return Iceberg::executeRemoveOrphanFiles(
|
|
args, context, object_storage_, data_lake_settings, persistent_components);
|
|
}
|
|
else
|
|
{
|
|
throw Exception(ErrorCodes::BAD_ARGUMENTS, "Unknown EXECUTE command '{}' for Iceberg table", command_name);
|
|
}
|
|
}
|
|
|
|
void IcebergMetadata::createInitial(
|
|
const ObjectStoragePtr & object_storage,
|
|
const StorageObjectStorageConfigurationWeakPtr & configuration,
|
|
const ContextPtr & local_context,
|
|
const std::optional<ColumnsDescription> & columns,
|
|
ASTPtr partition_by,
|
|
ASTPtr order_by,
|
|
bool if_not_exists,
|
|
std::shared_ptr<DataLake::ICatalog> catalog,
|
|
const StorageID & table_id_)
|
|
{
|
|
auto configuration_ptr = configuration.lock();
|
|
if (!configuration_ptr)
|
|
throw Exception(ErrorCodes::LOGICAL_ERROR, "Trying to create Iceberg table, but storage configuration is expired");
|
|
|
|
std::vector<String> metadata_files;
|
|
try
|
|
{
|
|
metadata_files = listFiles(*object_storage, configuration_ptr->getPathForRead().path, "metadata", ".metadata.json");
|
|
}
|
|
catch (const Exception & ex)
|
|
{
|
|
throw Exception(ErrorCodes::BAD_ARGUMENTS, "NoSuchBucket: {}", ex.what());
|
|
}
|
|
if (!metadata_files.empty())
|
|
{
|
|
if (if_not_exists)
|
|
return;
|
|
else
|
|
throw Exception(
|
|
ErrorCodes::TABLE_ALREADY_EXISTS, "Iceberg table with path {} already exists", configuration_ptr->getPathForRead().path);
|
|
}
|
|
|
|
String location_path = configuration_ptr->getRawPath().path;
|
|
if (location_path.find("://") == String::npos && !location_path.starts_with('/'))
|
|
location_path = "/" + location_path;
|
|
if (local_context->getSettingsRef()[Setting::write_full_path_in_iceberg_metadata].value)
|
|
location_path
|
|
= configuration_ptr->getTypeName() + "://" + configuration_ptr->getNamespace() + "/" + configuration_ptr->getRawPath().path;
|
|
auto [metadata_content_object, metadata_content] = createEmptyMetadataFile(
|
|
location_path, *columns, partition_by, order_by, local_context, configuration_ptr->getDataLakeSettings()[DataLakeStorageSetting::iceberg_format_version]);
|
|
auto compression_method_str = local_context->getSettingsRef()[Setting::iceberg_metadata_compression_method].value;
|
|
auto compression_method = chooseCompressionMethod(compression_method_str, compression_method_str);
|
|
|
|
auto compression_suffix = compression_method_str;
|
|
if (!compression_suffix.empty())
|
|
compression_suffix = "." + compression_suffix;
|
|
|
|
auto filename = fmt::format("{}metadata/v1{}.metadata.json", configuration_ptr->getRawPath().path, compression_suffix);
|
|
|
|
try
|
|
{
|
|
writeMessageToFile(metadata_content, filename, object_storage, local_context, "*", "", compression_method);
|
|
}
|
|
catch (const Exception & e)
|
|
{
|
|
/// The write uses `If-None-Match: *`, so S3 returns PreconditionFailed when the metadata file
|
|
/// already exists (e.g. leftover data after `DROP TABLE` with `iceberg_delete_data_on_drop` off,
|
|
/// or a concurrent creation). When `IF NOT EXISTS` was specified, this is expected.
|
|
if (if_not_exists && e.code() == ErrorCodes::S3_ERROR
|
|
&& e.message().find("PreconditionFailed") != String::npos)
|
|
return;
|
|
throw;
|
|
}
|
|
|
|
if (configuration_ptr->getDataLakeSettings()[DataLakeStorageSetting::iceberg_use_version_hint].value)
|
|
{
|
|
auto filename_version_hint = configuration_ptr->getRawPath().path + "metadata/version-hint.text";
|
|
writeMessageToFile("1", filename_version_hint, object_storage, local_context, "*", "");
|
|
}
|
|
|
|
if (catalog)
|
|
{
|
|
auto catalog_filename = configuration_ptr->getTypeName() + "://" + configuration_ptr->getNamespace() + "/"
|
|
+ configuration_ptr->getRawPath().path + "metadata/v1.metadata.json";
|
|
const auto & [namespace_name, table_name] = DataLake::parseTableName(table_id_.getTableName());
|
|
catalog->createTable(namespace_name, table_name, catalog_filename, metadata_content_object);
|
|
}
|
|
}
|
|
|
|
Iceberg::IcebergDataSnapshotPtr IcebergMetadata::getRelevantDataSnapshotFromTableStateSnapshot(
|
|
Iceberg::TableStateSnapshot table_state_snapshot, ContextPtr local_context) const
|
|
{
|
|
IcebergDataSnapshotPtr data_snapshot;
|
|
auto metadata_object = getMetadataJSONObject(
|
|
table_state_snapshot.metadata_file_path,
|
|
object_storage,
|
|
persistent_components.metadata_cache,
|
|
local_context,
|
|
log,
|
|
persistent_components.metadata_compression_method,
|
|
persistent_components.table_uuid);
|
|
if (!table_state_snapshot.snapshot_id.has_value())
|
|
return nullptr;
|
|
Poco::JSON::Object::Ptr snapshot_object = traverseMetadataAndFindNecessarySnapshotObject(
|
|
metadata_object, *table_state_snapshot.snapshot_id, persistent_components.schema_processor);
|
|
|
|
return createIcebergDataSnapshotFromSnapshotJSON(snapshot_object, *table_state_snapshot.snapshot_id, local_context);
|
|
}
|
|
|
|
DataLakeMetadataPtr IcebergMetadata::create(
|
|
const ObjectStoragePtr & object_storage,
|
|
const StorageObjectStorageConfigurationWeakPtr & configuration,
|
|
const ContextPtr & local_context)
|
|
{
|
|
auto configuration_ptr = configuration.lock();
|
|
if (!configuration_ptr)
|
|
throw Exception(ErrorCodes::LOGICAL_ERROR, "Trying to create Iceberg table, but storage configuration is expired");
|
|
|
|
auto log = getLogger("IcebergMetadata");
|
|
|
|
IcebergMetadataFilesCachePtr cache_ptr = nullptr;
|
|
|
|
if (local_context->getSettingsRef()[Setting::use_iceberg_metadata_files_cache])
|
|
cache_ptr = local_context->getIcebergMetadataFilesCache();
|
|
else
|
|
LOG_TRACE(
|
|
log, "Not using in-memory cache for iceberg metadata files, because the setting use_iceberg_metadata_files_cache is false.");
|
|
auto persistent_components = initializePersistentTableComponents(object_storage, configuration_ptr, cache_ptr, local_context, log);
|
|
return std::make_unique<IcebergMetadata>(object_storage, configuration_ptr, std::move(persistent_components), local_context);
|
|
}
|
|
|
|
|
|
IcebergMetadata::IcebergHistory IcebergMetadata::getHistory(ContextPtr local_context) const
|
|
{
|
|
const auto [metadata_version, metadata_file_path, compression_method] = getLatestOrExplicitMetadataFileAndVersion(
|
|
object_storage,
|
|
persistent_components.table_path,
|
|
data_lake_settings,
|
|
persistent_components.metadata_cache,
|
|
local_context,
|
|
log.get(),
|
|
persistent_components.table_uuid,
|
|
persistent_components.metadata_compression_method);
|
|
|
|
auto metadata_object
|
|
= getMetadataJSONObject(metadata_file_path, object_storage, persistent_components.metadata_cache, local_context, log, compression_method, persistent_components.table_uuid);
|
|
chassert(persistent_components.format_version == metadata_object->getValue<int>(f_format_version));
|
|
|
|
/// History
|
|
std::vector<Iceberg::IcebergHistoryRecord> iceberg_history;
|
|
|
|
auto snapshots = metadata_object->get(f_snapshots).extract<Poco::JSON::Array::Ptr>();
|
|
auto snapshot_logs = metadata_object->get(f_snapshot_log).extract<Poco::JSON::Array::Ptr>();
|
|
|
|
std::vector<Int64> ancestors;
|
|
std::map<Int64, Int64> parents_list;
|
|
for (size_t i = 0; i < snapshots->size(); ++i)
|
|
{
|
|
const auto snapshot = snapshots->getObject(static_cast<UInt32>(i));
|
|
auto snapshot_id = snapshot->getValue<Int64>(f_metadata_snapshot_id);
|
|
|
|
if (snapshot->has(f_parent_snapshot_id) && !snapshot->isNull(f_parent_snapshot_id))
|
|
parents_list[snapshot_id] = snapshot->getValue<Int64>(f_parent_snapshot_id);
|
|
else
|
|
parents_list[snapshot_id] = 0;
|
|
}
|
|
|
|
/// For empty table we may have no snapshots
|
|
if (metadata_object->has(f_current_snapshot_id))
|
|
{
|
|
auto current_snapshot_id = metadata_object->getValue<Int64>(f_current_snapshot_id);
|
|
/// Add current snapshot-id to ancestors list
|
|
ancestors.push_back(current_snapshot_id);
|
|
while (parents_list[current_snapshot_id] != 0)
|
|
{
|
|
ancestors.push_back(parents_list[current_snapshot_id]);
|
|
current_snapshot_id = parents_list[current_snapshot_id];
|
|
}
|
|
}
|
|
|
|
for (size_t i = 0; i < snapshots->size(); ++i)
|
|
{
|
|
IcebergHistoryRecord history_record;
|
|
|
|
const auto snapshot = snapshots->getObject(static_cast<UInt32>(i));
|
|
history_record.snapshot_id = snapshot->getValue<Int64>(f_metadata_snapshot_id);
|
|
history_record.manifest_list_path = IcebergPathFromMetadata::deserialize(snapshot->getValue<String>(f_manifest_list));
|
|
|
|
if (const auto summary = snapshot->getObject(f_summary))
|
|
{
|
|
auto snapshot_summary = SnapshotSummary::fromJSON(*summary, /*with_extra_fields=*/true);
|
|
|
|
if (snapshot_summary)
|
|
history_record.snapshot_summary = std::move(snapshot_summary.value());
|
|
else
|
|
LOG_ERROR(
|
|
log,
|
|
"Error '{}' while parsing snapshot's summary with snapshot_id={}",
|
|
snapshot_summary.error(),
|
|
history_record.snapshot_id);
|
|
}
|
|
|
|
if (snapshot->has(f_parent_snapshot_id) && !snapshot->isNull(f_parent_snapshot_id))
|
|
history_record.parent_id = snapshot->getValue<Int64>(f_parent_snapshot_id);
|
|
else
|
|
history_record.parent_id = 0;
|
|
|
|
for (size_t j = 0; j < snapshot_logs->size(); ++j)
|
|
{
|
|
const auto snapshot_log = snapshot_logs->getObject(static_cast<UInt32>(j));
|
|
if (snapshot_log->getValue<Int64>(f_metadata_snapshot_id) == history_record.snapshot_id)
|
|
{
|
|
auto value = snapshot_log->getValue<std::string>(f_timestamp_ms);
|
|
ReadBufferFromString in(value);
|
|
DateTime64 time = 0;
|
|
readDateTime64Text(time, 6, in);
|
|
|
|
history_record.made_current_at = time;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (std::find(ancestors.begin(), ancestors.end(), history_record.snapshot_id) != ancestors.end())
|
|
history_record.is_current_ancestor = true;
|
|
else
|
|
history_record.is_current_ancestor = false;
|
|
|
|
iceberg_history.push_back(history_record);
|
|
}
|
|
|
|
return iceberg_history;
|
|
}
|
|
|
|
namespace
|
|
{
|
|
|
|
String formatPartitionKeyValue(const DB::Row & partition_key_value)
|
|
{
|
|
if (partition_key_value.empty())
|
|
return "{}";
|
|
|
|
String result = "{";
|
|
for (size_t i = 0; i < partition_key_value.size(); ++i)
|
|
{
|
|
if (i)
|
|
result += ", ";
|
|
result += applyVisitor(FieldVisitorToString(), partition_key_value[i]);
|
|
}
|
|
result += "}";
|
|
return result;
|
|
}
|
|
|
|
IcebergFileRecord buildIcebergFileRecord(
|
|
const Iceberg::ProcessedManifestFileEntryPtr & processed,
|
|
Int64 inherited_snapshot_id,
|
|
const Iceberg::IcebergPathResolver & path_resolver)
|
|
{
|
|
const auto & parsed = *processed->parsed_entry;
|
|
|
|
IcebergFileRecord record;
|
|
record.snapshot_id = parsed.parsed_snapshot_id.value_or(inherited_snapshot_id);
|
|
record.content = parsed.content_type;
|
|
record.file_path = path_resolver.resolve(parsed.file_path_key);
|
|
record.file_format = parsed.file_format;
|
|
record.record_count = parsed.record_count;
|
|
record.file_size_in_bytes = parsed.file_size_in_bytes;
|
|
record.partition = formatPartitionKeyValue(parsed.partition_key_value);
|
|
record.schema_id = processed->resolved_schema_id;
|
|
record.sequence_number = processed->sequence_number;
|
|
record.sort_order_id = parsed.sort_order_id;
|
|
|
|
for (const auto & [column_id, info] : parsed.columns_infos)
|
|
{
|
|
if (info.bytes_size.has_value())
|
|
record.column_sizes.emplace(column_id, *info.bytes_size);
|
|
if (info.rows_count.has_value())
|
|
record.value_counts.emplace(column_id, *info.rows_count);
|
|
if (info.nulls_count.has_value())
|
|
record.null_value_counts.emplace(column_id, *info.nulls_count);
|
|
}
|
|
|
|
record.equality_ids = parsed.equality_ids.value_or(std::vector<Int32>{});
|
|
|
|
return record;
|
|
}
|
|
|
|
}
|
|
|
|
IcebergMetadata::IcebergFiles IcebergMetadata::getFilesForManifest(
|
|
const Iceberg::IcebergDataSnapshotPtr & data_snapshot,
|
|
const Iceberg::TableStateSnapshot & table_state,
|
|
size_t manifest_index,
|
|
ContextPtr local_context) const
|
|
{
|
|
chassert(data_snapshot);
|
|
chassert(manifest_index < data_snapshot->manifest_list_entries.size());
|
|
|
|
const auto & manifest_list_entry = data_snapshot->manifest_list_entries[manifest_index];
|
|
auto handle = getManifestFileEntriesHandle(
|
|
object_storage, persistent_components, local_context, log, manifest_list_entry, table_state.schema_id);
|
|
|
|
IcebergFiles result;
|
|
for (auto content_type : {FileContentType::DATA, FileContentType::POSITION_DELETE, FileContentType::EQUALITY_DELETE})
|
|
{
|
|
for (const auto & processed : handle.getFilesWithoutDeleted(content_type))
|
|
result.push_back(buildIcebergFileRecord(processed, manifest_list_entry.added_snapshot_id, persistent_components.path_resolver));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool IcebergMetadata::isDataSortedBySortingKey(StorageMetadataPtr storage_metadata_snapshot, ContextPtr context) const
|
|
{
|
|
if (!storage_metadata_snapshot->hasSortingKey())
|
|
return false;
|
|
|
|
auto sorting_key = storage_metadata_snapshot->getSortingKey();
|
|
if (!sorting_key.sort_order_id.has_value())
|
|
return false;
|
|
|
|
auto table_state_snapshot = extractIcebergSnapshotIdFromMetadataObject(storage_metadata_snapshot);
|
|
if (table_state_snapshot == nullptr)
|
|
{
|
|
throw Exception(
|
|
ErrorCodes::LOGICAL_ERROR,
|
|
"Can't extract iceberg table state from storage snapshot for table location {}",
|
|
persistent_components.table_location);
|
|
}
|
|
|
|
/// Empty table is sorted table
|
|
auto data_snapshot = getRelevantDataSnapshotFromTableStateSnapshot(*table_state_snapshot, context);
|
|
if (!data_snapshot)
|
|
return true;
|
|
|
|
for (const auto & manifest_list_entry : data_snapshot->manifest_list_entries)
|
|
{
|
|
auto files_handle = getManifestFileEntriesHandle(
|
|
object_storage, persistent_components, context, log, manifest_list_entry, table_state_snapshot->schema_id);
|
|
|
|
if (!files_handle.areAllDataFilesSortedBySortOrderID(sorting_key.sort_order_id.value()))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::optional<size_t> IcebergMetadata::totalRows(ContextPtr local_context) const
|
|
{
|
|
auto [actual_data_snapshot, actual_table_state_snapshot] = getRelevantState(local_context);
|
|
|
|
if (!actual_data_snapshot)
|
|
{
|
|
ProfileEvents::increment(ProfileEvents::IcebergTrivialCountOptimizationApplied);
|
|
return 0;
|
|
}
|
|
|
|
|
|
/// All these "hints" with total rows or bytes are optional both in
|
|
/// metadata files and in manifest files, so we try all of them one by one
|
|
if (auto total_rows = actual_data_snapshot->getTotalRows(); total_rows.has_value())
|
|
{
|
|
ProfileEvents::increment(ProfileEvents::IcebergTrivialCountOptimizationApplied);
|
|
return total_rows;
|
|
}
|
|
|
|
Int64 result = 0;
|
|
for (const auto & manifest_list_entry : actual_data_snapshot->manifest_list_entries)
|
|
{
|
|
auto manifest_file_ptr = getManifestFileEntriesHandle(
|
|
object_storage, persistent_components, local_context, log, manifest_list_entry, actual_table_state_snapshot.schema_id);
|
|
auto data_count = manifest_file_ptr.getRowsCountInAllFilesExcludingDeleted(FileContentType::DATA);
|
|
auto position_deletes_count = manifest_file_ptr.getRowsCountInAllFilesExcludingDeleted(FileContentType::POSITION_DELETE);
|
|
if (!data_count.has_value() || !position_deletes_count.has_value())
|
|
return {};
|
|
|
|
result += data_count.value() - position_deletes_count.value();
|
|
}
|
|
|
|
ProfileEvents::increment(ProfileEvents::IcebergTrivialCountOptimizationApplied);
|
|
return result;
|
|
}
|
|
|
|
std::optional<size_t> IcebergMetadata::totalBytes(ContextPtr local_context) const
|
|
{
|
|
auto [actual_data_snapshot, actual_table_state_snapshot] = getRelevantState(local_context);
|
|
|
|
if (!actual_data_snapshot)
|
|
return 0;
|
|
|
|
/// All these "hints" with total rows or bytes are optional both in
|
|
/// metadata files and in manifest files, so we try all of them one by one
|
|
if (actual_data_snapshot->total_bytes.has_value())
|
|
return actual_data_snapshot->total_bytes;
|
|
|
|
Int64 result = 0;
|
|
for (const auto & manifest_list_entry : actual_data_snapshot->manifest_list_entries)
|
|
{
|
|
auto manifest_file_ptr = getManifestFileEntriesHandle(
|
|
object_storage, persistent_components, local_context, log, manifest_list_entry, actual_table_state_snapshot.schema_id);
|
|
auto count = manifest_file_ptr.getBytesCountInAllDataFilesExcludingDeleted();
|
|
if (!count.has_value())
|
|
return {};
|
|
|
|
result += count.value();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
ObjectIterator IcebergMetadata::iterate(
|
|
const ActionsDAG * filter_dag,
|
|
FileProgressCallback callback,
|
|
size_t /* list_batch_size */,
|
|
StorageMetadataPtr storage_metadata,
|
|
ContextPtr local_context) const
|
|
{
|
|
if (prepared_iterator)
|
|
{
|
|
return prepared_iterator;
|
|
}
|
|
auto iceberg_table_state = extractIcebergSnapshotIdFromMetadataObject(storage_metadata);
|
|
if (iceberg_table_state == nullptr)
|
|
{
|
|
throw Exception(
|
|
ErrorCodes::LOGICAL_ERROR,
|
|
"Can't extract iceberg table state from storage snapshot for table location {}",
|
|
persistent_components.table_location);
|
|
}
|
|
|
|
ProfileEventTimeIncrement<Microseconds> watch(ProfileEvents::IcebergIteratorInitializationMicroseconds);
|
|
|
|
return std::make_shared<IcebergIterator>(
|
|
object_storage,
|
|
local_context,
|
|
filter_dag,
|
|
callback,
|
|
iceberg_table_state,
|
|
getRelevantDataSnapshotFromTableStateSnapshot(*iceberg_table_state, local_context),
|
|
persistent_components);
|
|
}
|
|
|
|
NamesAndTypesList IcebergMetadata::getTableSchema(ContextPtr local_context) const
|
|
{
|
|
auto [actual_data_snapshot, actual_table_state_snapshot] = getRelevantState(local_context);
|
|
return *persistent_components.schema_processor->getClickhouseTableSchemaById(actual_table_state_snapshot.schema_id);
|
|
}
|
|
|
|
std::optional<DataLakeTableStateSnapshot> IcebergMetadata::getTableStateSnapshot(ContextPtr local_context) const
|
|
{
|
|
auto [actual_data_snapshot, actual_table_state_snapshot] = getRelevantState(local_context);
|
|
return DataLakeTableStateSnapshot{actual_table_state_snapshot};
|
|
}
|
|
|
|
std::unique_ptr<StorageInMemoryMetadata> IcebergMetadata::buildStorageMetadataFromState(
|
|
const DataLakeTableStateSnapshot & state, ContextPtr local_context) const
|
|
{
|
|
ProfileEventTimeIncrement<Microseconds> watch(ProfileEvents::IcebergMetadataUpdateMicroseconds);
|
|
chassert(std::holds_alternative<Iceberg::TableStateSnapshot>(state));
|
|
const auto & iceberg_state = std::get<Iceberg::TableStateSnapshot>(state);
|
|
auto result = std::make_unique<StorageInMemoryMetadata>();
|
|
result->setColumns(
|
|
ColumnsDescription{*persistent_components.schema_processor->getClickhouseTableSchemaById(iceberg_state.schema_id)});
|
|
result->setDataLakeTableState(state);
|
|
result->sorting_key = getSortingKey(local_context, iceberg_state);
|
|
return result;
|
|
}
|
|
|
|
bool IcebergMetadata::shouldReloadSchemaForConsistency(ContextPtr) const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
void IcebergMetadata::modifyFormatSettings(FormatSettings & format_settings, const Context & local_context) const
|
|
{
|
|
if (!local_context.getSettingsRef()[Setting::use_roaring_bitmap_iceberg_positional_deletes].value)
|
|
/// IcebergStreamingPositionDeleteTransform requires increasing row numbers from both the
|
|
/// data reader and the deletes reader.
|
|
format_settings.parquet.preserve_order = true;
|
|
}
|
|
|
|
void IcebergMetadata::addDeleteTransformers(
|
|
ObjectInfoPtr object_info,
|
|
QueryPipelineBuilder & builder,
|
|
const std::optional<FormatSettings> & format_settings,
|
|
FormatParserSharedResourcesPtr parser_shared_resources,
|
|
ContextPtr local_context) const
|
|
{
|
|
auto iceberg_object_info = std::dynamic_pointer_cast<IcebergDataObjectInfo>(object_info);
|
|
if (!iceberg_object_info)
|
|
return;
|
|
|
|
if (!iceberg_object_info->info.position_deletes_objects.empty())
|
|
{
|
|
builder.addSimpleTransform(
|
|
[&](const SharedHeader & header)
|
|
{ return iceberg_object_info->getPositionDeleteTransformer(object_storage, header, format_settings, parser_shared_resources, local_context); });
|
|
}
|
|
const auto & delete_files = iceberg_object_info->info.equality_deletes_objects;
|
|
LOG_DEBUG(log, "Constructing filter transform for equality delete, there are {} delete files", delete_files.size());
|
|
for (const EqualityDeleteObject & delete_file : delete_files)
|
|
{
|
|
auto simple_transform_adder = [&](const SharedHeader & header)
|
|
{
|
|
/// get header of delete file
|
|
Block delete_file_header;
|
|
RelativePathWithMetadata delete_file_object(delete_file.file_path);
|
|
{
|
|
auto schema_read_buffer = createReadBuffer(delete_file_object, object_storage, local_context, log);
|
|
auto schema_reader = FormatFactory::instance().getSchemaReader(delete_file.file_format, *schema_read_buffer, local_context);
|
|
auto columns_with_names = schema_reader->readSchema();
|
|
ColumnsWithTypeAndName initial_header_data;
|
|
for (const auto & elem : columns_with_names)
|
|
{
|
|
initial_header_data.push_back(ColumnWithTypeAndName(elem.type, elem.name));
|
|
}
|
|
delete_file_header = Block(initial_header_data);
|
|
}
|
|
/// with equality ids, we can know which columns should be deleted, here we calculate the indexes.
|
|
const std::vector<Int32> & equality_ids = *(delete_file.equality_ids);
|
|
Block block_for_set;
|
|
std::vector<size_t> equality_indexes_delete_file;
|
|
for (Int32 col_id : equality_ids)
|
|
{
|
|
NameAndTypePair name_and_type
|
|
= persistent_components.schema_processor->getFieldCharacteristics(delete_file.schema_id, col_id);
|
|
size_t position_in_delete_file = delete_file_header.getPositionByName(name_and_type.name);
|
|
/// Take the type from the delete file header rather than from the table schema:
|
|
/// the nullability of a column in the delete file may differ from its nullability
|
|
/// in the table schema, and the columns read below have exactly the header's types.
|
|
block_for_set.insert(ColumnWithTypeAndName(delete_file_header.getByPosition(position_in_delete_file).type, name_and_type.name));
|
|
equality_indexes_delete_file.push_back(position_in_delete_file);
|
|
}
|
|
/// Then we read the content of the delete file.
|
|
auto mutable_columns_for_set = block_for_set.cloneEmptyColumns();
|
|
std::unique_ptr<ReadBuffer> data_read_buffer = createReadBuffer(delete_file_object, object_storage, local_context, log);
|
|
CompressionMethod compression_method = chooseCompressionMethod(delete_file.file_path, "auto");
|
|
auto delete_format = FormatFactory::instance().getInput(
|
|
delete_file.file_format,
|
|
*data_read_buffer,
|
|
delete_file_header,
|
|
local_context,
|
|
local_context->getSettingsRef()[DB::Setting::max_block_size],
|
|
format_settings,
|
|
parser_shared_resources,
|
|
nullptr,
|
|
true,
|
|
compression_method);
|
|
/// only get the delete columns and construct a set by 'block_for_set'
|
|
while (true)
|
|
{
|
|
Chunk delete_chunk = delete_format->read();
|
|
if (!delete_chunk)
|
|
break;
|
|
size_t rows = delete_chunk.getNumRows();
|
|
Columns delete_columns = delete_chunk.detachColumns();
|
|
for (size_t i = 0; i < equality_indexes_delete_file.size(); i++)
|
|
{
|
|
mutable_columns_for_set[i]->insertRangeFrom(*delete_columns[equality_indexes_delete_file[i]], 0, rows);
|
|
}
|
|
}
|
|
block_for_set.setColumns(std::move(mutable_columns_for_set));
|
|
/// we are constructing a 'not in' expression
|
|
const auto & settings = local_context->getSettingsRef();
|
|
SizeLimits size_limits_for_set
|
|
= {settings[Setting::max_rows_in_set], settings[Setting::max_bytes_in_set], settings[Setting::set_overflow_mode]};
|
|
FutureSetPtr future_set = std::make_shared<FutureSetFromTuple>(
|
|
CityHash_v1_0_2::uint128(), nullptr, block_for_set.getColumnsWithTypeAndName(), true, size_limits_for_set);
|
|
ColumnConst::Ptr set_col = ColumnConst::create(ColumnSet::create(1, future_set), 0);
|
|
ActionsDAG dag(header->getColumnsWithTypeAndName());
|
|
/// Construct right argument of 'not in' expression, it is the column set.
|
|
const ActionsDAG::Node * in_rhs_arg = &dag.addColumn(std::move(set_col), std::make_shared<DataTypeSet>(), "set column");
|
|
/// Construct left argument of 'not in' expression
|
|
ActionsDAG::NodeRawConstPtrs left_columns;
|
|
std::unordered_map<std::string_view, const ActionsDAG::Node *> outputs;
|
|
for (const auto & output : dag.getOutputs())
|
|
outputs.emplace(output->result_name, output);
|
|
/// select columns to use in 'notIn' function
|
|
for (Int32 col_id : equality_ids)
|
|
{
|
|
NameAndTypePair name_and_type = persistent_components.schema_processor->getFieldCharacteristics(
|
|
iceberg_object_info->info.underlying_format_read_schema_id, col_id);
|
|
auto it = outputs.find(name_and_type.name);
|
|
if (it == outputs.end())
|
|
throw Exception(ErrorCodes::LOGICAL_ERROR, "Cannot find column {} in dag outputs", name_and_type.name);
|
|
left_columns.push_back(it->second);
|
|
}
|
|
FunctionOverloadResolverPtr func_tuple_builder
|
|
= std::make_unique<FunctionToOverloadResolverAdaptor>(std::make_shared<FunctionTuple>());
|
|
const ActionsDAG::Node * in_lhs_arg
|
|
= left_columns.size() == 1 ? left_columns.front() : &dag.addFunction(func_tuple_builder, std::move(left_columns), {});
|
|
/// we got the NOT IN function
|
|
auto func_not_in = FunctionFactory::instance().get("notIn", nullptr);
|
|
const auto & not_in_node = dag.addFunction(func_not_in, {in_lhs_arg, in_rhs_arg}, "notInResult");
|
|
dag.getOutputs().push_back(¬_in_node);
|
|
LOG_DEBUG(log, "Use expression {} in equality deletes", dag.dumpDAG());
|
|
return std::make_shared<FilterTransform>(header, std::make_shared<ExpressionActions>(std::move(dag)), "notInResult", true);
|
|
};
|
|
builder.addSimpleTransform(simple_transform_adder);
|
|
}
|
|
}
|
|
|
|
SinkToStoragePtr IcebergMetadata::write(
|
|
SharedHeader sample_block,
|
|
const StorageID & table_id,
|
|
ObjectStoragePtr /*object_storage*/,
|
|
StorageObjectStorageConfigurationPtr configuration,
|
|
const std::optional<FormatSettings> & format_settings,
|
|
ContextPtr context,
|
|
std::shared_ptr<DataLake::ICatalog> catalog)
|
|
{
|
|
if (context->getSettingsRef()[Setting::allow_insert_into_iceberg])
|
|
{
|
|
return std::make_shared<IcebergStorageSink>(object_storage, configuration, format_settings, sample_block, context, catalog, persistent_components, table_id);
|
|
}
|
|
else
|
|
{
|
|
throw Exception(
|
|
ErrorCodes::SUPPORT_IS_DISABLED,
|
|
"Insert into iceberg is in beta."
|
|
"To allow its usage, enable setting allow_insert_into_iceberg");
|
|
}
|
|
}
|
|
|
|
void IcebergMetadata::drop(ContextPtr context)
|
|
{
|
|
if (context->getSettingsRef()[Setting::iceberg_delete_data_on_drop].value)
|
|
{
|
|
auto files = listFiles(*object_storage, persistent_components.table_path, persistent_components.table_path, "");
|
|
for (const auto & file : files)
|
|
object_storage->removeObjectIfExists(StoredObject(file));
|
|
}
|
|
}
|
|
|
|
ColumnMapperPtr IcebergMetadata::getColumnMapperForObject(ObjectInfoPtr object_info) const
|
|
{
|
|
IcebergDataObjectInfo * iceberg_object_info = dynamic_cast<IcebergDataObjectInfo *>(object_info.get());
|
|
if (!iceberg_object_info)
|
|
return nullptr;
|
|
chassert(object_info->getFileFormat().has_value());
|
|
if (Poco::toLower(*object_info->getFileFormat()) != "parquet")
|
|
return nullptr;
|
|
|
|
return persistent_components.schema_processor->getColumnMapperById(iceberg_object_info->info.underlying_format_read_schema_id);
|
|
}
|
|
|
|
// (TODO): usage of this function is wrong and should be removed completely (it was done as a temporary workaround for a bug which occurred during supporting concurrent SELECTs).
|
|
ColumnMapperPtr IcebergMetadata::getColumnMapperForCurrentSchema(StorageMetadataPtr storage_metadata_snapshot, ContextPtr context) const
|
|
{
|
|
if (Poco::toLower(write_format) != "parquet")
|
|
return nullptr;
|
|
auto iceberg_table_state = extractIcebergSnapshotIdFromMetadataObject(storage_metadata_snapshot);
|
|
// This is a temporary cludge for cluster functions because now the state on replicas is not synchronized with the primary state on the coordinator
|
|
if (!iceberg_table_state)
|
|
{
|
|
auto [actual_data_snapshot, actual_table_state_snapshot] = getRelevantState(context);
|
|
iceberg_table_state = std::make_shared<TableStateSnapshot>(actual_table_state_snapshot);
|
|
}
|
|
return persistent_components.schema_processor->getColumnMapperById(iceberg_table_state->schema_id);
|
|
}
|
|
|
|
KeyDescription IcebergMetadata::getSortingKey(ContextPtr local_context, TableStateSnapshot actual_table_state_snapshot) const
|
|
{
|
|
auto metadata_object = getMetadataJSONObject(
|
|
actual_table_state_snapshot.metadata_file_path,
|
|
object_storage,
|
|
persistent_components.metadata_cache,
|
|
local_context,
|
|
log,
|
|
persistent_components.metadata_compression_method,
|
|
persistent_components.table_uuid);
|
|
|
|
auto [schema, current_schema_id] = parseTableSchemaV2Method(metadata_object);
|
|
auto result = getSortingKeyDescriptionFromMetadata(metadata_object, *persistent_components.schema_processor->getClickhouseTableSchemaById(current_schema_id), local_context);
|
|
auto sort_order_id = metadata_object->getValue<Int64>(f_default_sort_order_id);
|
|
result.sort_order_id = sort_order_id;
|
|
return result;
|
|
}
|
|
|
|
DataLakeMetadataPtr IcebergMetadata::createWithDeserialization(
|
|
const ObjectStoragePtr & object_storage,
|
|
const StorageObjectStorageConfigurationWeakPtr & configuration,
|
|
const ContextPtr & local_context,
|
|
ReadBuffer & in)
|
|
{
|
|
auto log = getLogger("IcebergMetadata");
|
|
Int32 format_version = 0;
|
|
String table_location;
|
|
readVarInt(format_version, in);
|
|
readStringBinary(table_location, in);
|
|
Int32 metadata_compression_method_val = 0;
|
|
readVarInt(metadata_compression_method_val, in);
|
|
auto metadata_compression_method = static_cast<CompressionMethod>(metadata_compression_method_val);
|
|
IcebergMetadataFilesCachePtr cache_ptr = nullptr;
|
|
if (local_context->getSettingsRef()[Setting::use_iceberg_metadata_files_cache])
|
|
cache_ptr = local_context->getIcebergMetadataFilesCache();
|
|
else
|
|
LOG_TRACE(
|
|
log, "Not using in-memory cache for iceberg metadata files, because the setting use_iceberg_metadata_files_cache is false.");
|
|
auto configuration_ptr = configuration.lock();
|
|
if (!configuration_ptr)
|
|
throw Exception(ErrorCodes::LOGICAL_ERROR, "Trying to create Iceberg table, but storage configuration is expired");
|
|
auto standard_persistent_components
|
|
= initializePersistentTableComponents(object_storage, configuration_ptr, cache_ptr, local_context, log);
|
|
auto schema_processor = standard_persistent_components.schema_processor;
|
|
auto deserialized_persistent_components = Iceberg::PersistentTableComponents{
|
|
.schema_processor = schema_processor,
|
|
.metadata_cache = standard_persistent_components.metadata_cache,
|
|
.format_version = format_version,
|
|
.table_location = table_location,
|
|
.metadata_compression_method = metadata_compression_method,
|
|
.table_path = standard_persistent_components.table_path,
|
|
.table_uuid = standard_persistent_components.table_uuid,
|
|
.path_resolver = IcebergPathResolver(
|
|
table_location,
|
|
standard_persistent_components.table_path,
|
|
configuration_ptr->getTypeName(),
|
|
configuration_ptr->getNamespace())};
|
|
auto metadata = std::make_unique<IcebergMetadata>(object_storage, configuration.lock(), std::move(deserialized_persistent_components), local_context);
|
|
return metadata;
|
|
}
|
|
|
|
}
|
|
|
|
#endif
|