writer: track attribute creation order with track_order
h5py's track_order=True orders attributes as well as links; the writer tracked links only. A tracking object's header now sets the attribute creation order tracked/indexed flags and carries per-message creation orders, an Attribute Info message holds the next order (inline too), and dense storage gets a type-9 creation-order index. The file default applies to datasets, with DatasetBuilder::track_order per dataset; more than 65 535 attributes on a tracking object is an error (libhdf5's counter is 2 bytes). The reader lists such attributes in creation order. h5py lists them in order (inline, dense, 20 000 on one dataset) and keeps numbering in r+ mode, including its inline-to-dense move. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -88,9 +88,11 @@ impl FileBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Track link creation order in every group that does not set its own
|
||||
/// (`GroupBuilder::track_order`), as h5py's `track_order=True`: libhdf5
|
||||
/// then lists members in the order they were added.
|
||||
/// Track the creation order of links and attributes in every group, and
|
||||
/// of attributes on every dataset, that does not set its own
|
||||
/// (`GroupBuilder::track_order`, `DatasetBuilder::track_order`), as
|
||||
/// h5py's `track_order=True`: libhdf5 then lists members and attributes
|
||||
/// in the order they were added.
|
||||
pub fn track_order(&mut self, track: bool) -> &mut Self {
|
||||
self.writer.track_order(track);
|
||||
self
|
||||
|
||||
@@ -531,6 +531,97 @@ fn ten_thousand_links_in_one_group() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn track_order_lists_attributes_in_creation_order() {
|
||||
skip_if_no_python!();
|
||||
// h5py's track_order=True orders an object's attributes as well as a
|
||||
// group's links; the writer tracked links only, so h5py listed the
|
||||
// attributes by name. Now the object header's flags say attribute
|
||||
// creation order is tracked and indexed, an Attribute Info message
|
||||
// holds the next order, inline attributes carry theirs, and dense
|
||||
// storage gets a creation-order index (B-tree type 9).
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let small = ["zeta", "alpha", "mid"];
|
||||
let mut b = FileBuilder::new();
|
||||
b.track_order(true); // the root, and every group and dataset by default
|
||||
for (i, n) in small.iter().enumerate() {
|
||||
b.set_attr(n, AttrValue::I64(i as i64));
|
||||
}
|
||||
let mut g = b.create_group("g"); // dense: 30 attributes
|
||||
for i in (0..30).rev() {
|
||||
g.set_attr(&format!("a{i:02}"), AttrValue::I64(i));
|
||||
}
|
||||
b.add_group(g.finish());
|
||||
let d = b.create_dataset("d");
|
||||
d.with_i32_data(&[1]);
|
||||
for (i, n) in small.iter().enumerate() {
|
||||
d.set_attr(n, AttrValue::I64(i as i64));
|
||||
}
|
||||
// 20 000 attributes: a one-leaf creation-order index of 20 000 records.
|
||||
let big = b.create_dataset("big");
|
||||
big.with_i32_data(&[2]);
|
||||
for i in (0..20_000).rev() {
|
||||
big.set_attr(&format!("b{i:05}"), AttrValue::I64(i));
|
||||
}
|
||||
let plain = b.create_dataset("plain");
|
||||
plain.with_i32_data(&[3]).track_order(false);
|
||||
for (i, n) in small.iter().enumerate() {
|
||||
plain.set_attr(n, AttrValue::I64(i as i64));
|
||||
}
|
||||
let path = write(&dir, "attr_order.h5", b);
|
||||
|
||||
let out = h5py(
|
||||
&path,
|
||||
"def order(o):\n\
|
||||
\x20 return o.id.get_create_plist().get_attr_creation_order()\n\
|
||||
with h5py.File(path, 'r') as f:\n\
|
||||
\x20 big = list(f['big'].attrs)\n\
|
||||
\x20 print(json.dumps([list(f.attrs), [int(v) for v in f.attrs.values()],\n\
|
||||
\x20 list(f['g'].attrs)[:3], len(f['g'].attrs), list(f['d'].attrs),\n\
|
||||
\x20 big[:2], big == ['b%05d' % i for i in range(19999, -1, -1)],\n\
|
||||
\x20 int(f['big'].attrs['b00007']), list(f['plain'].attrs),\n\
|
||||
\x20 [order(f['/']), order(f['g']), order(f['d']), order(f['plain'])]]))",
|
||||
);
|
||||
assert_eq!(
|
||||
out,
|
||||
r#"[["zeta", "alpha", "mid"], [0, 1, 2], ["a29", "a28", "a27"], 30, ["zeta", "alpha", "mid"], ["b19999", "b19998"], true, 7, ["alpha", "mid", "zeta"], [3, 3, 3, 0]]"#
|
||||
);
|
||||
h5dump_ok(&path);
|
||||
let f = File::open(&path).unwrap();
|
||||
assert_eq!(f.dataset("big").unwrap().attrs().unwrap().len(), 20_000);
|
||||
assert!(matches!(
|
||||
f.group("g").unwrap().attrs().unwrap()["a07"],
|
||||
AttrValue::I64(7)
|
||||
));
|
||||
drop(f);
|
||||
|
||||
// libhdf5 continues the numbering: new attributes come last, also when
|
||||
// it moves the inline ones of `d` to dense storage.
|
||||
let out = h5py(
|
||||
&path,
|
||||
"with h5py.File(path, 'r+') as f:\n\
|
||||
\x20 f.attrs['new'] = 9\n\
|
||||
\x20 del f.attrs['alpha']\n\
|
||||
\x20 f['g'].attrs['new'] = 9\n\
|
||||
\x20 del f['g'].attrs['a15']\n\
|
||||
\x20 for i in range(8):\n\
|
||||
\x20 f['d'].attrs['x%d' % i] = i\n\
|
||||
\x20 f['big'].attrs['new'] = 9\n\
|
||||
\x20 for i in range(0, 20000, 2):\n\
|
||||
\x20 del f['big'].attrs['b%05d' % i]\n\
|
||||
with h5py.File(path, 'r') as f:\n\
|
||||
\x20 big = list(f['big'].attrs)\n\
|
||||
\x20 print(json.dumps([list(f.attrs), list(f['g'].attrs)[-2:], len(f['g'].attrs),\n\
|
||||
\x20 list(f['d'].attrs)[:4], len(f['d'].attrs),\n\
|
||||
\x20 big == ['b%05d' % i for i in range(19999, -1, -2)] + ['new']]))",
|
||||
);
|
||||
assert_eq!(
|
||||
out,
|
||||
r#"[["zeta", "mid", "new"], ["a00", "new"], 30, ["zeta", "alpha", "mid", "x0"], 11, true]"#
|
||||
);
|
||||
h5dump_ok(&path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn track_order_lists_members_in_creation_order() {
|
||||
skip_if_no_python!();
|
||||
|
||||
Reference in New Issue
Block a user