Writing Attributes#

write_attribute copies a column of data — one value per prim in a query — into the stage at an explicit ordinal. The write is asynchronous: it enqueues and returns an op_index, and the data becomes readable only after you advance the write floor to seal that ordinal.

The Write → Seal Sequence#

The minimal example builds a DLTensor over one float per prim, writes it at ordinal 1, and advances the write floor:

# 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

Key Parameters#

  • attribute — the column key, given as a token or string (refer to String Handling).

  • ordinal — must be above the current write floor, or the write is rejected with OVSTAGE_ERROR_WRITE_FLOOR_VIOLATION.

  • is_array — declares the attribute kind explicitly: false for a fixed-size attribute (one stacked tensor for all prims), true for a ragged/array attribute (a single packed tensor or one tensor per prim). Refer to DLPack Tensor Exchange.

  • tensors — the DLPack payload; can be CPU- or CUDA-resident.

Sealing with the Write Floor#

A write is not observable until ovstage_advance_write_floor seals its ordinal. Advancing the floor is monotonic in effect: a backwards advance clamps rather than erroring. Reads then target data at or below the floor.

Attribute Semantics#

Writes carry an ovstage_attribute_semantic_t (AttributeSemantic in Python; NONE by default). Geometric semantics stamp a role on the column — for example a 4×4 transform is written with the MATRIX semantic. Identity semantics (token / relationship / connection path ids) pin the column’s base type and require pre-interned id payloads. For a worked transform-write example over successive ordinals, refer to Runtime Loop.

Passing the Attribute as String or Token#

# Attribute arguments accept an interned token (int) or a plain str:
# a token skips the per-call dictionary lookup, a str is interned for
# you at call time.
stage.write_attribute(
    query, "temperature", ordinal=2, tensors=np.array([4.0, 5.0, 6.0], np.float32),
    is_array=False,
).wait()
stage.advance_write_floor(ordinal=2).wait()
// Attributes pass as ovx_string_or_token_t. We already hold an interned
// token, so set it (token != 0) and leave the string empty to skip a lookup.
ovx_string_or_token_t attrArg{ attr, {} };

Where to Go Next#