Skip to content

[GH-3115] Add netcdf.metadata data source for NetCDF file metadata#2829

Merged
jiayuasu merged 10 commits into
apache:masterfrom
jiayuasu:netcdf-sedonainfo-support
Jul 20, 2026
Merged

[GH-3115] Add netcdf.metadata data source for NetCDF file metadata#2829
jiayuasu merged 10 commits into
apache:masterfrom
jiayuasu:netcdf-sedonainfo-support

Conversation

@jiayuasu

@jiayuasu jiayuasu commented Apr 7, 2026

Copy link
Copy Markdown
Member

Did you read the Contributor Guide?

Is this PR related to a ticket?

What changes were proposed in this PR?

Add a read-only Spark DataSourceV2 (netcdf.metadata) that extracts NetCDF file metadata without reading data arrays, the NetCDF counterpart to geotiff.metadata (#2846).

Usage

df = sedona.read.format("netcdf.metadata").load("/path/to/data.nc")

# Discover variables across a collection
df.selectExpr("path", "explode(variables) as v").where("NOT v.isCoordinate")

Output schema

One row per file, with a NetCDF-native schema rather than a copy of the GeoTIFF one:

Column Type Notes
path String File path
driver String "NetCDF"
fileSize Long File size in bytes
format String NetCDF (classic) / NetCDF-4
width, height Int Grid X/Y size; null if no gridded variable
srid, crs Int / String EPSG code and WKT, from the CF grid_mapping variable
geoTransform Struct upperLeftX/Y, scaleX/Y, skewX/Y; null for irregular grids
cornerCoordinates Struct minX/Y, maxX/Y
dimensions Array[Struct] name, length, isUnlimited
variables Array[Struct] name, dataType, dimensions, shape, units, longName, standardName, noDataValue, isCoordinate, attributes
globalAttributes Map[String,String] Root-group attributes

Key implementation details

  • Ranged remote reads: opens the file through a Hadoop-backed ucar.unidata.io.RandomAccessFile with a random read-policy hint (fs.option.openfile.read.policy=random), so object stores (S3A) transfer only the header and 1-D coordinate arrays via range requests — never the full file. Classic NetCDF headers are contiguous (a few requests); NetCDF-4/HDF5 metadata is scattered (more, smaller requests).
  • Column pruning: path/driver/fileSize come from the file listing without opening the file; coordinate arrays are read only when geoTransform/cornerCoordinates is requested.
  • Regular-grid detection: geoTransform is emitted only when coordinates lie within half a pixel of the best-fit line (robust to float32 quantization); irregular grids report a centers-only extent, matching RS_FromNetCDF conventions.
  • CRS/SRID: resolved from the CF grid_mapping variable (crs_wkt/spatial_ref), then global-attribute fallback; SRID via a GeoTools EPSG lookup.
  • Per-file partition values for bin-packed partitions; scan equality includes the pushed limit; unsigned attributes widened to their true value; non-numeric/non-finite coordinates degrade to a null extent.
  • Read-only: newWriteBuilder throws UnsupportedOperationException.

Files

  • New package: spark/common/.../io/netcdfmetadata/NetCdfMetadataDataSource, NetCdfMetadataTable, NetCdfMetadataScanBuilder, NetCdfMetadataPartitionReaderFactory, NetCdfMetadataPartitionReader, HadoopRandomAccessFile
  • Service registration: added to META-INF/services/org.apache.spark.sql.sources.DataSourceRegister
  • Documentation: docs/tutorial/files/netcdfmetadata-sedona-spark.md + mkdocs.yml nav entry

How was this patch tested?

17 tests in netcdfMetadataTest.scala with exact-match assertions:

  • Classic test.nc: metadata, geoTransform, cornerCoordinates, dimensions, per-variable metadata (CF attributes, _FillValue/missing_value, isCoordinate), empty globalAttributes
  • Cross-validation of extent against RS_FromNetCDF
  • NetCDF-4/HDF5 fixture: format="NetCDF-4", extent, and unsigned (ubyte) attribute widening
  • CRS/SRID resolution from a grid_mapping variable (EPSG:4326)
  • Irregular grid: null geoTransform with centers-only cornerCoordinates
  • File with no gridded variable: null grid fields, populated dimensions/variables
  • Glob/directory (recursive) loading, LIMIT pushdown across multiple files, column pruning, and a cheap-projection oracle proving the file is not opened when only path/driver/fileSize are selected

Verified locally on Spark 3.4/Scala 2.12 and cross-compiled on Spark 4.0/Scala 2.13.

Did this PR include necessary documentation updates?

  • Yes, I have updated the documentation.

Add a read-only Spark DataSourceV2 (`netcdf.metadata`) that extracts NetCDF
metadata -- dimensions, variables, attributes, and spatial extent -- without
reading data arrays, mirroring the geotiff.metadata source.

Reading:
- Ranged remote reads through a Hadoop-backed ucar RandomAccessFile with a
  random read-policy hint, so only the header and 1-D coordinate arrays are
  transferred, never the whole file.
- Column pruning: path/driver/fileSize come from the file listing without
  opening the file; coordinate arrays are read only when the extent is requested.

NetCDF-native schema:
- dimensions[], variables[] (dimensions, shape, CF units/long_name/
  standard_name, _FillValue), and a globalAttributes map.
- geoTransform/cornerCoordinates are nullable; geoTransform is emitted only for
  evenly spaced grids (best-fit-line test, robust to float32 coordinate
  quantization), and irregular grids report a centers-only extent.
- crs/srid resolved from the CF grid_mapping variable (crs_wkt/spatial_ref),
  falling back to the equivalent global attributes.

Correctness:
- Per-file partition values for bin-packed partitions.
- Scan equality accounts for the pushed limit.
- Unsigned attributes are widened to their true value.
- Non-numeric or non-finite coordinates degrade to a null extent.

Closes apache#3115. The NetCDF counterpart of the geotiff.metadata source (apache#2846).

Tested against classic NetCDF-3 and NetCDF-4/HDF5 fixtures; docs and nav added.
@jiayuasu
jiayuasu force-pushed the netcdf-sedonainfo-support branch from 2e39b87 to d2ce3e5 Compare July 17, 2026 08:48
@jiayuasu jiayuasu changed the title [GH-2824] Add NetCDF metadata extraction to sedonainfo [GH-3115] Add netcdf.metadata data source for NetCDF file metadata Jul 17, 2026
…scoping, CRS via proj4sedona

Grid and extent correctness:
- Skip CF bounds/climatology variables when selecting the grid-defining variable
  and prefer variables whose trailing dimensions have resolvable 1-D numeric
  coordinate variables, so lat_bnds(lat, nv) can no longer hijack the grid.
- Resolve coordinate variables and grid_mapping references in the data
  variable's group and its ancestors (NUG scoping) instead of a global
  short-name search, fixing wrong bindings in nested-group files.
- Decode packed coordinates: apply CF scale_factor/add_offset and widen
  _Unsigned integer storage, so transforms are in real-world units.
- Tighten the regularity test: strict monotonicity plus a per-step tolerance
  (0.1% relative, floored at a few ulps of the stored magnitude) plus the
  half-pixel best-fit bound, so locally uneven grids like [0, 1.4, 2] no
  longer receive an affine transform while fine float32 grids still do.

CRS (minimal CF support):
- SRID lookup via proj4sedona instead of GeoTools, removing this data
  source's only dependency on the optional LGPL GeoTools runtime.
- Parse the extended grid_mapping form ("crs: lat lon").
- Infer EPSG:4326 for a latitude_longitude grid mapping with no explicit
  ellipsoid/datum parameters when the file carries no CRS WKT.
- Parameter-only projected grid mappings remain untranslated (documented).

Data source behavior:
- fallbackFileFormat now returns a stub FileFormat that raises a clear
  UnsupportedOperationException for catalog paths (CREATE TABLE ... USING),
  which previously threw a NullPointerException.
- Directory handling: defaults never override explicit user options
  (recursiveFileLookup=false keeps Hive-style partition discovery), and the
  NetCDF extension filter now also applies to multi-directory loads.
- readToByteChannel drains channels that accept partial writes.

Adds 9 tests (26 total) and 5 fixtures covering each fix; documentation
updated, including CRS resolution rules and downstream RS_SetSRID/RS_SetCRS
usage.
jiayuasu added 6 commits July 18, 2026 00:46
…g, option handling

CRS:
- EPSG:4326 is no longer inferred from the latitude_longitude mapping name
  alone; it now requires positive WGS 84 identification (datum/CRS name or
  WGS 84 ellipsoid parameters, with a Greenwich prime meridian).
- grid_mapping references resolve absolute (/crs) and relative (sub/crs,
  ../crs) group paths in addition to proximity-searched plain names.

Grid and coordinates:
- Grid selection also excludes variables referenced by ancillary_variables,
  coordinates, cell_measures, and formula_terms (token-split, keyed-form
  aware), so declared ancillary variables cannot shadow the data variable.
- Coordinate lookup adds a lateral fallback matched strictly by dimension
  identity, finding NUG coordinate variables that live outside the data
  variable's lineage (e.g. a sibling group) while still rejecting identically
  named dimensions from unrelated groups.
- _FillValue/missing_value honor the owning variable's _Unsigned semantics
  (byte -1 with _Unsigned reports 255, matching the enhanced value).

RS_FromNetCDF interop:
- NetCdfReader resolves full-path variable names (e.g. "sub/temp", as
  reported by netcdf.metadata for nested groups) and binds coordinate
  variables from the record variable's group before falling back to the
  whole-file search, making the documented discovery-to-load workflow work
  for nested files.

Data source options:
- Explicit option checks are case-insensitive, so mixed-case spellings such
  as RecursiveFileLookup=false reliably suppress the defaults.
- A user-supplied pathGlobFilter disables the glob-path rewrite so Spark
  applies both the path glob and the filter natively.
- The unsupported-fallback FileFormat now fails on instantiation, rejecting
  catalog DDL that supplies an explicit schema (which skips inferSchema).
- readToByteChannel returns the transferred count on zero-progress channel
  writes instead of busy-spinning.

Adds 7 tests (33 total) and 3 fixtures; extends 3 fixtures; documentation
updated for the revised CRS and reference-resolution semantics.
…selection, loader parity

CRS:
- EPSG:4326 now requires positive WGS 84 identification by datum/CRS *name*;
  ellipsoid parameters can only veto, never qualify, so datums such as CR05
  that use the WGS 84 ellipsoid are no longer misreported as WGS 84.
- The expanded grid_mapping form with multiple mappings ("geographic: lat lon
  projected: x y") selects the mapping whose coordinate list matches the
  grid's (y, x) coordinates, so the reported CRS always corresponds to the
  coordinates used for the extent; an unmatched multi-mapping is ambiguous
  and reports no CRS.

Grid selection and schema:
- CF metadata references are resolved from their owning variable and
  excluded by variable identity, so a path reference like /aux/temperature
  no longer shadows an unrelated root variable sharing its basename.
- Variable dimension names are qualified with their declaring group's path
  (sub/temp reports [sub/lat, sub/lon]), joinable against the dimensions
  inventory.
- Native unsigned 64-bit coordinates decode as unsigned; values at or above
  2^63 no longer surface as negative.

RS_FromNetCDF parity (NetCdfReader):
- Coordinate variables are matched by dimension identity in both the scoped
  and lateral searches, so a same-named variable over an unrelated dimension
  (declared earlier in the file) can no longer silently shrink the raster.
- Non-spatial (band) dimension variables resolve in the record variable's
  scope with the same identity validation, fixing nested files whose band
  dimension shadows a root dimension of a different length.
- Coordinate reads decode _Unsigned, scale_factor, and add_offset, so packed
  files georeference identically in the loader and the metadata reader; the
  element reader also accepts unsigned NetCDF-4 types.

Adds 8 tests (40 total) and 6 fixtures; documentation updated for the
stricter datum inference, mapping selection, and qualified dimension names.
… identity, ensemble datum

RS_FromNetCDF (NetCdfReader):
- Coordinate candidates are validated against the specific requested
  dimension, not just any dimension of the record variable, so a same-named
  variable over a different axis (e.g. a local lat(time) next to the true
  lat(lat)) can no longer silently reshape the raster.
- Path-resolved coordinate names are validated the same way; explicitly
  naming coordinates over unrelated dimensions now fails loudly instead of
  returning a mis-sized raster. The long form of getRaster keeps accepting
  any record dimension, since the user names the coordinate variables and
  the axis is derived from the variable itself.

netcdf.metadata:
- Expanded grid_mapping clauses resolve their coordinate references relative
  to the data variable and match the grid's actual coordinate variables by
  identity, so a clause referencing same-named coordinates in another group
  (/geo/x vs the root x) is never selected by basename overlap. When the
  grid coordinates cannot be resolved, basename matching against the grid
  dimension names remains as a best-effort fallback.
- The EPSG:6326 ensemble name ("World Geodetic System 1984 ensemble") is
  recognized as a WGS 84 identifier.

Adds 4 tests (44 total) and 3 fixtures.
… CRS name vetoes, lateral clause matching

RS_FromNetCDF (NetCdfReader):
- The long form of getRaster validates each explicitly named coordinate
  against its intended spatial axis: the lat coordinate must be attached to
  the record variable's Y (second-to-last) dimension and the lon coordinate
  to its X (last) dimension, so non-spatial, duplicate, or swapped
  coordinates are rejected instead of silently reshaping the raster.
- Axis slice indices are derived from the coordinate variables' actual
  dimension identities rather than name matching, and the band/plane split
  compares axis indices instead of dimension names, so a band dimension
  sharing a coordinate's name can no longer be miscounted.

netcdf.metadata:
- Explicit reference_ellipsoid_name and prime_meridian_name values that
  contradict WGS 84 (e.g. GRS 1980, Paris) veto the EPSG:4326 inference,
  consistent with the documented any-disagreeing-identifier rule.
- Expanded grid_mapping clause references that are plain names not visible
  in the data variable's scope fall back to the same CF-aware lateral
  resolution used for the grid coordinates themselves, so a mapping tied to
  sibling-group coordinates (e.g. /coords/x referenced as "x") is selected
  correctly.

Adds 5 tests (49 total) and 3 fixtures.
… axes, repeated dimensions

netcdf.metadata:
- An expanded grid_mapping clause — including a single one — is selected
  only when its resolved coordinate set covers both of the grid's (y, x)
  coordinate variables. CF's extended form explicitly scopes a mapping to
  its listed coordinates, so a "geographic: lat lon" clause tied to 2-D
  auxiliary coordinates no longer labels the 1-D x/y metre extent with a
  geographic CRS; the extent is still reported, unlabeled.

RS_FromNetCDF (NetCdfReader):
- The four-argument overload now implements its documented contract for
  non-trailing axes: each named coordinate prefers a candidate serving the
  default trailing axis and otherwise accepts one attached to any record
  dimension, the axes are derived from the coordinates' actual dimension
  identities, and the raster plane is extracted with stride-aware permuted
  indexing. Every remaining dimension becomes a band dimension in row-major
  order; for trailing axes the arithmetic reduces exactly to the previous
  contiguous-plane form.
- Repeated dimensions in a variable's shape resolve to the default trailing
  occurrence when applicable and are rejected as ambiguous otherwise, and
  the band/plane split works by axis index throughout, fixing band metadata
  for shapes like temp(n, y, n).

Adds 3 tests and reworks 1 (52 total, with value-level assertions verifying
the permuted plane walk), plus 3 fixtures.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new read-only Spark DataSourceV2, netcdf.metadata, to expose NetCDF file metadata (dimensions, variables, attributes, CRS/extent) efficiently—designed to avoid loading data arrays and to support ranged reads for cloud/object storage, analogous to the existing geotiff.metadata source.

Changes:

  • Introduces netcdf.metadata DataSourceV2 implementation (table/scan/reader + Hadoop-backed random-access reader for ranged reads).
  • Adds comprehensive Spark-side and common-module NetCDF reader tests and extends NetCDF raster reading logic (coordinate resolution, axis permutation, packed/unsigned handling).
  • Registers the data source and documents usage/schema in the Sedona docs site.

Reviewed changes

Copilot reviewed 12 out of 42 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/io/netcdfmetadata/NetCdfMetadataDataSource.scala Implements the netcdf.metadata data source and directory/glob default behaviors; provides V1 fallback rejection with a descriptive error.
spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/io/netcdfmetadata/NetCdfMetadataTable.scala Defines the table and the NetCDF-native output schema for the data source.
spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/io/netcdfmetadata/NetCdfMetadataScanBuilder.scala Implements scan building, LIMIT pushdown handling, and scan equality semantics.
spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/io/netcdfmetadata/NetCdfMetadataPartitionReaderFactory.scala Creates per-file readers while preserving per-file Hive partition values even in bin-packed Spark partitions.
spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/io/netcdfmetadata/NetCdfMetadataPartitionReader.scala Extracts NetCDF header metadata and (optionally) coordinate arrays to compute extent/transform/CRS with CF-aware scoping rules.
spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/io/netcdfmetadata/HadoopRandomAccessFile.scala Adds a Hadoop FSDataInputStream-backed random-access implementation intended to encourage ranged reads on object stores.
spark/common/src/main/resources/META-INF/services/org.apache.spark.sql.sources.DataSourceRegister Registers NetCdfMetadataDataSource for Spark format lookup.
spark/common/src/test/scala/org/apache/sedona/sql/netcdfMetadataTest.scala Adds extensive end-to-end tests for schema fields, CRS/extent logic, column pruning, LIMIT pushdown, and ranged-read behaviors.
common/src/main/java/org/apache/sedona/common/raster/netcdf/NetCdfReader.java Enhances NetCDF raster extraction logic (explicit coordinate validation, permuted axes, unsigned/packed decoding, missing/validity rules).
common/src/test/java/org/apache/sedona/common/raster/RasterConstructorsTest.java Adds regression tests for packed band metadata, missing-value behavior, and int64 validity comparisons.
docs/tutorial/files/netcdfmetadata-sedona-spark.md Documents the new netcdf.metadata source, schema semantics, CRS/extent rules, and usage patterns.
mkdocs.yml Adds the new NetCDF metadata doc page to the site navigation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@jiayuasu
jiayuasu marked this pull request as ready for review July 19, 2026 08:53
@jiayuasu
jiayuasu requested a review from Copilot July 19, 2026 08:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 44 changed files in this pull request and generated 1 comment.

Comment thread docs/tutorial/files/netcdfmetadata-sedona-spark.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add netcdf.metadata data source for NetCDF file metadata

2 participants