Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

Parquet schema cache ignores `input_format_parquet_detect_variant_by_structure`

Aperta Adatta ai principianti
#124,986 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

@groeneai ci sta già lavorando.

Dal 12/10/2026.

Valutazione

Difficoltà
2/5
Tempo stimato
Mezza giornata
Idoneità per principianti
66/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
cpp
Ambito
databases

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

ai_p2 comp-parquet-reader-v3
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 Dynamic or Tuple for 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 typed Dynamic.

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:

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

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di ClickHouse/ClickHouse

Tutte le issue di ClickHouse/ClickHouse

Issue simili

Altre issue su C++

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.