Parquet schema cache ignores `input_format_parquet_detect_variant_by_structure`
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- Half a day
- Newbie friendliness
- 66/100
Research direction
Start with the Parquet getter for the schema-cache key in src/Processors/Formats/Impl/ParquetV3BlockInputFormat.cpp (around lines 644-661), then compare it with the setting read in src/Processors/Formats/Impl/Parquet/SchemaConverter.cpp near line 777. The existing test 04757_schema_inference_cache_key_settings_parquet.sh is the place to add a case. Done means that running the two setting orders on the same file returns Dynamic for detect=1 and Tuple for detect=0 regardless of which query runs first.
Written by the indexing model from the issue text.
Description
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
- Dominant language
- C++
- Stars
- 50.3k
- Forks
- 9.1k
- Avg merge
- 22h 15m
- Merged PRs (30d)
- 448
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from ClickHouse/ClickHouse
-
comp-mergetree performance
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
ClickHouse/ClickHouse#124794 ·
Maintainers usually reply within 1 day
-
`MySQL` database engine accepts `connection_auto_close` but its tables ignore itPossibly taken @groeneai claimed this today. Openbug comp-mysql external
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
ClickHouse/ClickHouse#124749 · 3 comments ·
Maintainers usually reply within 1 day
-
`S3Queue` with a duplicated setting in its definition logs that it uses the first value but uses the last onePossibly taken @buggins claimed this today. Opencomp-s3queue external minor
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
ClickHouse/ClickHouse#124746 · 2 comments ·
Maintainers usually reply within 1 day
-
ai_p3 comp-mergetree
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
ClickHouse/ClickHouse#124371 ·
Maintainers usually reply within 1 day
-
Parquet v3 reader: a negative `num_values` in a column-chunk footer trips `chassert(diff.by_stage[i] >= 0)` in `ReadManager`Possibly taken @groeneai claimed this 3 days ago. Opencomp-parquet-reader-v3 testing
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
ClickHouse/ClickHouse#124224 · 3 comments ·
Maintainers usually reply within 1 day
All issues in ClickHouse/ClickHouse
Similar issues
-
[request] vsg/1.1.16Openupstream update
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
conan-io/conan-center-index#31142 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Difficulty 1/5 Under an hour Newbie friendliness 84/100
NVIDIA/DeepStream#78 ·