Approach (step-by-step)1. Scope & baseline: record source parquet layout (partition columns, file paths, file counts, per-file row counts, min/max/null stats from footers).2. Create Iceberg table with identical schema and partition spec. Ingest data (bulk load or rewrite) without altering partition keys.3. Validate at three levels: file-level, partition-level, and logical table-level (rows & column checksums). Also validate Iceberg metadata (manifests, snapshots, partition metrics).Verification queries & commandsA. Source (Parquet) inventory (Spark)scala
// list files, sizes, row counts via Spark
spark.read.format("parquet").load("s3://bucket/source/path")
.selectExpr("input_file_name() as file", "size", "count(*) over () as total_rows")
.groupBy("file").count().show(false)
Or use parquet-tools to read footers: parquet-tools meta <file>B. Row countssql
-- Source: read raw parquet
SELECT COUNT(*) AS src_rows FROM parquet.`s3://bucket/source/path`;
-- Target: Iceberg table
SELECT COUNT(*) AS tgt_rows FROM iceberg_db.table;
Counts must match.C. Partition-level row counts and file countssql
-- Source
SELECT dt, COUNT(*) AS rows, COUNT(DISTINCT(input_file_name())) AS files
FROM parquet.`s3://bucket/source/path`
GROUP BY dt;
-- Target
SELECT dt, SUM(record_count) AS rows, COUNT(*) AS manifests_or_files
FROM iceberg_db.table.snapshots -- or use table.files() API
GROUP BY dt;
Compare per-partition rows and file counts.D. Column-level checksums (detect content drift). Compute deterministic row hash (exclude metadata, sort stable key if necessary):sql
-- example using Spark SQL
SELECT md5(concat_ws('|', col1, col2, col3)) AS row_hash
FROM parquet.`s3://bucket/source/path`
-- aggregate
SELECT count(*) AS cnt, md5(concat_ws(',', collect_list(row_hash))) AS partition_hash
FROM (...)
GROUP BY dt;
And same for iceberg table:sql
SELECT dt, COUNT(*) AS cnt,
md5(concat_ws(',', collect_list(md5(concat_ws('|', col1,col2,col3))))) AS partition_hash
FROM iceberg_db.table
GROUP BY dt;
Partition_hash and cnt should match per partition. If dataset is large, use summarized aggregates: SUM(hash_as_bigint) or XOR of 64-bit hashes.E. Column statistics: compare min/max/null countssql
-- Source min/max/nulls
SELECT dt,
MIN(colA) AS minA, MAX(colA) AS maxA,
SUM(CASE WHEN colA IS NULL THEN 1 ELSE 0 END) AS nullsA
FROM parquet.`...` GROUP BY dt;
-- Target same query against iceberg table
Match across partitions.F. File-level integrity: compare parquet file checksums (ETag) and Iceberg file paths/sizes in manifests- List source file ETags and sizes; compare to Iceberg’s data file entries (manifest list shows file path, file_size, record_count). Use Iceberg Java/Python API:python
from iceberg.api import Table
t = catalog.load_table("db.table")
for mf in t.currentSnapshot().allManifests():
for entry in mf:
print(entry.file_path, entry.file_size, entry.record_count)
G. Metadata validation- Confirm snapshot ids, manifest lists, and partition spec: DESCRIBE HISTORY/SELECT * FROM table.snapshots;- Verify partition spec equals expected: SHOW PARTITIONS or use Iceberg table.spec() API.Checksums & counts to compare (summary)- Total row count (global)- Per-partition row counts- Per-partition file counts (and file sizes)- Per-partition checksum/hash (e.g., XOR64 or MD5 aggregate of row hashes)- Column min/max and null counts per partition- Per-file record_count and file path present in Iceberg manifestsHandling large scale & determinism- Use deterministic hashing (stable column order, null representation)- For large partitions, use streaming aggregates (XOR or sum of 64-bit hashes) instead of collect_list to avoid OOM.- Sampling: if full-scan expensive, run prioritized validation on recent partitions then sample historical ones.What to do on mismatch- Drill down to partition -> file -> row level: compare filesize/row_count and run diff queries (LEFT JOIN by primary key) to find missing/extra rows and bad transformations.- Re-run ingestion for affected partitions or perform targeted repair using Iceberg rewrite or re-ingest.This approach ensures parity across physical files, partitioning semantics, and logical row/column integrity.