Skip to content

Rebuild the Atlas reader on the single-file format - #490

Merged
robinskil merged 17 commits into
mainfrom
features/reimplement-atlas
Sep 9, 2026
Merged

robinskil merged 17 commits into
mainfrom
features/reimplement-atlas

Conversation

@robinskil

Copy link
Copy Markdown
Collaborator

Summary

Atlas moved to one write-once container, data.atlas. The old reader used the directory layout and was out of the build. This PR rebuilds the reader on the 0.17 container and registers STORED AS ATLAS and read_atlas again.

Changes

  • One segment holds one variable for every dataset. One open serves every dataset of a collection.
  • A LOCATION names the container, or a glob such as obs/**/data.atlas. Beacon does not read a collection from before 0.17.
  • A dataset attribute is a column under a leading dot, as in NetCDF and Zarr.
  • A scan reads through the shared nd pipeline. One dataset is one unit of work.
  • A reader pool holds one dataset queue per collection. Every partition receives every collection in a rotated order. Partitions share a queue, so Beacon reads a dataset once.
  • A predicate prunes whole datasets from the footer statistics in one pass. The filter above the scan still decides each row.
  • A read of no column uses the widest array of each dataset. count(*) returns the full row count and reads no cell.
  • Errors keep their cause chain. The crate converts to a DataFusion error in one place.
  • The crawler discovers a collection as one file.

Docs and tests

  • The format page, the tuning page, the configuration page and the changelog describe the current behaviour.
  • Unit tests cover the pool, the deal, the view and pruning. Session tests read three collections over three partitions. beacon-core tests cover the table function, external tables and a restart.

Known gaps

  • Beacon parses read_dimensions but does not apply it.
  • Each format instance owns its reader cache. There is no server setting.
  • Partitions share datasets, not chunks. One large last dataset uses one partition.

Atlas 0.16 replaced the directory of per-array files with one write-once
container, `data.atlas`, holding every dataset and a footer that describes
them all. The reader here was written against the old layout, every call it
made is gone, and the crate had been excluded from the workspace since the
morsel scan landed. `STORED AS ATLAS` and `read_atlas` failed.

The crate is rewritten on the new format and registered again. A `LOCATION`
names the container rather than a marker beside it. A collection written
before 0.16 is not read at all: its registry is not a marker, so a listing
passes over it. There is no compatibility path.

One dataset is one unit of work. The format lists a collection at plan time
and emits one entry per dataset, those entries go into the shared morsel
queue, and the scan sits under the nd spine like netCDF and Zarr. A worker
takes the next dataset when it is free and helps drain an open one when none
is left, so a collection of a million small datasets and one of four large
ones both divide over every core. Level two of the queue follows the chunk
shape the writer chose, so one pop reads one stored chunk.

A predicate skips whole datasets. The footer records the minimum, the maximum
and the null count of every array, so the first scan of a collection pivots
those into one index -- one row per dataset, one typed Arrow column per
column the predicate names -- and judges every dataset in a single vectorised
pass. A million datasets cost one pass rather than a million decisions. A
dataset-level attribute is exact in the footer, so a predicate on one prunes
too. Every path fails open, and the filter above the scan still decides each
row.

Three behaviour changes come with the rebuild. A dataset attribute is a
column under a leading dot, matching netCDF and Zarr rather than the bare
key. A column two datasets type in two families refuses the merge by name
instead of silently becoming text; `keep_first` settles it the other way. And
collections are crawlable now, because a collection is one file whose
extension is its format.

Atlas applies no CF decoding: it has a native timestamp type and `atlas
create` applies scale, offset and time units before the write.

Verified against a collection built by the real `atlas create`: its
per-dataset statistics are present, so pruning works on collections built the
normal way, and xarray's NaN fills and marker attributes read as documented.
The plan served the rebuild and is finished. What it decided is in the code
and its doc comments, what it changed for a user is in the changelog and the
format page, and the four requests it made of atlas-rust belong upstream
rather than in this repository.
Atlas 0.17 changes where the bytes live. A container used to hold one
segment per dataset, with the footer carrying every dataset's shapes,
attribute values and statistics. It now holds one segment per *variable*:
a segment holds one array name across the whole collection, and each
dataset's copy sits inside it under the dataset's own name.

The footer therefore names things and nothing else. An array's layout
(shape, chunking, dimension names, fill value), its statistics, and every
attribute value moved into the variable's segment, and reading one is
async. One open answers for every dataset of the collection, so a column
costs one request whether the collection holds ten datasets or a million.

What that changes here:

- The dataset build asks for a layout per array instead of reading it off
  the footer, and skips a dtype Beacon cannot surface before it opens
  anything.
- The pruning index gathers a column with one call —
  `array_stats_by_dataset` for an array, `attributes_by_dataset` for an
  attribute — and pivots what comes back. That drops the 100k-dataset
  limit on indexing an attribute, which existed only because an attribute
  had needed a view per dataset.
- Schema inference keys a dataset on its arrays, its attribute keys and
  its dimension names. The interned schema is coarser than it was — it
  names types and no longer implies a grid — so the names come from the
  segments to keep two datasets with different grids apart.
- `COUNT(*)` picks its driving array by element count from the layouts.
- An attribute can no longer be a timestamp: atlas stores none, because
  one would go to disk as an i64 and could not come back.

Verified against the fixtures atlas-rust 0.17 ships, including the one
its Python layer writes: schema, values, timestamps, string arrays and
both attribute scopes all read back.
A read that projects no column loaded no array, so every dataset sat on a
rank-0 grid and `count(*)` returned the dataset count. The view now drives
such a read with each dataset's widest array, and a chunk states its row
count through `DatasetSource::chunk_rows`, so a count reads no cell.

The changelog and the format, tuning, configuration and external-table
pages named four `BEACON_ATLAS_*` settings and two metrics that no longer
exist. They now describe the reader cache, pruning and the metrics as the
code has them.

The beacon-core atlas tests referenced the removed `AtlasConfig`. The
pruning test now compares a predicate against a full read filtered in
memory.
@codecov

codecov Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.12283% with 135 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.79%. Comparing base (2922c47) to head (76068c3).

Files with missing lines Patch % Lines
...file-formats/beacon-nd-array/src/dataset/source.rs 0.00% 35 Missing ⚠️
...rmats/beacon-arrow-atlas/src/datafusion/pruning.rs 93.83% 26 Missing ⚠️
...e-formats/beacon-arrow-atlas/src/datafusion/mod.rs 87.26% 20 Missing ⚠️
...acon-file-formats/beacon-arrow-atlas/src/compat.rs 96.20% 11 Missing ⚠️
...-formats/beacon-arrow-atlas/src/datafusion/view.rs 96.80% 10 Missing ⚠️
...eacon-file-formats/beacon-arrow-atlas/src/store.rs 95.87% 9 Missing ⚠️
...eacon-arrow-atlas/src/datafusion/table_function.rs 73.33% 8 Missing ⚠️
...-formats/beacon-arrow-atlas/src/datafusion/pool.rs 97.26% 7 Missing ⚠️
...formats/beacon-arrow-atlas/src/datafusion/error.rs 0.00% 3 Missing ⚠️
...con-file-formats/beacon-arrow-atlas/src/backend.rs 98.69% 2 Missing ⚠️
... and 3 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #490      +/-   ##
==========================================
+ Coverage   83.44%   83.79%   +0.35%     
==========================================
  Files         375      390      +15     
  Lines       63334    66523    +3189     
==========================================
+ Hits        52848    55742    +2894     
- Misses      10486    10781     +295     
Files with missing lines Coverage Δ
beacon-db/beacon-core/src/crawler/discovery.rs 99.64% <100.00%> (+0.01%) ⬆️
beacon-db/beacon-core/src/runtime_builder.rs 92.26% <100.00%> (+0.02%) ⬆️
beacon-db/beacon-datafusion-ext/src/nd/encoding.rs 98.87% <100.00%> (+<0.01%) ⬆️
beacon-db/beacon-datafusion-ext/src/nd/mod.rs 97.70% <ø> (ø)
beacon-db/beacon-datafusion-ext/src/scan_adapt.rs 98.44% <100.00%> (+0.01%) ⬆️
beacon-db/beacon-db-py/src/connection.rs 0.00% <ø> (ø)
...rmats/beacon-arrow-atlas/src/datafusion/metrics.rs 100.00% <ø> (ø)
...ormats/beacon-arrow-atlas/src/datafusion/opener.rs 100.00% <100.00%> (ø)
...ile-formats/beacon-arrow-atlas/src/test_support.rs 100.00% <100.00%> (ø)
...on-file-formats/beacon-nd-array/src/arrow/batch.rs 96.80% <100.00%> (+0.21%) ⬆️
... and 18 more

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@robinskil
robinskil merged commit 85d06d3 into main Sep 9, 2026
7 checks passed
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.

1 participant