Reading Attributes#
read_attributes requests one or more attribute columns over a
query and returns the results as groups you fetch and
then release. Reads target sealed data at or below the write floor, so advance
the floor after writing (refer to Writing Attributes) before reading the data
back.
Read → Fetch → Release#
The minimal example writes a column, seals it, then reads it back — mapping the returned tensor as a zero-copy view before releasing the group and the read:
# Write one float per prim into the "temperature" column at ordinal 1,
# seal it by advancing the write floor to 1, then read it back. Tensor
# data crosses as a numpy array (CPU) via DLPack; async ops return an
# Operation whose .wait() raises OvstageError on failure. The read is
# a context manager -- block exit releases its handle even when an
# error interrupts -- and the fetched group is released in a finally.
stage.write_attribute(
query, attr, ordinal=1, tensors=np.array([1.0, 2.0, 3.0], np.float32), is_array=False
).wait()
stage.advance_write_floor(ordinal=1).wait()
with stage.read_attributes(query, [attr], OrdinalRange.latest(1)) as read:
read.wait()
group = read.fetch_next()
if group is None:
raise SystemExit("read returned no group at ordinal 1")
try:
# group.array(i) is a zero-copy numpy view of tensor i (CPU).
print("read back ordinal", group.ordinal, group.array(0)) # -> [1. 2. 3.]
finally:
stage.release_group(group)
// Write one float per prim into the "temperature" column (UPSERT creates
// the prims on first write), seal it by advancing the write floor to
// ordinal 1, then read the column back.
float values[] = { 1.0f, 2.0f, 3.0f };
int64_t shape[] = { 3 };
int64_t strides[] = { 1 };
DLTensor tensor{};
tensor.data = values;
tensor.device = { kDLCPU, 0 };
tensor.ndim = 1;
tensor.dtype = { kDLFloat, 32, 1 }; // {code, bits, lanes}
tensor.shape = shape;
tensor.strides = strides;
ovstage_write_data_t write{};
write.tensors = &tensor;
write.tensor_count = 1;
write.is_array = false;
ovstage_enqueue_result_t enq =
ovstage_write_attribute(stage, query, attrArg, /*ordinal*/ 1, write, OVSTAGE_PRIM_MODE_UPSERT);
waitOp(stage, enq, "write_attribute");
ovstage_write_floor_desc_t writeFloor{};
writeFloor.ordinal = 1;
writeFloor.scope = OVSTAGE_SCOPE_ALL;
enq = ovstage_advance_write_floor(stage, &writeFloor);
waitOp(stage, enq, "advance_write_floor");
ovstage_ordinal_range_t range{};
range.end_ordinal = 1;
range.has_start_ordinal = false;
ovstage_read_handle_t read = OVSTAGE_INVALID_READ_HANDLE;
enq = ovstage_read_attributes(stage, query, &attr, 1, range, &read);
waitOp(stage, enq, "read_attributes");
ovstage_read_group_t group{};
status = ovstage_fetch_read_next(stage, read, OVSTAGE_TIMEOUT_INFINITE, &group);
check(stage, status, "fetch_read_next");
if (group.data.tensor_count != 1 || !group.data.tensors[0].data)
{
std::fprintf(stderr, "unexpected read layout\n");
return EXIT_FAILURE;
}
const float* out = static_cast<const float*>(group.data.tensors[0].data);
std::printf("read back ordinal %llu: %.1f %.1f %.1f\n", (unsigned long long)group.ordinal, out[0], out[1], out[2]);
ovstage_release_group(stage, &group); // the tensor data is only valid until the group is released
Groups and Lifetime#
ovstage_read_attributesenqueues the read and returns a read handle.ovstage_fetch_read_nextyields the nextovstage_read_group_t— each group carries a prim group and itsovstage_data_ttensors — untilOVSTAGE_ERROR_END_OF_ITERATION.Release each group with
ovstage_release_group, then the read withovstage_release_read. In Python theReadhandle and its groups clean up through their normal object lifetime.
A group’s tensor data is a borrowed view into the latest committed snapshot. Copy it if you need it after the next commit (refer to DLPack Tensor Exchange).
Reading Built-in Metadata#
ovstage auto-maintains reserved metadata attributes — usd-path,
usd-schemas, usd-prim-type, usd-parent, usd-children — which
you read like any other column; they cross as uint64 token ids you resolve
through the path dictionary. The runtime-loop
example reads usd-prim-type to confirm a populate landed; refer to
Runtime Loop.
usd-path, usd-parent, and usd-children are derived on demand
rather than stored: a latest read synthesizes point-in-time group(s) at
range.end_ordinal (usd-children batches ragged rows like other array
reads, so iterate groups to the end as usual), while a range
(“since”/”between”) read reports them as never changed (zero groups) — they
keep no per-write change stream, so poll current values with latest reads.
The synthesized values reflect the latest committed structural state, like
query membership itself. Prims without a value (a root prim’s usd-parent,
a leaf prim’s usd-children) contribute no row, and writing, deleting, or
mapping any of the four derived names (including usd-active) is rejected
with NOT_SUPPORTED.
Note
usd-active is currently not supported: a live prim is always
active, so the attribute carries no information, and reads or filter
predicates naming it return NOT_SUPPORTED. The name remains in the
header contract for stability only and is subject to removal in a future
release.
Note
This build retains only the latest committed state, so reads return the latest sealed value. Do not design flows that read back older ordinals.
Where to Go Next#
Queries — build the query a read runs over.
Writing Attributes — produce the data you read back.
DLPack Tensor Exchange — interpreting the returned tensors and their residency.