Skip to content

Finish the Locator migration: no library code fetches (#114), and keep unknown locator types (#122) - #151

Merged
sathabbott merged 11 commits into
nsmith-:mainfrom
sathabbott:fix/114-locator-migration
Oct 6, 2026
Merged

sathabbott merged 11 commits into
nsmith-:mainfrom
sathabbott:fix/114-locator-migration

Conversation

@sathabbott

@sathabbott sathabbott commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

🤖 AI generated content

Fixes #114. Fixes #122. Fixes #149.

No library object fetches data any more: both older generations of "get me the object" go, and every reference is reached through a *_locator, as docs/design.md asks ("objects describe where data is; callers decide when and how to fetch it"). The plan was posted on #114 (2026-09-24, updated 2026-10-05).

What changed

The DataFetcher interface goes (ROOTFile.get_TFile, get_StreamerInfo, TFile.get_KeyList, TDirectory.get_KeyList, TKey.read_object, DataFetcher). TKey.read_from(buffer) and TypedTKey.read_from(buffer) are now the implementation, with no behaviour change. Before removing the get_* methods I compared their checks with their locators': KeyListLocator lacked one, that the key's fNbytes equals the directory's fNbytesKeys, and now has it.

The middle generation goes (RNTuple.from_anchor, ROOT3a3aRNTuple.get_header / get_footer, REnvelopeLink.read_envelope, FooterEnvelope.get_pagelists, PageListEnvelope.get_pages, RPageDescription.get_page). The chain is anchor → header_locator / footer_locator → footer's pagelist_locators → each page description's page_locator.

  • RNTuple.from_envelopes(header, footer, pagelists) does no I/O and keeps from_anchor's checks: the footer and every page list must record the header's checksum.
  • Fetcher.rntuple(anchor) fetches the envelopes one read at a time; from a FileReader, reader.fetch.rntuple(anchor). It is on Fetcher because an RNTuple needs only the bootstrap classes, so it works on a Fetcher on BOOTSTRAP_CONTEXT. That is the only way to read the RNTuples of files whose StreamerInfo can't be turned into classes: FileReader.open fails on the CMS MiniAOD files (Implement double32 type #41). It takes the anchor, so finding the key stays with the caller.

Pages follow the envelope pattern, as set out approving #128. RPageDescription is the page-list record only, like REnvelopeLink, and loses offset, size and read_from. Its page_locator returns an RPageLocator, which follows the Locator protocol and, like REnvelopeLocator, keeps the RLocator. Its size covers the checksum trailer when there is one, and its read_from verifies it (moved from #128's RPageDescription.read_from). PageListEnvelope.page_locators returns RPageLocators. #131's InterpretablePage.pageDescription stays the record; its docstring says the bytes come from page_locator.

Unknown locator types are kept (#122). RLocator.read raised on any non-standard type but 0x01, so one such page made its whole page list unreadable. ROOT reads an unknown type as kTypeUnknown and fails only when reading what it locates (RNTupleSerializer::DeserializeLocator, tree/ntuple/src/RNTupleSerialize.cxx:1112-1155 at 6.40.04). Now such a locator reads as an UnknownLocator with its type, reserved byte and payload as stored, and page_locator / envelope_locator raise NotImplementedError on it. That includes 0x02, ROOT's DAOS locator (ERRATA 4, "Locator type 0x02 is assigned and implemented, not reserved"); reading it is #148. Only StandardLocator and LargeLocator (FileLocator) can be fetched.

A large locator as ROOT writes it now reads (#149, found while doing #122). ROOT writes a non-standard locator's first word as the negation of (type << 24 | reserved << 16 | size), where the size counts the word itself, and negates it back before splitting it (:1104-1107, :1124-1127). rootfilespec took the type from the top byte of the negated word, so a large locator read as type 0xfe and raised. No fixture has one: ROOT writes them only for blocks over 2 GiB. (The spec's "the absolute value of the 8bit integer" (BinaryFormatSpecification, "Locators and Envelope Links") gives the type only when the low 24 bits are zero, and they hold the size: filed as ariostas/root-io-spec#5.)

docs/design.md: the RNTuple locator chain, the record/locator split, an RNTuple example, a batch example that fetches the header and footer in parallel and then the page lists, and the bootstrap-only path.

Tests

  • test_record_locators.py: every check the removed get_* methods made, on the locators, on rntuple/anchor.root. test_size_mismatch_raises[keylist] fails on main.
  • test_rntuple_fetching.py: on every root-io-spec RNTuple fixture, the RNTuple built by hand from the locators and from_envelopes equals Fetcher.rntuple's, and a Fetcher on BOOTSTRAP_CONTEXT reads the same RNTuples as open_path. from_envelopes refuses a footer, and a page list, recording another header's checksum. All fail on main (no from_envelopes, no Fetcher.rntuple).
  • test_unknown_locators.py: a large locator as ROOT writes it reads (fails on main: type 0xfe). Types 0x7e, 0x02 (both Object64 payload sizes) and an unused one keep their payload and reserved byte. rntuple/anchor.root with its page locator at 724 (in the page list's RBlob record, which case.toml places at 590) replaced by a 0x7e locator of the same length (envelope checksum recomputed) reads, and only that page's page_locator raises (fails on main: the page list raises).
  • Ported, assertions unchanged: test_read_multiple_rntuples (the last test on the DataFetcher interface, now a Fetcher on BOOTSTRAP_CONTEXT), every from_anchor call (→ fetch.rntuple, including those in [rntuple] Refuse a cluster summary with the sharded-cluster flag #146's test_sharded_clusters.py and [rntuple] Read a frame's members from its fSize bytes only #150's test_frame_bounds.py), read_object calls (→ read_from), and test_page_checksums.py (→ page_locator).

Full suite: 631 passed, 59 skipped, 49 xfailed, on Python 3.13. pre-commit run --all-files passes.

Breaking changes

Removed or changed Use instead
ROOTFile.get_TFile(fetch_data) fetch.resolve(file.tfile_locator)
ROOTFile.get_StreamerInfo(fetch_data) fetch.resolve(file.streamerinfo_locator)
TFile.get_KeyList(fetch_data), TDirectory.get_KeyList(fetch_data) fetch.resolve(directory.keylist_locator), or reader.keylist(directory)
TKey.read_object(fetch_data) fetch(key), or key.read_from(buffer): typed object since #147, so narrow with isinstance
TKey.read_object(fetch_data, cls) fetch(TypedTKey(key, cls))
rootfilespec.serializable.DataFetcher a Fetcher, or your own fetching of (offset, size)
RNTuple.from_anchor(anchor, fetch.buffer) fetch.rntuple(anchor), or RNTuple.from_envelopes(header, footer, pagelists)
ROOT3a3aRNTuple.get_header / get_footer fetch(anchor.header_locator) / fetch(anchor.footer_locator)
REnvelopeLink.read_envelope(fetch_data, envtype) fetch(link.envelope_locator(envtype))
FooterEnvelope.get_pagelists(fetch_data) [fetch(loc) for loc in footer.pagelist_locators]
PageListEnvelope.get_pages(fetch_data) fetch(loc) for each of pagelist.page_locators
RPageDescription.get_page, .read_from, .offset, .size page.page_locator (an RPageLocator): fetch(page.page_locator), .offset, .size
PageListEnvelope.page_locators gave RPageDescriptions it gives RPageLocators; the records are in pageLocations
RLocator.size only on StandardLocator and LargeLocator; an UnknownLocator has none
LargeLocator.read(buffer) (read the payload) RLocator.read(buffer) for a whole locator; LargeLocator.from_payload(payload, reserved). LargeLocator.read and UnknownLocator.read raise TypeError: which class a locator is depends on its first word
RLocator.read raised ValueError on an unknown non-standard type returns an UnknownLocator; page_locator / envelope_locator raise NotImplementedError on it
KeyListLocator.read_from accepted a key whose fNbytes differs from the locator's size raises ValueError

Assisted-by: claude-code:claude-opus-5-5

TKey.read_from and TypedTKey.read_from read the key's object from the buffer
they are given, through _read_payload, instead of passing read_object a
lambda that returns it. read_object becomes a wrapper that fetches the key's
bytes and calls the same code, until it is removed with the rest of the
DataFetcher interface (nsmith-#114).

No behaviour change, except that read_object on an RBlob key now fetches its
bytes before refusing it.

Assisted-by: claude-code:claude-opus-5-5
Before ROOTFile.get_TFile, get_StreamerInfo and TDirectory.get_KeyList go
(nsmith-#114), compare their checks with their locators'. TFileLocator and
StreamerInfoLocator already make every check theirs do. KeyListLocator
lacked one: the key at fSeekKeys must have fNbytes == fNbytesKeys, the size
the directory records for its key list. It now raises otherwise.

tests/test_record_locators.py pins each check of the three locators on
rntuple/anchor.root; test_size_mismatch_raises[keylist] fails on main.

Assisted-by: claude-code:claude-opus-5-5
The last test on the DataFetcher interface (get_TFile, get_KeyList,
read_object) reads through rootfilespec.reader instead, so that interface
can go (nsmith-#114). It keeps to the bootstrap classes, now as a Fetcher on
BOOTSTRAP_CONTEXT: the path for files whose StreamerInfo can't be turned into
classes, such as the CMS MiniAOD files (nsmith-#41). Its assertions are unchanged.

Assisted-by: claude-code:claude-opus-5-5
ROOTFile.get_TFile, ROOTFile.get_StreamerInfo, TFile.get_KeyList,
TDirectory.get_KeyList, TKey.read_object and the DataFetcher alias fetched
inside library code, which docs/design.md rules out ("objects describe where
data is; callers decide when and how to fetch it"). Each has a locator that
does the same without fetching, with the same checks (previous commit):

- get_TFile: fetch.resolve(file.tfile_locator)
- get_StreamerInfo: fetch.resolve(file.streamerinfo_locator)
- get_KeyList: fetch.resolve(directory.keylist_locator), or reader.keylist()
- key.read_object(fetch_data): fetch(key), or key.read_from(buffer)
- key.read_object(fetch_data, cls): fetch(TypedTKey(key, cls))

The tests that called read_object call read_from on the same bytes.

Part of nsmith-#114.

Assisted-by: claude-code:claude-opus-5-5
RNTuple.from_anchor fetched, through a locator-to-buffer callable, and so did
the methods it called: ROOT3a3aRNTuple.get_header / get_footer,
REnvelopeLink.read_envelope, FooterEnvelope.get_pagelists. So did
PageListEnvelope.get_pages and RPageDescription.get_page. docs/design.md rules
that out, and Nick asked for the whole middle generation to go (nsmith-#114). All are
removed; every reference in the chain is reached through its locator:
anchor -> header_locator / footer_locator -> footer.pagelist_locators ->
page list.

- RNTuple.from_envelopes(header, footer, pagelists) does no I/O and keeps
  from_anchor's checks: the footer and each page list must record the
  header's checksum.
- Fetcher.rntuple(anchor) fetches the envelopes one read at a time, for the
  synchronous case. It is on Fetcher rather than FileReader because an
  RNTuple needs only the bootstrap classes: a Fetcher on BOOTSTRAP_CONTEXT
  reads the RNTuples of files that FileReader.open can't open, such as the
  CMS MiniAOD files (nsmith-#41). From a FileReader: reader.fetch.rntuple(anchor).
  It takes the anchor, so finding the key stays with the caller.

Tests: from_anchor calls become fetch.rntuple. New
tests/test_rntuple_fetching.py: on every root-io-spec RNTuple fixture, the
RNTuple built by hand from the locators equals Fetcher.rntuple's, and a
bootstrap-only Fetcher reads the same RNTuples as open_path; from_envelopes
refuses a footer or a page list of another header. New
tests/test_no_fetch_callables.py: no public function or method under
rootfilespec.bootstrap or rootfilespec.rntuple takes a callable or a fetch
argument; on main it lists get_TFile, from_anchor and the others.

Part of nsmith-#114.

Assisted-by: claude-code:claude-opus-5-5
RPageDescription was its page's locator (nsmith-#128). As Nick set out approving
nsmith-#128, pages now follow the envelope pattern (REnvelopeLink and
REnvelopeLocator): RPageDescription is the page-list record only, and loses
offset, size and read_from, so there is one way to read a page. Its
page_locator returns an RPageLocator, which follows the Locator protocol and,
like REnvelopeLocator, keeps the RLocator instead of copying out integers.
Its size covers the checksum trailer when the page has one, and its
read_from verifies it (moved from RPageDescription.read_from).

page_locator covers the locators of a byte range in the file,
StandardLocator and LargeLocator (FileLocator), and raises
NotImplementedError for any other: the Locator protocol is an offset and a
size in the file. PageListEnvelope.page_locators returns RPageLocators.

InterpretablePage.pageDescription (nsmith-#131) stays the record; its docstring now
says the bytes come from page_locator.

Tests: tests/test_page_checksums.py reads pages through page_locator, with
the same assertions on rntuple/anchor.root's page at 550 and on every
fixture's pages.

Part of nsmith-#114.

Assisted-by: claude-code:claude-opus-5-5
… ROOT does

RLocator.read raised on any non-standard locator type but 0x01, so one page
with such a locator made its whole page list unreadable. ROOT reads a type it
doesn't know as kTypeUnknown, and only fails when something reads what it
locates (RNTupleSerializer::DeserializeLocator,
tree/ntuple/src/RNTupleSerialize.cxx:1112-1155 at 6.40.04). rootfilespec now
does the same (nsmith-#122): such a locator reads as an UnknownLocator, keeping its
type, reserved byte and payload as stored, and page_locator and
envelope_locator raise NotImplementedError on it. That includes 0x02, ROOT's
DAOS locator (root-io-spec ERRATA 4), which the release doesn't need;
reading it is nsmith-#148.

Finding the payload needs the locator's own size, and that showed the header
word was decoded wrongly. ROOT writes it as the negation of
(type << 24 | reserved << 16 | size), with the size counting the word
itself, and negates it back before splitting it (:1104-1107, :1124-1127).
rootfilespec took the type from the top byte of the negated word, so a
large locator as ROOT writes it read as type 0xfe and raised. No fixture has
one: ROOT writes them only for blocks over 2 GiB. The spec's "absolute value
of the 8bit integer" agrees with ROOT only when the low 24 bits are zero,
which they never are.

- RLocator no longer has a size: an unknown locator has none.
  StandardLocator and LargeLocator have their own, in the same place.
- LargeLocator keeps its reserved byte (default 0); a payload other than 16
  bytes raises. LargeLocator.read is replaced by LargeLocator.from_payload.
- REnvelopeLocator.locator is a FileLocator, so offset needs no cast.

Tests: tests/test_unknown_locators.py. A large locator as ROOT writes it
reads (fails on main: type 0xfe); types 0x7e, 0x02 (both Object64 payload
sizes) and an unused type are kept with their payload and reserved byte;
rntuple/anchor.root with its page locator at 724 replaced by a 0x7e locator
of the same length and the envelope checksum recomputed reads, and only
that page's page_locator raises (fails on main: the page list raises).

Assisted-by: claude-code:claude-opus-5-5
The Locator Pattern gains the RNTuple chain (anchor -> header_locator /
footer_locator -> pagelist_locators -> page_locator), the record/locator
split pages now share with envelopes, which locators can be fetched, and
RNTuple.from_envelopes. The reader section gains an RNTuple example, and the
bootstrap-only path for files whose StreamerInfo can't be turned into
classes (nsmith-#41). The sentence about the callable RNTuple.from_anchor expected
goes with from_anchor. Both examples were run on rntuple/anchor.root.

Part of nsmith-#114.

Assisted-by: claude-code:claude-opus-5-5
…smith-#150) into fix/114-locator-migration

Conflicts resolved and their changes ported to this branch's API:

- N (nsmith-#147): TKey._read_payload without objtype and TKey.read_from return
  object, as N types read_object and read_from; N's unbound T_co and its
  comment kept, without DataFetcher.
- M (nsmith-#146): pagelist.py keeps M's Members / ReadBuffer imports for
  ClusterSummary.update_members; tests/test_sharded_clusters.py uses
  reader.fetch.rntuple(anchor).
- O (nsmith-#145): O's key lookups and isinstance narrowing kept in the tests both
  sides edited, with from_anchor ported to fetch.rntuple;
  test_read_multiple_rntuples and the two docs/design.md RNTuple examples use
  keylist.get_by_name.
- P (nsmith-#150): tests/test_frame_bounds.py uses reader.fetch.rntuple(anchor).

631 passed, 59 skipped, 49 xfailed on Python 3.13, 3.14 and 3.10;
pre-commit clean.

Assisted-by: claude-code:claude-opus-5-5
@codecov-commenter

codecov-commenter commented Oct 6, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.13%. Comparing base (dd03c6c) to head (3cb6a71).
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #151      +/-   ##
==========================================
+ Coverage   93.36%   95.13%   +1.77%     
==========================================
  Files          37       37              
  Lines        3299     3269      -30     
==========================================
+ Hits         3080     3110      +30     
+ Misses        219      159      -60     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

… types

Which class a locator is depends on its first word, so LargeLocator.read and
UnknownLocator.read, inherited from RLocator.read, could return another
class (a StandardLocator, say), unlike every other X.read in rootfilespec.
They now raise TypeError pointing to RLocator.read, which nothing in
rootfilespec bypasses. StandardLocator.read is unchanged.

tests/test_no_fetch_callables.py types its helpers with ModuleType and
Callable[..., object] instead of object, which drops its type: ignore;
test_rntuple_fetching.py annotates the key list. New:
test_only_rlocator_reads_a_locator.

Part of nsmith-#114.

Assisted-by: claude-code:claude-opus-5-5
@sathabbott
sathabbott requested a review from nsmith- October 6, 2026 00:49
@sathabbott

Copy link
Copy Markdown
Collaborator Author

@nsmith- ok I have a decent understanding of this, and I think it is ready for you!

nsmith-
nsmith- previously requested changes Oct 6, 2026

@nsmith- nsmith- left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Two changes, one item to file for later work

Comment thread docs/design.md
Comment thread src/rootfilespec/rntuple/pagelocations.py
Comment thread tests/test_no_fetch_callables.py Outdated
docs/design.md gains a batch example for the RNTuple case, as Nick asked on
nsmith-#151: with an asynchronous fetch_data, the header and the footer are fetched
in parallel, then every page list, and RNTuple.from_envelopes assembles them.
Run on every root-io-spec RNTuple fixture, it gives the RNTuple that
Fetcher.rntuple gives.

tests/test_no_fetch_callables.py goes (Nick: not needed).

Part of nsmith-#114.

Assisted-by: claude-code:claude-opus-5-5
@sathabbott
sathabbott requested a review from nsmith- October 6, 2026 14:08
@sathabbott
sathabbott dismissed nsmith-’s stale review October 6, 2026 14:51

He has approved it

@sathabbott
sathabbott merged commit 7958d73 into nsmith-:main Oct 6, 2026
10 checks passed
@sathabbott
sathabbott deleted the fix/114-locator-migration branch October 6, 2026 14:56
sathabbott added a commit that referenced this pull request Oct 6, 2026
…51) (#156)

* Read the footer's list of linked attribute sets

The footer ends with a list frame of linked attribute set records (spec,
"Footer Envelope", "Linked Attribute Set Record Frame"), which
rootfilespec left in the footer's _unknown: the empty list of every footer
of format 1.0.1.0 or later, and both records of attributes.root.

- LinkedAttributeSet, a record frame: fSchemaVersionMajor and
  fSchemaVersionMinor (u16), fAnchorLength (u32: the uncompressed length
  of the set's anchor, 78 bytes for class version 2, root-io-spec RNTuple
  ERRATA 12), locator (the anchor object's RLocator), fName (an RNTuple
  string). Field for field as RNTupleSerializer writes and reads it
  (RNTupleSerialize.cxx:1843-1866, :2045-2078 at 6.40.04).
- FooterEnvelope.attributeSets, a ListFrame of them, or None when the
  footer ends before it: the list arrived with format 1.0.1.0, and ROOT
  reads it only if bytes remain before the checksum (ERRATA 11;
  RNTupleSerialize.cxx:2015-2017). REnvelope.read already hands
  update_members a payload that ends before the checksum (#129), so the
  remaining payload length #51 asks for is the length of that buffer.
- structutil.IfBytesRemain: Annotated[T | None, IfBytesRemain()] reads T
  if bytes remain and gives None otherwise.

Tests (tests/test_attribute_sets.py): attributes.root's two records
against its case.toml; every root-io-spec RNTuple fixture's footer has
the list, and nothing in _unknown; the two 1.0.0.0 files of
scikit-hep-testdata have no list and still read. The hardcoded footers
of those two files gain attributeSets=None.

Part of #51.

Assisted-by: claude-code:claude-opus-5-5

* Open an attribute set through its footer record: anchor_locator

An attribute set's anchor is a ROOT::RNTuple key that no directory lists:
the footer record's locator, which names the anchor object rather than
its key, is the only way to it (root-io-spec RNTuple NOTES 8, ERRATA 12).
LinkedAttributeSet.anchor_locator returns a locator of that anchor, read
with ROOT3a3aRNTuple.read (#85's checksum and length rules), so the set
opens like any RNTuple: fetch.rntuple(fetch(record.anchor_locator)).

ROOT opens the anchor from the record as an RNTupleLink, the link of an
uncompressed length and a locator that envelopes use, and decompresses it
the same way (RPageSourceFile::OpenWithDifferentAnchor,
RPageStorageFile.cxx:369-376 at 6.40.04). So anchor_locator is an
REnvelopeLocator, whose type parameter now allows any ROOTSerializable
rather than only an REnvelope. Like envelope_locator, it raises
NotImplementedError if the locator is not in the file.

Tests: attributes.root's two sets open through their records, at the
anchor offsets, header and footer positions the case.toml pins; each is
an RNTuple named after its set, with schema 1.0's three top-level fields
and an empty attribute set list of its own; the _rangeStart and
_rangeLen pages read (0, 3), (3, 3) for "runs" and (3, 0), (1, 4) for
"flags". A record whose locator is not in the file raises.

Part of #51.

Assisted-by: claude-code:claude-opus-5-5

* Check attribute sets as ROOT does: names, schema version, meta-fields

What ROOT refuses about attribute sets, and where:

- Reading the main footer, a set without a name, or two of one name
  (RNTupleAttrSetDescriptorBuilder::MoveDescriptor and
  RNTupleDescriptorBuilder::AddAttributeSet, RNTupleDescriptor.cxx:1156-1159,
  :1438-1446 at 6.40.04; spec, "Linked Attribute Sets": "All linked
  attribute sets must have a non-empty, distinct name"). Now
  RNTuple.from_envelopes raises ValueError.
- Opening a set (RNTupleAttrSetReader, RNTupleAttrReading.cxx:20-46): a
  major schema version other than 1, which the spec says a reader refuses
  ("Attribute Schema Version"); and top-level fields other than
  _rangeStart, _rangeLen and _userData, in that order. That includes a
  fourth field, which ROOT refuses whatever the minor version, though the
  spec says a later minor version's fields are ignored (root-io-spec
  RNTuple ERRATA 13). Now LinkedAttributeSet.check(rntuple) raises
  NotImplementedError for the major version and ValueError for the fields.

check also tests the spec's three restrictions on an attribute set's
RNTuple, which ROOT's writer keeps and its reader doesn't check (NOTES 8):
no attribute sets of its own, no alias columns, no field of structural
role 0x04.

Fetcher.attribute_set(record) opens a set as ROOT's OpenAttributeSet
does: fetch.rntuple of the record's anchor, then record.check.
fetch.rntuple(fetch(record.anchor_locator)) still reads any set as an
RNTuple.

Tests: both sets of attributes.root open with Fetcher.attribute_set; with
the "runs" record's major version patched to 2 (footer checksum
recomputed), the main RNTuple reads and "flags" opens but "runs" raises,
as ERRATA 13 found ROOT does. On copies of "runs": a fourth top-level
field (minor version 0 and 1), a renamed meta-field, and each restriction
raise. A footer with a duplicate or empty set name raises in
from_envelopes.

Part of #51.

Assisted-by: claude-code:claude-opus-5-5

* docs/design.md: attribute sets in the RNTuple locator chain

Each linked attribute set record has an anchor_locator, and
Fetcher.attribute_set opens the set, in the RNTuple example.

Part of #51.

Assisted-by: claude-code:claude-opus-5-5

* Check attribute sets only as ROOT does: drop the spec's restrictions

LinkedAttributeSet.check no longer refuses a set that links sets of its
own, has alias columns, or has a field of structural role 0x04. ROOT's
writer never writes such a set, but its reader opens one without
complaint (root-io-spec RNTuple NOTES 8: the set reader checks only the
major version and the three meta-fields, RNTupleAttrReading.cxx:20-46 at
6.40.04), and rootfilespec aims at ROOT's strictness. check keeps those
two tests.

test_restrictions becomes test_restrictions_are_not_checked: a copy of
"runs" breaking all three passes.

Part of #51.

Assisted-by: claude-code:claude-opus-5-5

* A class of its own for linked anchors: RAnchorLocator

LinkedAttributeSet.anchor_locator returned an REnvelopeLocator of
ROOT3a3aRNTuple, with REnvelopeLocator's type parameter widened from
REnvelope to any ROOTSerializable. An anchor isn't an envelope, so it now
has its own locator, RAnchorLocator (uncompressed length and FileLocator,
read with ROOT3a3aRNTuple.read), and REnvelopeLocator is as #151 left it.

The two share what ROOT does for both, as it opens a linked anchor as an
RNTupleLink, the link envelopes use (RPageStorageFile.cxx:369-376 at
6.40.04): check the fetched size against the locator's, and decompress
when the stored size is below the length (root-io-spec RNTuple NOTES 2).
That moves from REnvelopeLocator.read_from to the helper _uncompressed,
with the same errors.

Tests: anchor_locator is an RAnchorLocator; "runs"' anchor in a zlib
block reads as the raw one does; a short fetch raises.

Part of #51.

Assisted-by: claude-code:claude-opus-5-5

* Refuse the attribute set records ROOT refuses as it reads the footer

ROOT refuses the whole footer, so the main RNTuple, for an attribute set
record with an anchor length of 0, or with an anchor locator of a type it
doesn't know: any non-standard type but 0x01 and 0x02, which
DeserializeLocator reads as kTypeUnknown
(RNTupleAttrSetDescriptorBuilder::MoveDescriptor, RNTupleDescriptor.cxx:
1156-1163; RNTupleSerialize.cxx:1131-1141 at 6.40.04). rootfilespec read
such a footer and failed only when the set was opened. RNTuple.from_envelopes
now refuses both, with the empty and repeated names it already refused,
in one function, _check_attribute_sets.

A DAOS (0x02) anchor locator is a type ROOT knows: the footer reads, and
anchor_locator raises when the set is opened, as before.

Tests: from_envelopes raises for an anchor length of 0 and for anchor
locators of types 0x7e (ROOT's test type) and 0x40; a 0x02 locator reads,
and only its anchor_locator raises. The names test uses the same helper.

Part of #51.

Assisted-by: claude-code:claude-opus-5-5

* docs/design.md: RAnchorLocator among the records and their locators

Part of #51.

Assisted-by: claude-code:claude-opus-5-5

* docs/design.md: attribute sets in the RNTuple batch-fetching example

Their anchors are known once the footer is, so they can be fetched in
parallel with the page lists, and each set read the same way.

Part of #51.

Assisted-by: claude-code:claude-opus-5-5

* docs/design.md: keep RNTuple details out, as on main

Nick on #156: the file should stay scoped to the design philosophy, with
only a few examples, not details of the RNTuple structure. F's additions
(attribute sets in the RNTuple locator chain, RAnchorLocator among the
records and their locators, and attribute sets after the batch-fetching
example) go; the file is as on main. Attribute sets are documented in the
docstrings of LinkedAttributeSet, RAnchorLocator and Fetcher.attribute_set.

Part of #51.

Assisted-by: claude-code:claude-opus-5-5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants