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. The effective floor starts at 0. The floor is compared against the state a read would serve, never against the requested end ordinal: a snapshot read whose current recorded state sits above the floor, or an explicit range selecting an unsealed in-range change, fails with OVSTAGE_ERROR_WRITE_FLOOR_VIOLATION, including before the first floor advance. Lowering the requested end ordinal does not avoid this; advancing the floor does. An empty explicit range and a missing recorded attribute still return no groups, while derived built-ins are not floor-gated.

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).

Fixed-Size Result Shape#

A raw fixed-size read group is lane-canonical: its single tensor has ndim == 1, shape == (N,), and the full tuple width in dtype.lanes. N is the transported data-row count and may differ from data.count (the logical prim count): it can be smaller when data.index_map shares rows, or larger when a query touches only part of a transported bucket. A convenience write shape is not reconstructed; for example, a matrix written as (N, 4, 4) is read as (N,) with 16 lanes. Python DLPack export presents that as (N, 16).

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 payload. An explicit [start, end] range first selects keys changed in that interval. If a selected key also has a retained change after end, the read returns OVSTAGE_ERROR_OUT_OF_RANGE because the payload for that fixed range is no longer available. Widen/rebase the range or request current state; advancing the floor alone does not resolve this error.

Where to Go Next#