Zoltán Borók-Nagy created IMPALA-15444:
------------------------------------------

             Summary: Iceberg: Parquet and ORC data files without field ids are 
resolved by position; schema.name-mapping.default is ignored (wrong results)
                 Key: IMPALA-15444
                 URL: https://issues.apache.org/jira/browse/IMPALA-15444
             Project: IMPALA
          Issue Type: Bug
          Components: Backend
            Reporter: Zoltán Borók-Nagy


h3. Problem
Iceberg tables built by importing existing Parquet or ORC files ({{add_files}}, 
{{snapshot}}, {{migrate}}) contain data files without field ids. Iceberg's 
procedures set {{schema.name-mapping.default}} for them, and the spec ("Column 
Projection") requires readers to use that mapping for files without field ids. 
Impala ignores the mapping: it numbers the file's columns by their physical 
position (the IMPALA-11034 logic for migrated tables) and matches the table's 
field ids against those numbers. (Tables created by {{snapshot}} keep their 
files outside the table location; with the default 
{{-iceberg_allow_datafiles_in_table_location_only=true}}, Impala refuses to 
load them before reaching this bug, see IMPALA-11618.)
* If an imported file stores its columns in a different order than the table, 
has extra columns, or lacks some of the table's columns, and the types fit, 
Impala *silently returns the values of the wrong column*. Nested struct fields 
are affected the same way. Row-group min/max and dictionary filtering are 
evaluated on the wrong column too.
* If the types do not fit, the query fails with {{File '...' has an 
incompatible Parquet schema for column '...'}}.
* {{SET PARQUET_FALLBACK_SCHEMA_RESOLUTION=NAME}} has no effect: the resolver 
always uses FIELD_ID for Iceberg tables.

Spark 4.1.3 + Iceberg 1.11.0 reads the same tables correctly. Trino 483 and 
ClickHouse 26.8 return the correct results on the benchmark's name-mapping 
table.

h3. Steps to reproduce
1. Write a Parquet file without field ids whose column order differs from the 
table:
{code:python}
import pyarrow as pa, pyarrow.parquet as pq
pq.write_table(pa.table({"b": pa.array([20, 21], pa.int32()), "a": 
pa.array([10, 11], pa.int32())}), "swap_b_a.parquet")
{code}
2. Import it with Spark + Iceberg ({{add_files}} sets 
{{schema.name-mapping.default}} when it is missing; it registers files in 
place, so copy the file under the table location first):
{code:sql}
CREATE TABLE cat.db.swap (a INT, b INT) USING iceberg TBLPROPERTIES 
('format-version'='2');
-- copy swap_b_a.parquet to <table location>/data/imported/
CALL cat.system.add_files(table => 'db.swap', source_table => 
'`parquet`.`<table location>/data/imported`');
INSERT INTO cat.db.swap VALUES (1, 2);   -- a natively written file with field 
ids
{code}
The table now has {{schema.name-mapping.default = 
[{"field-id":1,"names":["a"]},{"field-id":2,"names":["b"]}]}}. (Also reproduced 
through the Java API: set {{schema.name-mapping.default}} to 
{{MappingUtil.create(schema)}}, compute metrics with 
{{ParquetUtil.fileMetrics(file, metricsConfig, nameMapping)}} and append with 
{{AppendFiles}}; {{AppendFiles}} does not set the mapping itself. Same result.)
3. Query in Impala (the table was registered as an external 
{{'iceberg.catalog'='hadoop.tables'}} table):
{code:sql}
SELECT a, b FROM swap ORDER BY a;
SELECT count(*) FROM swap WHERE a = 10;
SELECT sum(a), sum(b) FROM swap;
{code}

h3. Expected result (Spark returns this)
Rows (1, 2), (10, 20), (11, 21); {{count(*) WHERE a = 10}} = 1; {{sum(a), 
sum(b)}} = 22, 43.

h3. Actual result (Impala 4.5.2)
Rows (1, 2), *(20, 10), (21, 11)*; {{count(*) WHERE a = 10}} = *0* (and {{WHERE 
a = 20}} = 1); {{sum(a), sum(b)}} = *42, 23*. The natively written row is 
correct; in the imported rows {{a}} and {{b}} are swapped. With a file {{(s 
STRING, id BIGINT)}} imported into a table {{(id BIGINT, s STRING)}} the query 
fails instead:
{noformat}
File '.../err/data/imported/err_s_id.parquet' has an incompatible Parquet 
schema for column 'db.err.id'. Column type: BIGINT, Parquet schema:
optional byte_array s [i:0 d:1 r:0]
{noformat}
Master 3d4d47dc30 (5.0.0-SNAPSHOT) gives identical rows, counts, sums and 
error. For {{WHERE a = 10}} min/max statistics eliminate the imported file's 
row group (RowsRead 0). A file with only column {{b}} imported into {{(a, b)}} 
returns {{a}} = 20, 21 and {{b}} = NULL (Spark: {{a}} = NULL, {{b}} = 20, 21). 
If the mapping is removed from the table ({{ALTER TABLE ... UNSET TBLPROPERTIES 
('schema.name-mapping.default')}}), Spark returns the same swapped rows as 
Impala.

h3. Analysis (same logic on 4.5.2 and master 3d4d47dc30; repro confirmed on 
master)
* {{ParquetSchemaResolver::Init()}} 
({{be/src/exec/parquet/parquet-metadata-utils.h:156-171}}) forces the FIELD_ID 
strategy for Iceberg tables and, if the first column has no field id, calls 
{{GenerateFieldIDs()}} ({{parquet-metadata-utils.cc:983}} on 4.5.2, {{:1027}} 
on master), which assigns 1, 2, 3, ... in physical order.
* Column readers, dictionary filtering and row-group/page statistics all 
resolve through the same {{ResolvePath()}}, so values and filters use the wrong 
column.
* Nothing in {{be/}} or {{fe/}} reads {{schema.name-mapping.default}}.
* The ORC scanner does the same ({{be/src/exec/orc/orc-metadata-utils.cc:61}}: 
{{OrcSchemaResolver::Init()}} calls {{GenerateFieldIDs()}}, defined at {{:330}} 
on 4.5.2, {{:334}} on master). Verified on master: an ORC file (b, a) imported 
with {{add_files}} returns (20, 10), (21, 11), and {{SET 
ORC_SCHEMA_RESOLUTION=NAME}} has no effect.
* Iceberg Java applies position-based fallback ids only when the table has *no* 
name mapping ({{ReadConf}}: file ids -> name mapping -> {{addFallbackIds}}).
* IMPALA-11034's description proposed a NameMapping; the commit assigns ids by 
position instead. That matches Iceberg's fallback only for tables without a 
name mapping. It is wrong whenever a mapping exists and a file's columns differ 
from the table's order: imported files, and also in-place migrated Hive 
directories, since Hive reads Parquet by name. Impala's own {{ALTER TABLE ... 
CONVERT TO ICEBERG}} sets no name mapping ({{MigrateTableUtil}} passes null), 
so tables it converts stay position-resolved in every engine, and this fix 
would not change them.
* History (code reading): at 4.1.0, Parquet files without field ids were 
resolved by NAME (IMPALA-11053, c10e951bcb). That handled reordered, extra and 
missing columns but not renames. IMPALA-11034 (4.2.0, backported to 4.1.1) 
replaced it with position-based ids. Earlier, IMPALA-10974 fell back to 
{{PARQUET_FALLBACK_SCHEMA_RESOLUTION}} (default POSITION). ORC has used 
POSITION for such files since IMPALA-10974 (4.1.0).

Suggested fix: pass the table's name mapping (including nested fields and all 
aliases) to the Parquet and ORC scanners. For files without field ids, assign 
ids by name through it. Columns without a mapped file column follow the spec's 
missing-field rules: the identity-partition value (existing 
IMPALA-11053/IMPALA-13364 logic), then initial-default (IMPALA-14589), else 
NULL. Keep {{GenerateFieldIDs()}} only for tables without a mapping. Follow-up: 
have {{CONVERT TO ICEBERG}} set {{schema.name-mapping.default}}, as Iceberg's 
{{migrate}} and {{snapshot}} do.

h3. Workaround
Rewrite the imported files with an engine that applies the name mapping, e.g. 
Spark {{CALL cat.system.rewrite_data_files(table => 'db.swap', options => 
map('rewrite-all','true'))}} (verified, also on master: Impala returns the 
correct rows afterwards).




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to