diff --git a/src/Storages/ObjectStorage/StorageObjectStorage.cpp b/src/Storages/ObjectStorage/StorageObjectStorage.cpp index 7d784e8c73d7..18caf93c588f 100644 --- a/src/Storages/ObjectStorage/StorageObjectStorage.cpp +++ b/src/Storages/ObjectStorage/StorageObjectStorage.cpp @@ -882,6 +882,14 @@ SchemaCache & StorageObjectStorage::getSchemaCache(const ContextPtr & context, c void StorageObjectStorage::mutate([[maybe_unused]] const MutationCommands & commands, [[maybe_unused]] ContextPtr context_) { + /// For datalake tables (e.g. Iceberg), refresh external metadata so that the + /// storage snapshot contains the `datalake_table_state`. Without this the mutation + /// pipeline will hit a `LOGICAL_ERROR` exception in `iterate` when building the read side. + /// Normally `updateExternalDynamicMetadataIfExists` is called by the + /// analyzer/interpreter for `SELECT` and `INSERT` queries, but `InterpreterAlterQuery` + /// does not call it before invoking `mutate`. + updateExternalDynamicMetadataIfExists(context_); + auto metadata_snapshot = getInMemoryMetadataPtr(); auto storage = getStorageID(); configuration->mutate(commands, context_, storage, metadata_snapshot, catalog, format_settings); diff --git a/tests/queries/0_stateless/04092_iceberg_mutate_without_prior_select.reference b/tests/queries/0_stateless/04092_iceberg_mutate_without_prior_select.reference new file mode 100644 index 000000000000..61780798228d --- /dev/null +++ b/tests/queries/0_stateless/04092_iceberg_mutate_without_prior_select.reference @@ -0,0 +1 @@ +b diff --git a/tests/queries/0_stateless/04092_iceberg_mutate_without_prior_select.sh b/tests/queries/0_stateless/04092_iceberg_mutate_without_prior_select.sh new file mode 100755 index 000000000000..cae6d7a32f0d --- /dev/null +++ b/tests/queries/0_stateless/04092_iceberg_mutate_without_prior_select.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Tags: no-fasttest + +# Regression test: ALTER TABLE UPDATE on an Iceberg table used to throw a +# LOGICAL_ERROR exception ("Can't extract iceberg table state from storage +# snapshot") when no SELECT or INSERT preceded the mutation in the same server +# lifetime. +# The root cause was that `StorageObjectStorage::mutate` did not call +# `updateExternalDynamicMetadataIfExists` before reading data, so the storage +# snapshot lacked the `datalake_table_state` required by `IcebergMetadata::iterate`. + +# Use `IcebergS3` rather than `IcebergLocal`, because the local object storage +# becomes read-only after `ATTACH` (it honours the `is_readonly` flag passed to +# `createObjectStorage`), which would block the mutation. S3 ignores that flag. + +CURDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +# shellcheck source=../shell_config.sh +. "$CURDIR"/../shell_config.sh + +TABLE="t_${CLICKHOUSE_DATABASE}_${RANDOM}" +TABLE_PATH="04092_iceberg_mutate/${CLICKHOUSE_TEST_UNIQUE_NAME}" + +${CLICKHOUSE_CLIENT} --query "DROP TABLE IF EXISTS ${TABLE}" +${CLICKHOUSE_CLIENT} --query " + CREATE TABLE ${TABLE} (c0 String) + ENGINE = IcebergS3(s3_conn, filename = '${TABLE_PATH}') +" + +# Populate the Iceberg data files in S3. +${CLICKHOUSE_CLIENT} --allow_insert_into_iceberg=1 --query "INSERT INTO ${TABLE} VALUES ('a')" + +# DETACH + ATTACH rebuilds the storage object from the stored CREATE statement, +# so its in-memory metadata has no `datalake_table_state` loaded — the same +# state as after a server restart. The Iceberg files in S3 are preserved. +${CLICKHOUSE_CLIENT} --query "DETACH TABLE ${TABLE}" +${CLICKHOUSE_CLIENT} --query "ATTACH TABLE ${TABLE}" + +# This mutation is the first data-reading operation on the freshly attached +# storage — no prior SELECT or INSERT has called +# `updateExternalDynamicMetadataIfExists`. +# `validate_mutation_query = 0` is required to suppress the validate stage +# of `MutationsInterpreter`, which would otherwise construct an +# `InterpreterSelectQuery` whose constructor calls +# `updateExternalDynamicMetadataIfExists` as a side effect and masks the bug. +# Without the fix, this throws a `LOGICAL_ERROR` exception. +${CLICKHOUSE_CLIENT} --allow_insert_into_iceberg=1 --validate_mutation_query=0 --query "ALTER TABLE ${TABLE} UPDATE c0 = 'b' WHERE TRUE" + +${CLICKHOUSE_CLIENT} --query "SELECT c0 FROM ${TABLE}" + +${CLICKHOUSE_CLIENT} --query "DROP TABLE IF EXISTS ${TABLE}"