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_attributes enqueues the read and returns a read handle.

  • ovstage_fetch_read_next yields the next ovstage_read_group_t — each group carries a prim group and its ovstage_data_t tensors — until OVSTAGE_ERROR_END_OF_ITERATION.

  • Release each group with ovstage_release_group, then the read with ovstage_release_read. In Python the Read handle 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#