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