Parquet schema cache ignores `input_format_parquet_detect_variant_by_structure`
I maintainer di solito rispondono entro 1 giorno
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- Mezza giornata
- Idoneità per principianti
- 66/100
Direzione di ricerca
Inizia dal getter Parquet per la chiave della cache dello schema in src/Processors/Formats/Impl/ParquetV3BlockInputFormat.cpp (circa righe 644-661), poi confrontalo con l'impostazione letta in src/Processors/Formats/Impl/Parquet/SchemaConverter.cpp vicino alla riga 777. Il test esistente 04757_schema_inference_cache_key_settings_parquet.sh è il posto dove aggiungere un caso. È completato quando eseguire i due ordini delle impostazioni sullo stesso file restituisce Dynamic per detect=1 e Tuple per detect=0, indipendentemente da quale query venga eseguita per prima.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Describe what's wrong
- Schema cache for Parquet files does not key on
input_format_parquet_detect_variant_by_structure. - Whichever query infers the schema first fixes the type as
DynamicorTuplefor all later queries on that file. - Expected each query to get the type matching its own setting; instead the second query reuses the first query's cached type.
- Can return raw blob values typed
Tuple(value String, metadata String)instead of decoded values typedDynamic.
Root cause: getKeysForSchemaCache keys the schema cache on FormatFactory::getAdditionalInfoForSchemaCache, but the Parquet getter in ParquetV3BlockInputFormat.cpp omits detect_variant_by_structure from the key format string, even though SchemaConverter::processSubtreeDynamic uses that setting to choose between Dynamic and Tuple.
Analysis details (evidence, affected locations, impact)
Why we believe this is a bug: getKeysForSchemaCache (ReadSchemaUtils.cpp:599) keys the cache on FormatFactory::getAdditionalInfoForSchemaCache; the Parquet getter (ParquetV3BlockInputFormat.cpp:644-661) lists enable_json_parsing, local_time_as_utc, allow_geoparquet_parser, skip_columns_with_unsupported_types etc., but not the new setting that SchemaConverter::processSubtreeDynamic reads at SchemaConverter.cpp:777 to choose Dynamic over Tuple.
Affected locations:
src/Processors/Formats/Impl/ParquetV3BlockInputFormat.cpp:656— Parquet schema-cache key format string and arguments (648-660) lack detect_variant_by_structuresrc/Processors/Formats/Impl/Parquet/SchemaConverter.cpp:777— setting decides Dynamic vs Tuple during schema inference
Impact: On one server, queries with different compatibility (< 26.10) or explicit values of the setting read the same Parquet file with each other's inferred schema: Spark variant columns come back as Tuple of raw blobs under default settings, or as Dynamic when the user disabled detection. Exercised with file(); the cache is shared by all users of the server.
Does it reproduce on most recent release?
Confirmed on master; not checked on a release.
How to reproduce
Reproduce with two file() queries against the same Parquet fixture using opposite values of the setting, with the file's mtime older than the first query's run.
# no-fasttest: needs Parquet.
# Each carrier below is probed in BOTH orders, because an order-insensitive probe cannot
# detect a missing cache-key field: whichever query runs first decides the cached type.
# Every order gets its OWN file so the two orders never share a cache entry.
#
# The fixtures are aged with `touch -d`: SchemaCache::tryGetImpl drops an entry when the
# source's mtime is >= the entry's registration time, and both are whole seconds, so a file
# written in the same second as the first query is re-inferred and nothing is cached.
T=test_repro
AGE="2000-01-01 00:00:00"
# --- enable_nullable_tuple_type -----------------------------------------------
# Decides whether an OPTIONAL group with an all-REQUIRED subtree is inferred as Nullable(Tuple(...))
# or as a plain Tuple(...), so each pair must report the type its own query asks for, whichever ran
# first. A stale entry also changes the value read back: the plain Tuple has nowhere to put a
# struct-level NULL and returns a tuple of NULLs instead.
clickhouse-local -q "
SET enable_nullable_tuple_type = 1;
SELECT * FROM values('p Nullable(Tuple(a UInt8, b UInt8))', tuple(1, 2), NULL)
INTO OUTFILE '${T}_nt_a.parquet' TRUNCATE FORMAT Parquet"
cp "${T}_nt_a.parquet" "${T}_nt_b.parquet"
touch -d "$AGE" "${T}"_nt_*.parquet
echo "-- Parquet nullable_tuple, nt=1 first"
clickhouse-local -m -q "
DESC file('${T}_nt_a.parquet', 'Parquet') SETTINGS enable_nullable_tuple_type = 1;
DESC file('${T}_nt_a.parquet', 'Parquet') SETTINGS enable_nullable_tuple_type = 0;" | cut -f2
echo "-- Parquet nullable_tuple, nt=0 first"
clickhouse-local -m -q "
DESC file('${T}_nt_b.parquet', 'Parquet') SETTINGS enable_nullable_tuple_type = 0;
DESC file('${T}_nt_b.parquet', 'Parquet') SETTINGS enable_nullable_tuple_type = 1;" | cut -f2
echo "-- Parquet nullable_tuple value, nt=1 first"
clickhouse-local -m -q "
SELECT isNull(p) FROM file('${T}_nt_a.parquet', 'Parquet') ORDER BY 1 SETTINGS enable_nullable_tuple_type = 1;
SELECT isNull(p) FROM file('${T}_nt_a.parquet', 'Parquet') ORDER BY 1 SETTINGS enable_nullable_tuple_type = 0;"
# --- input_format_parquet_allow_geoparquet_parser -----------------------------------------
# Decides whether a GeoParquet geometry column is inferred as a geo type or as its raw String
# representation, so each pair must report LineString for the =1 query and Nullable(String)
# for the =0 query, whichever ran first.
for suffix in a b; do cp "$CUR_DIR"/data_parquet/03445_geoparquet_null_linestring.parquet "${T}_geo_${suffix}.parquet"; done
touch -d "$AGE" "${T}"_geo_*.parquet
echo "-- Parquet allow_geoparquet_parser, geo=1 first"
clickhouse-local -m -q "
DESC file('${T}_geo_a.parquet', 'Parquet') SETTINGS input_format_parquet_allow_geoparquet_parser = 1;
DESC file('${T}_geo_a.parquet', 'Parquet') SETTINGS input_format_parquet_allow_geoparquet_parser = 0;" | awk -F'\t' '$1 == "geometry" {print $2}'
echo "-- Parquet allow_geoparquet_parser, geo=0 first"
clickhouse-local -m -q "
DESC file('${T}_geo_b.parquet', 'Parquet') SETTINGS input_format_parquet_allow_geoparquet_parser = 0;
DESC file('${T}_geo_b.parquet', 'Parquet') SETTINGS input_format_parquet_allow_geoparquet_parser = 1;" | awk -F'\t' '$1 == "geometry" {print $2}'
# --- input_format_parquet_skip_columns_with_unsupported_types_in_schema_inference ---------
# Decides whether a column of an unsupported type is dropped or the file is rejected, so the
# permissive query must not let a later strict query skip the exception. Only this direction is a
# carrier: the strict query throws, and a throwing inference caches nothing.
# The data file has one VARIANT-typed column `u`, which is a valid Parquet logical type that is not
# implemented here, and one supported Int32 column `id`.
cp "$CUR_DIR"/data_parquet/parquet_variant_logical_type.parquet "${T}_unsup_a.parquet"
touch -d "$AGE" "${T}"_unsup_*.parquet
echo "-- Parquet skip_columns_with_unsupported_types, skip=1 first then strict must throw"
clickhouse-local -m -q "
DESC file('${T}_unsup_a.parquet', 'Parquet') SETTINGS input_format_parquet_skip_columns_with_unsupported_types_in_schema_inference = 1 FORMAT Null;
DESC file('${T}_unsup_a.parquet', 'Parquet') FORMAT Null;" \
2>&1 | grep -c INCORRECT_DATA
echo "-- Parquet skip_columns_with_unsupported_types, strict alone throws (control)"
clickhouse-local -q "DESC file('${T}_unsup_a.parquet', 'Parquet') FORMAT Null" \
2>&1 | grep -c INCORRECT_DATA
# Without this the arm above would also pass if nothing had been cached at all: the strict query
# throws either way. This shows the permissive query really did leave an entry, keyed on its value.
echo "-- Parquet skip_columns_with_unsupported_types, the permissive entry exists and is keyed"
clickhouse-local -m -q "
DESC file('${T}_unsup_a.parquet', 'Parquet') SETTINGS input_format_parquet_skip_columns_with_unsupported_types_in_schema_inference = 1 FORMAT Null;
SELECT count(), extract(additional_format_info, 'skip_columns_with_unsupported_types=\w+')
FROM system.schema_inference_cache WHERE format = 'Parquet' GROUP BY 2 ORDER BY 2;"
# An entry being present still does not prove a later query read it: with cache reads bypassed every
# query re-infers and rewrites the same entry. Repeating one query at unchanged settings must hit.
echo "-- Parquet skip_columns_with_unsupported_types, a repeated query hits the cache"
clickhouse-local -m -q "
DESC file('${T}_unsup_a.parquet', 'Parquet') SETTINGS input_format_parquet_skip_columns_with_unsupported_types_in_schema_inference = 1 FORMAT Null;
DESC file('${T}_unsup_a.parquet', 'Parquet') SETTINGS input_format_parquet_skip_columns_with_unsupported_types_in_schema_inference = 1 FORMAT Null;
SELECT value > 0 FROM system.events WHERE event = 'SchemaInferenceCacheSchemaHits';"
# --- input_format_parquet_detect_variant_by_structure ----------------------------------
# Decides whether an unannotated group with only `metadata` and `value` BYTE_ARRAY fields (how
# Spark 4.0 writes a variant column) is inferred as Dynamic or as Tuple, so each pair must report
# the type its own query asks for, whichever ran first.
for suffix in a b; do cp "$CUR_DIR"/data_parquet/04928_variant_spark.parquet "${T}_var_${suffix}.parquet"; done
touch -d "$AGE" "${T}"_var_*.parquet
echo "-- Parquet detect_variant_by_structure, detect=1 first"
clickhouse-local -m -q "
DESC file('${T}_var_a.parquet', 'Parquet') SETTINGS input_format_parquet_detect_variant_by_structure = 1, print_pretty_type_names = 0;
DESC file('${T}_var_a.parquet', 'Parquet') SETTINGS input_format_parquet_detect_variant_by_structure = 0, print_pretty_type_names = 0;" | awk -F'\t' '$1 == "v" {print $2}'
echo "-- Parquet detect_variant_by_structure, detect=0 first"
clickhouse-local -m -q "
DESC file('${T}_var_b.parquet', 'Parquet') SETTINGS input_format_parquet_detect_variant_by_structure = 0, print_pretty_type_names = 0;
DESC file('${T}_var_b.parquet', 'Parquet') SETTINGS input_format_parquet_detect_variant_by_structure = 1, print_pretty_type_names = 0;" | awk -F'\t' '$1 == "v" {print $2}'
rm -f "${T}"_*
Note: an automated re-run of this exact block on current master (38bc12ded730) did not show the failure; the analyst's run did (outputs below). Environment or ordering may matter.
Expected behavior
Each query reading the file should get Dynamic or Tuple according to its own value of input_format_parquet_detect_variant_by_structure.
Expected output of the reproducer above:
-- Parquet detect_variant_by_structure, detect=1 first
Dynamic
Tuple(value String, metadata String)
-- Parquet detect_variant_by_structure, detect=0 first
Tuple(value String, metadata String)
Dynamic
Error message and/or stacktrace
The first query's inferred type is cached and returned to a later query with the opposite setting, on the same server across users.
Actual output of the reproducer above:
-- Parquet detect_variant_by_structure, detect=1 first
Dynamic
Dynamic
-- Parquet detect_variant_by_structure, detect=0 first
Tuple(value String, metadata String)
Tuple(value String, metadata String)
Suggested fix
Add detect_variant_by_structure={} / settings.parquet.detect_variant_by_structure to the Parquet getter, and the two-order case to 04757_schema_inference_cache_key_settings_parquet.sh (test_sql_content).
Additional context
Same pattern as #123194 (found by: lexical, vector; vector: cosine distance 0.23 (agrees with the lexical leg)).
Found during automated review of PR #117106; whether that PR introduced it could not be established, so nobody is tagged. Severity P2 · Finding h_pr117106_001
- Lingua principale
- C++
- Stelle
- 50.5k
- Fork
- 9.1k
- Merge medio
- 1g 3h
- PR unite (30g)
- 540
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di ClickHouse/ClickHouse
-
ai_p2 comp-formats
Difficoltà 2/5 1-3 ore Idoneità per principianti 84/100
ClickHouse/ClickHouse#126833 ·
I maintainer di solito rispondono entro 1 giorno
-
ai_p3 comp-ci-infrastructure
Difficoltà 2/5 1-3 ore Idoneità per principianti 86/100
ClickHouse/ClickHouse#126502 ·
I maintainer di solito rispondono entro 1 giorno
-
ai_p3 comp-data-types
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
ClickHouse/ClickHouse#126316 ·
I maintainer di solito rispondono entro 1 giorno
-
comp-documentation external
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 82/100
ClickHouse/ClickHouse#126208 ·
I maintainer di solito rispondono entro 1 giorno
-
ai_p2 comp-query-analyzer
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
ClickHouse/ClickHouse#126203 ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di ClickHouse/ClickHouse
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
qbittorrent/qBittorrent#25121 ·
I maintainer di solito rispondono entro 2 giorni
-
SVT-AV1: Unnecessary size paddingForse già presa @pszemus l’ha presa oggi. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
strukturag/libheif#1945 ·
I maintainer di solito rispondono entro 1 giorno
-
v9.6.0: CMake incorrectly enforces presence of libevdevForse già presa Una pull request collegata a questa issue è aperta o già unita. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 67/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 65/100
mltframework/shotcut#1943 ·
I maintainer di solito rispondono entro 1 giorno
-
Rowan (2017) electron heating gives NaN for beta > beta_max; clip() then sets electrons to kel_minAperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
parthenon-hpc-lab/kharma#213 · 1 commento ·
I maintainer di solito rispondono entro 3 giorni