New content enters a store three ways: objects and geometry created directly in code, native files (ingestion), and files attached to an object version (artifacts). All of it publishes through the same checked transaction model: a write names the numbered versions it read, and a stale expectation conflicts without writing anything. Exports and project records are covered here too. Changing existing geometry is editing.
observed = {u: store.current_version(u) for u in (a, b)}
with store.write(expected_versions=observed) as tx:
tx.set_meta(a, "review_note", "checked")
tx.set_meta(edge, "confidence", 0.9)
print(tx.receipt["commit_sequence"], tx.receipt["objects"])store.write(...) accepts:
| Argument | Meaning |
|---|---|
expected_versions |
{uuid: version} for every object changed; None expects absence (creation — preselect the UUID) |
expected_relationships |
Same, for relationships |
read_dependencies |
Versions of records read but not changed; checked, never versioned |
expected_commit_sequence |
Guards a query's complete membership/absence; any intervening commit conflicts |
request_uuid, author |
Retry identity and attribution (see typed requests) |
Every object a transaction changes — including both endpoints of any
relationship it touches — must be expected. One commit publishes at most one
revision per changed object, shared endpoints included. After the block,
tx.receipt holds the commit sequence and the new version of every expected
and changed record (None for deleted).
C++ uses store.write(TransactionContext{...}). Through a session, reads go
through readStore and writes are typed requests built with newRequest and
applied with submit — see sessions.
Plain store.write() with no expectations serializes read/modify/write under
the writer lock, but cannot protect a read made before it. Standalone Python
mutations (store.set_meta, store.put_geometry, ...) each commit their own
transaction.
from vc_object_store import PointCollection, Track
uid = store.upsert_object({"kind": "point_collection", "name": "seeds",
"tags": ["review"]})
revision = store.put_geometry(uid, PointCollection([42, 7], [[1, 2, 3], [4, 5, 6]]))
track = store.upsert_object({"kind": "track",
"track": {"external_key": "t0", "family": "vertical"}})
store.put_geometry(track, Track([[1, 2, 3], [4, 5, 6]]))upsert_object(record)creates (or replaces the record of) an object and returns its UUID. Pass"uuid"to preselect identity. Insidestore.write()usetx.upsert_object. Per-kind detail dicts (fiber,surface,track) carry their summaries (counts, length, family, bbox).put_geometry(uuid, geometry, expected_version=None)publishes the first geometry revision. Python accepts an owned geometry value (PointCollection,Fiber,Track,Surface,Umbilicus— see typed geometry) or an array dict ({"kind": ..., "attributes": {...}, "arrays": [{"name", "dtype", "shape", "data"}]}). It returns the new geometry revision UUID.- Creation is one-way: sequence geometry (fibers, point collections, tracks)
is created exactly once with
put_geometry. Later changes are edits; once structural management has begun, fullput_geometryreplacement is rejected.store.affected_geometry_edges(uuid, geometry)previews the relationship decisions a full replacement with that geometry would need. - Initial per-point annotations from source metadata are consumed into the
first revision. Fiber controls and point collections have stable point IDs
from creation; fiber JSON transfer preserves
control_point_idsand allocates IDs when a new input omits them. - A surface placeholder bound for later loading is an ordinary record with
storage_kind/storage_ref— see ingest_registered_geometry.
Creating a surface from a grid, in a volume's voxels, with provenance:
from vc_object_store import surfaces
uuid = surfaces.create_surface(
"objects.vcobj", xyz, # (rows, columns, 3) in `volume` voxels
name="fit 42", scale=1.0, volume="20260411134726",
tool="lasagna", parameters={...}, sources=[seed_uuid],
channels={"ink": ink_array}, tags=["fit"],
)surfaces.create_surface / surfaces.read_surface take a store location
(a file path or a vc_object_server endpoint), so the same call works on a
served store; locations.attached(location) gives a short-lived session
attachment for other work. C++ uses createSurfaceOperations, which returns
the operations creating the record and its geometry in one checked request.
Copying an object between stores is a session operation:
session.copy_object(from_attachment, to_attachment, uuid) copies the
current record and geometry under a new UUID (meta.copied_from names the
source store, object, numbered version, and geometry revision; point
annotations kept). Relationships, history, and approval stay behind.
C++ and hosted writes share one serializable request form (objects/Transactions.hpp):
vc::objects::TransactionRequest r;
r.requestUuid = vc::objects::util::newUuid();
r.storeUuid = status.storeUuid; r.generation = status.generation; r.author = "reviewer";
r.expectedObjects = {{objectUuid, 3}};
r.operations = {vc::objects::SetMetaOp{objectUuid, "review_note", R"("checked")"}};
const auto receipt = store.submit(r); // receipt.objects[objectUuid] == 4- A request carries a client-minted
requestUuid, expected store and generation, author, expected object/relationship versions, read dependencies, an optional commit-sequence guard, and operations:CreateObjectOp,UpdateObjectOp,SetMetaOp,RemoveMetaOp,SetApprovalOp,RetireObjectOp,RestoreObjectOp,DeleteObjectOp,AddEdgeOp,UpdateEdgeOp,RemoveEdgeOp,RetainRelationshipOp,KeepRecordedRelationshipOp,PutGeometryOp,EditGeometryOp,EditGeometryRectangleOp,SetSurfaceChannelsOp,UpdateGeometryArraysOp,IngestGeometryOp,RecordCatalogTransformsOp,CheckVolumeMappingOp. - Retries are safe. An identical retry returns the saved receipt
(
replayed) before any version check, so a lost reply never conflicts with its own changes. The same request UUID with different contents fails. Recover a lost outcome withreceipt(request_uuid)— never resubmit under a new UUID. - Stale preconditions throw
TransactionConflictand write nothing. Geometry operations name each relationship decision, or setkeepAffectedRecordedto keep every undecided affected relationship as recorded (the store decides which are affected from the checked state); either way the request must expect both endpoints. - objects/TransactionJson.hpp
is the canonical JSON form: binary payloads named by SHA-256 and sent
separately. Python
store.submit(request, payloads)takes that JSON as a dict or text with payloads keyed by SHA-256. Missing payloads raise/return their hashes: stage them and resubmit the identical request. - Helpers fill in the expectations an edit depends on:
expectRelationshipEndpoints(far ends of an edited object's relationships),editCurrentRequest/ sessioneditCurrent(edit existing objects at current versions, relationships kept as recorded),bindCoordinatesOperation,volumeTransformRecord,volumeTransformChoiceRecord,attachmentEdge,createSurfaceOperations. Code holding an object keeps aPinnedObject(pinOf(attachment, revision, activeGeneration)); itsgenerationnames the published version for historical reads andwriteGenerationthe active history write requests expect — they can differ after a history reset, in which case reopen before editing.
Native export writes one object's revision back to its file format:
ref = store.export_geometry(uuid, "/data/out") # current
ref = store.export_geometry(uuid, "/data/out", version=3) # recorded version
ref = store.export_geometry(uuid, "/data/out", overwrite=True)
# CLI: vc_object_export --db DB --uuid UUID --output PATH [--version N] [--generation G] [--overwrite] [--valid-only]C++ exportGeometry also has an overload taking the caller's ReadTxn (for
example inside ObjectStoreSession::read), so read-only stores export too and
the caller can report the same snapshot. Publication is atomic: Linux
renameat2 or macOS renamex_np.
Export selects an immutable object revision — metadata and relationship
snapshot included — and stages output before publication; the return value is
the exported {store, generation, object, version} reference. Without
overwrite, an existing destination fails. Surfaces export each channel as a
sidecar <name>.tif, valid as a single-page mask.tif (255/0);
--valid-only instead writes invalid vertices as missing with no mask. Other
kinds reject auxiliary arrays. Every native exporter carries the authoritative
meta.coordinates (unknown values included) plus any coordinate_binding,
so export → import preserves the frame with no out-of-band context. Fibers
export as version-4 JSON, branches addressed by target_uuid with reciprocal
approval state and edge UUIDs (fiberNetworkJson in C++ exports whole linked
components pinned at numbered versions; fiberVersionJson is the
geometry-and-annotations codec).
export_json(object_kind=None, fields=[...]) exports metadata only
(records, with filters and field projection) and import_json reads it back.
It is not a geometry backup.
Artifacts are files that belong to an object but are not geometry — fitted
models, exports, logs. Their bytes are immutable and content-addressed beside
the database, at <store>.artifacts/sha256/<2 hex>/<sha256>; an object
version names them through a manifest in meta.artifacts: a sorted array of
{name, sha256, length}, names relative and /-separated. A manifest is
versioned and in the change feed like any metadata, and bytes are never
deleted while historical versions name them — back up the directory with the
database.
const auto hash = store.putArtifact(file); // durable (synced) before it returns
store.submit(request with SetMetaOp{object, kArtifactsMetaKey,
artifactManifestJson({{"fit/model.pt", hash, size}})});import vc_object_store as vos
digest = store.put_artifact(path) # -> sha256 hex
store.artifact_length(digest); store.read_artifact(digest)
manifest = vos.artifact_manifest_json(
[{"name": "fit/model.pt", "sha256": digest, "length": size}]) # for meta["artifacts"]The shared commit path refuses a changed manifest that is malformed or names
bytes the store does not hold, whichever API wrote it. Through a session the
same works on any attachment: artifacts(attachment, object, version),
publish_artifacts(attachment, object, expected_version, {name: path}, author)
(bytes stored durably, then named in one checked request),
materialize_artifacts (write a version's files under a local directory,
each atomic and verified), and artifact_file (one artifact as a cached local
file). CLI tools publish through ToolObjects::publishArtifacts, expecting
the version they read. Hosted clients get putArtifact / fetchArtifact
(verified, atomic). The older mutable per-object artifact directories are no
longer read or written, and their contents are not migrated.
A store is also a project (objects/Project.hpp):
its resource records reference the volumes, normal grids, and Lasagna datasets
it uses, and each author keeps a project_view record of their own.
import vc_object_store as vos
vos.create_project("project.vcobj", "scroll", {"open_data": {"sample": "PHerc0139"}},
author="reviewer") # creates the store with its project record
with ObjectStore.open("project.vcobj", author="reviewer") as store:
uuid = store.add_project_resource("volume", "/data/ct.zarr", tags=["main"])
store.project_resources("volume", viewer="reviewer") # shared + that author's own
store.project_record() # None on a plain storeResource kinds are volume, normal_grid, and lasagna_dataset; a resource
is shared, or one owner's alone (project_resources(kind, viewer=...)).
vos.project_resource_uuid(kind, location, owner="") is the deterministic
identity add_project_resource returns.
Documents attached to objects (point-collection documents such as growth
corrections) go through documents.attach_document(location, ...) /
attached_document / point_collections — one checked request each, on a
file or endpoint alike; their edges are attachment edges, not graph links
(see graph edges).
copyProject copies the artifact directory with the database.