fix: verify-stac reports existing parquet columns as absent (nested columns, wide assets) - #543
Open
eastagiletracker wants to merge 2 commits into
Conversation
… parquet_schema
check_declared_schema_matches_data built its per-file presence map from
parquet_schema filtered on `type IS NOT NULL`. That filter drops the schema root,
but the parquet schema is a tree and a LIST / STRUCT / MAP column is a group node
whose physical type is also NULL — so the filter dropped the column itself and kept
its machinery leaves ('element', 'key', 'value', struct fields) in its place.
Both halves of the check misfired on every asset with a nested column: the declared
column HARD-failed `declared-column-absent` ("fix the STAC table:columns" — advice
that would delete correct documentation), and its leaves were reported as
undocumented top-level columns. On the live catalog, iucn-taxonomy-2025 reports 9
HARD findings, one per populated `string[]` column, plus a phantom 'element'
advisory; the check exits 1 there, so any PR touching such a collection is blocked.
Derive the top-level name from each leaf chunk's `path_in_schema` instead, whose
first segment is the column the leaf belongs to ('common_names_en, list, element'
-> common_names_en). Still a footer read, no ordering assumption, and it covers
struct and map columns as well as lists.
Tests execute the generated SQL against a real parquet file carrying LIST, STRUCT
and MAP columns: the nested columns are no longer absent, their leaves are no longer
undocumented columns, and a genuinely absent declared column still HARD-fails.
…, not in Python check_declared_schema_matches_data read the asset's whole column list back from the MCP and diffed it in Python. The MCP query tool caps a result at 50 rows, so every column past the 50th silently vanished from the comparison and was reported HARD as "absent from all N parquet file(s)". GROUP BY is unordered, so which columns fell off varied between runs on the same asset. This is the failure check_values_match_distinct already documents and avoids by pushing its set-difference into DuckDB; do the same here. Both halves now return only the discrepancies — the declared columns whose file count is short, and the undocumented columns present in every file — so the result is bounded by the number of problems rather than the width of the table, and is usually empty. Live effect on the published catalog: gbif-hex-2026-06 (61 top-level columns) and the social-vulnerability tables (svi-2022 alone reported 399 absent columns) go from hundreds of fabricated HARD findings to their real ones. The bbox covering-struct special case goes away with it: now that a column's name comes from its leaf's path, a GeoParquet 1.1 covering struct lands on 'bbox' like any other column, so it needs no separate list of leaf names. Tests run the generated SQL against a real parquet file under the same 50-row cap, including a 61-column asset, and keep the mutation guard that a genuinely absent column still HARD-fails.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR proposes two fixes to
verify-stac.py'scheck_declared_schema_matches_data, which currently reports parquet columns that exist — and hold data — asdeclared-column-absent. We include this PR work along with a full history of your repo at https://eastagiletracker.com/projects/322. You can sign in with your GitHub ID to claim ownership of the project.What is wrong
1. A nested column is read as absent. The presence map came from
parquet_schema(...) WHERE type IS NOT NULL. That filter drops the schema root — but the parquet schema is a tree, and aLIST/STRUCT/MAPcolumn is a group node whose physicaltypeis also NULL. So the filter dropped the column itself and kept its machinery leaves (element,key,value, struct fields) in its place. Every declared nested column then failed HARD, and its leaves were reported as undocumented top-level columns.2. Any asset wider than 50 columns loses its tail. The check read the whole column list back over the MCP and diffed it in Python. The MCP query tool caps a result at 50 rows, so column 51 onwards silently vanished from the comparison and was reported absent.
GROUP BYis unordered, so which columns fell off changed between runs on the same asset. This is exactly the failurecheck_values_match_distinctalready documents and sidesteps by pushing its set-difference into DuckDB.Both are live on the published catalog, and both are HARD, so
verify-stac.pyexits 1 and the advice it prints ("fix the STACtable:columns") would delete correct documentation.Reproduction at current HEAD (86f09eb)
All nine exist and are populated —
SELECT common_names_en FROM read_parquet('s3://public-iucn/taxonomy/iucn-taxonomy.parquet') WHERE common_names_en IS NOT NULL LIMIT 1returns['Caucasian Woodsia']. What the old query looked at is the LIST group node:SELECT name, type, converted_type FROM parquet_schema(...) WHERE name = 'common_names_en'givestype = NULL, converted_type = LIST, sotype IS NOT NULLdiscards it.The width cap is just as easy to see.
gbif-hex-2026-06has 61 top-level columns, and the grouped presence query returns 50 rows:The change
A column's name now comes from its leaf chunk's
path_in_schema, whose first segment is the column that leaf belongs to (common_names_en, list, element→common_names_en;bbox, xmin→bbox). That is exact, needs no assumption about schema row order, and covers structs and maps as well as lists — it also subsumes thebboxcovering-struct special case, which is why that one goes away.parquet_metadatais the column-chunk footer, so this stays a footer read, and the CTE isMATERIALIZEDso each query reads it once.Both comparisons then run in SQL and return only the discrepancies — declared columns whose file count is short, and undocumented columns present in every file — so the result is bounded by the number of problems rather than the width of the table, and is normally empty.
Deliberately unchanged: the per-file grain that
#534exists for (COUNT(DISTINCT file_name)per column, so the#520heterogeneous hole still reads asdeclared-column-heterogeneous), the severities, the finding codes, and the message text. A file with no row groups now drops out of both the presence map and the file count, which is the honest reading — an empty file has no data to disagree with the STAC.Verification
python3 -m unittest discover -s tests— 36 tests, green, both with and withoutduckdbinstalled. Against upstream HEAD the four new tests plus the six updated ones fail (10 failures); with the change all 36 pass. The new tests execute the generated SQL against a real parquet file carryingLIST,STRUCT,MAPand 61-column shapes, through a stub that truncates at 50 rows the way the MCP does — so they fail if either defect returns. One is a mutation guard: a genuinely absent declared column must still fail HARD, and does.End to end on the live collection above:
9 hard, 3 advisory/FAILbecomes0 hard, 2 advisory/PASS, with the two genuine advisories (geoparquet-no-geom-column,cng-fid-missing-nonspatial) retained.Because the change touches a gate rather than a dataset, I also ran the check across 117 published collections before and after.
declared-column-absentgoes from 1,179 findings across 26 collections to 6 — and all 6 also fired before, so no new HARD finding appears anywhere:wdoecm-parquet OBJECTID,glwd-class-area-hex h0,regions-hex cartography/population/wikidata, andimma-hex bbox. Those look like genuine stale declarations worth a look, but they are yours to judge and are untouched here. Theundocumented-columnadvisories go from 443 to 384: 38 nested machinery names (element,key,value, and the Overture struct fields) stop being reported, while real undeclared columns the 50-row cap had been hiding —identifiedby,recordedby,mrgid_sov5, andh4/h6/h7/h9on the GBIF hex — now surface. An undeclared bbox covering stays exempt, as before. Six assets that previously answeredschema-match-check-failedare now checked, sinceparquet_metadatadoes not bind column names the way the old query did.How this was managed
Your issues and pull requests were imported to a live board, and this fix was tracked there as its own story: the story for this work, on the board.
If you'd rather not receive contributions like this, reply
no-more-prson this pull request and we won't open any further ones on your repositories.Lawrence W. Sinclair
CEO / East Agile
linkedin.com/in/lwsinclair/
eastagile.com