Coherent reads and scoped edits¶
Use a read session when a decision depends on several reads. Its objects, pages, and decision base come from one retained database view. Other writers can commit while that view stays unchanged. Close it before waiting for a UI decision or doing expensive media work; copied values and the detached base remain usable. Resolution and verification use that view’s stored knowledge while inspecting the current filesystem. A read session does not freeze paths or media bytes.
An edit started from the session retains its base automatically. A detached base includes production identity and the observed revision, including the initial empty journal. The store rejects another production’s base and invalid revision/sequence pairs. The base retains no database connection or lock.
static int lookup(const pp_read_session_t *view, const pp_asset_id_t *asset, const char *media) {
pp_media_root_set_t *roots = NULL;
pp_external_identifier_set_t *identifiers = NULL;
pp_object_ref_set_t *objects = NULL;
pp_known_media_set_t *matches = NULL;
pp_fingerprint_t *fingerprint = NULL;
pp_metadata_set_t *metadata = NULL;
pp_resolution_set_t *resolutions = NULL;
pp_error_t *error = NULL;
char *uri = NULL;
const char *algorithm;
const uint8_t *value;
uint16_t version;
uint64_t length;
pp_object_ref_t target;
int result = 1;
#define CHECK(call) do { if ((call) != PP_OK) goto cleanup; } while (0)
CHECK(pp_object_ref_from_asset(*asset, &target, &error));
CHECK(pp_read_session_media_roots(view, &roots, &error));
if (pp_media_root_set_count(roots) != 0) goto cleanup;
CHECK(pp_read_session_external_identifiers(view, &target, &identifiers, &error));
if (pp_external_identifier_set_count(identifiers) != 1) goto cleanup;
CHECK(pp_read_session_find_by_external_identifier(view, "https://example.com/id", "camera", NULL, &objects, &error));
if (pp_object_ref_set_count(objects) != 1) goto cleanup;
CHECK(pp_read_session_metadata(view, &target, &metadata, &error));
if (pp_metadata_set_count(metadata) != 1) goto cleanup;
pp_metadata_set_release(metadata); metadata = NULL;
CHECK(pp_read_session_find_metadata(view, "https://example.com/editorial", "title", &metadata, &error));
if (pp_metadata_set_count(metadata) != 1) goto cleanup;
pp_metadata_set_release(metadata); metadata = NULL;
CHECK(pp_read_session_query_metadata(view, "https://example.com/editorial", "title", NULL, 10, NULL, &metadata, &error));
if (pp_metadata_set_count(metadata) != 1) goto cleanup;
CHECK(pp_file_path_to_locator(media, &uri, &error));
CHECK(pp_read_session_find_known_media_by_locator(view, uri, NULL, 10, NULL, &matches, &error));
if (pp_known_media_set_count(matches) != 1) goto cleanup;
pp_asset_id_t matched_asset;
pp_representation_id_t representation;
pp_resource_id_t resource;
pp_content_verification_t verification;
CHECK(pp_known_media_set_get(matches, 0, &matched_asset, &representation, &resource, &error));
CHECK(pp_read_session_verify_resource(view, resource, media, NULL, &verification, &error));
if (verification != PP_CONTENT_MATCHES) goto cleanup;
CHECK(pp_read_session_resolve_assets(view, asset, 1, NULL, &resolutions, &error));
if (pp_resolution_set_representation_count(resolutions) != 1) goto cleanup;
pp_known_media_set_release(matches); matches = NULL;
CHECK(pp_fingerprint_file(media, &fingerprint, &error));
CHECK(pp_fingerprint_get(fingerprint, &algorithm, &version, &value, &length, &error));
CHECK(pp_read_session_find_known_media_by_fingerprint(view, algorithm, version, value, length, 10, NULL, &matches, &error));
if (pp_known_media_set_count(matches) != 1) goto cleanup;
result = 0;
cleanup:
pp_error_release(error);
pp_media_root_set_release(roots);
pp_external_identifier_set_release(identifiers);
pp_object_ref_set_release(objects);
pp_known_media_set_release(matches);
pp_fingerprint_release(fingerprint);
pp_metadata_set_release(metadata);
pp_resolution_set_release(resolutions);
pp_string_release(uri);
return result;
#undef CHECK
}
static int exercise(const char *path, const char *media) {
pp_production_t *production = NULL;
pp_read_session_t *empty = NULL, *view = NULL;
pp_transaction_t *edit = NULL;
pp_metadata_input_t *title = NULL;
pp_asset_set_t *assets = NULL;
pp_job_set_t *jobs = NULL;
pp_regeneration_plan_set_t *plans = NULL;
pp_revision_set_t *journal = NULL;
pp_revision_event_set_t *events = NULL, *next_events = NULL;
pp_activity_set_t *activities = NULL;
pp_dependency_set_t *dependencies = NULL;
pp_dependency_query_set_t *dependency_page = NULL;
pp_artifact_evaluation_t *evaluation = NULL;
pp_artifact_reproducibility_t *report = NULL;
pp_representation_set_t *representations = NULL;
pp_object_query_set_t *resource_page = NULL, *provenance_page = NULL;
pp_locator_query_set_t *locator_page = NULL;
pp_error_t *error = NULL;
pp_asset_id_t asset;
pp_representation_id_t representation;
pp_decision_base_t base, parsed;
char *token = NULL, *revision_text = NULL, *transaction_text = NULL;
pp_commit_receipt_t receipt;
int result = 1;
#define CHECK(call) do { if ((call) != PP_OK) goto cleanup; } while (0)
CHECK(pp_production_create(path, "Read views", &production, &error));
CHECK(pp_production_read_session(production, &empty, &error));
CHECK(pp_read_session_decision_base(empty, &base, &error));
CHECK(pp_decision_base_format(&base, &token, &error));
CHECK(pp_decision_base_parse(token, &parsed, &error));
if (parsed.has_revision || parsed.revision_sequence ||
memcmp(parsed.production_id.bytes, base.production_id.bytes, 16)) goto cleanup;
pp_string_release(token); token = NULL;
if (base.has_revision || base.revision_sequence != 0) goto cleanup;
CHECK(pp_read_session_begin_edit(empty, NULL, &edit, &error));
CHECK(pp_read_session_plan_regeneration(empty, NULL, 0, &plans, &error));
if (pp_regeneration_plan_set_count(plans)) goto cleanup;
CHECK(pp_transaction_import_file(edit, media, NULL, &asset, NULL, &error));
pp_object_ref_t target;
CHECK(pp_object_ref_from_asset(asset, &target, &error));
CHECK(pp_transaction_add_external_identifier(edit, &target, "https://example.com/id", "camera", NULL, &error));
CHECK(pp_metadata_input_create_string("Camera", NULL, &title, &error));
CHECK(pp_transaction_add_metadata_value(edit, &target, "https://example.com/editorial", "title", title, &error));
CHECK(pp_transaction_commit(edit, &receipt, &error));
if (receipt.outcome != PP_COMMIT_REVISION_CREATED || receipt.revision_sequence != 1)
goto cleanup;
pp_transaction_release(edit); edit = NULL;
CHECK(pp_read_session_assets_page(empty, 10, NULL, &assets, &error));
if (pp_asset_set_count(assets) != 0) goto cleanup;
pp_asset_set_release(assets); assets = NULL;
CHECK(pp_read_session_latest_revision(empty, &journal, &error));
if (pp_revision_set_count(journal)) goto cleanup;
pp_revision_set_release(journal); journal = NULL;
CHECK(pp_read_session_changes_since(empty, 0, 10, &journal, &error));
if (pp_revision_set_count(journal)) goto cleanup;
pp_revision_set_release(journal); journal = NULL;
pp_read_session_release(empty); empty = NULL;
CHECK(pp_production_read_session(production, &view, &error));
CHECK(pp_read_session_decision_base(view, &base, &error));
pp_revision_id_t revision_id;
pp_transaction_id_t transaction_id;
/* Formatting and parsing retain the recorded revision identity. */
revision_id = base.revision_id;
CHECK(pp_revision_id_format(revision_id, &revision_text, &error));
CHECK(pp_revision_id_parse(revision_text, &revision_id, &error));
if (memcmp(revision_id.bytes, base.revision_id.bytes, 16)) goto cleanup;
pp_string_release(revision_text); revision_text = NULL;
CHECK(pp_transaction_id_parse("00000000-0000-0000-0000-000000000001", &transaction_id, &error));
CHECK(pp_transaction_id_format(transaction_id, &transaction_text, &error));
if (strcmp(transaction_text, "00000000-0000-0000-0000-000000000001")) goto cleanup;
pp_string_release(transaction_text); transaction_text = NULL;
CHECK(pp_read_session_revision_events_page(view, base.revision_id, 1, NULL, &events, &error));
const char *event_cursor = pp_revision_event_set_next_cursor(events);
if (pp_revision_event_set_count(events) != 1 || !event_cursor) goto cleanup;
CHECK(pp_read_session_revision_events_page(view, base.revision_id, 1000, event_cursor, &next_events, &error));
pp_revision_event_t first_event, next_event;
CHECK(pp_revision_event_set_get(events, 0, &first_event, &error));
CHECK(pp_revision_event_set_get(next_events, 0, &next_event, &error));
if (next_event.position <= first_event.position || pp_revision_event_set_next_cursor(next_events)) goto cleanup;
pp_revision_event_set_release(next_events); next_events = NULL;
CHECK(pp_production_revision_events_page(production, base.revision_id, 1000, NULL, &next_events, &error));
if (pp_revision_event_set_count(next_events) <= 1) goto cleanup;
CHECK(pp_decision_base_format(&base, &token, &error));
CHECK(pp_decision_base_parse(token, &parsed, &error));
if (!parsed.has_revision || parsed.revision_sequence != base.revision_sequence ||
memcmp(parsed.revision_id.bytes, base.revision_id.bytes, 16)) goto cleanup;
pp_string_release(token); token = NULL;
CHECK(pp_read_session_unresolved_media(view, 10, NULL, &resource_page, &error));
if (pp_object_query_set_count(resource_page)) goto cleanup;
pp_object_query_set_release(resource_page); resource_page = NULL;
CHECK(pp_read_session_objects_changed_since(view, 0, 10, NULL, &resource_page, &error));
if (!pp_object_query_set_count(resource_page)) goto cleanup;
pp_object_query_set_release(resource_page); resource_page = NULL;
if (pp_read_session_representations_under_media_root(view, "missing", 10, NULL,
&representations, &error) != PP_ERROR_NOT_FOUND || representations) goto cleanup;
pp_error_release(error); error = NULL;
const pp_revision_event_kind_t filter = PP_REVISION_ASSET_IMPORTED;
uint64_t through;
CHECK(pp_read_session_changes_since_filtered(view, 0, &filter, 1, 10, &journal, &through, &error));
if (pp_revision_set_count(journal) != 1 || through != base.revision_sequence) goto cleanup;
pp_revision_set_release(journal); journal = NULL;
CHECK(pp_read_session_latest_revision(view, &journal, &error));
if (pp_revision_set_count(journal) != 1) goto cleanup;
pp_revision_set_release(journal); journal = NULL;
CHECK(pp_read_session_asset(view, asset, &assets, &error));
if (pp_asset_set_count(assets) != 1) goto cleanup;
CHECK(pp_read_session_representations_page(view, asset, 10, NULL, &representations, &error));
uint32_t kind, structure;
pp_asset_id_t owner;
uint64_t members, resources, fingerprints;
CHECK(pp_representation_set_get(representations, 0, &representation, &owner,
&kind, &structure, &members, &resources, &fingerprints, &error));
pp_representation_set_release(representations); representations = NULL;
CHECK(pp_read_session_representation(view, representation, &representations, &error));
CHECK(pp_read_session_resources_page(view, representation, 10, NULL, &resource_page, &error));
if (pp_object_query_set_count(resource_page) != 1) goto cleanup;
pp_object_ref_t resource;
uint32_t depth;
CHECK(pp_object_query_set_get(resource_page, 0, &resource, &depth, &error));
if (resource.kind != PP_OBJECT_RESOURCE) goto cleanup;
pp_resource_id_t resource_id;
CHECK(pp_object_ref_get_resource(&resource, &resource_id, &error));
CHECK(pp_read_session_locators_page(view, resource_id, 10, NULL, &locator_page, &error));
if (pp_locator_query_set_count(locator_page) != 1) goto cleanup;
pp_representation_set_release(representations); representations = NULL;
CHECK(pp_read_session_representations_using_resource(view, resource_id, 10, NULL, &representations, &error));
if (pp_representation_set_count(representations) != 1) goto cleanup;
if (lookup(view, &asset, media)) goto cleanup;
CHECK(pp_read_session_dependency_set(view, representation, &dependencies, &error));
uint8_t present;
pp_representation_id_t dependency_source;
uint64_t dependency_revision, dependency_count;
pp_dependency_set_status_t dependency_status;
CHECK(pp_dependency_set_get(dependencies, &present, &dependency_source,
&dependency_revision, &dependency_status, &dependency_count, &error));
if (present || dependency_count) goto cleanup;
CHECK(pp_read_session_dependencies(view, representation, 64, 1000, 10, NULL, &dependency_page, &error));
if (pp_dependency_query_set_count(dependency_page) != 0) goto cleanup;
pp_dependency_query_set_release(dependency_page); dependency_page = NULL;
CHECK(pp_read_session_dependents(view, &target, 64, 1000, 10, NULL, &dependency_page, &error));
if (pp_dependency_query_set_count(dependency_page) != 0) goto cleanup;
CHECK(pp_read_session_outputs_by_activity_kind(view, "example:render", 10, NULL, &provenance_page, &error));
if (pp_object_query_set_count(provenance_page)) goto cleanup;
pp_object_query_set_release(provenance_page); provenance_page = NULL;
CHECK(pp_read_session_outputs_by_tool(view, "Example", NULL, NULL, 10, NULL, &provenance_page, &error));
if (pp_object_query_set_count(provenance_page)) goto cleanup;
pp_object_query_set_release(provenance_page); provenance_page = NULL;
CHECK(pp_read_session_provenance_ancestors_page(view, representation, 64, 1000, 10, NULL, &provenance_page, &error));
if (pp_object_query_set_count(provenance_page)) goto cleanup;
pp_object_query_set_release(provenance_page); provenance_page = NULL;
CHECK(pp_read_session_provenance_descendants_page(view, representation, 64, 1000, 10, NULL, &provenance_page, &error));
if (pp_object_query_set_count(provenance_page)) goto cleanup;
pp_object_query_set_release(provenance_page); provenance_page = NULL;
CHECK(pp_read_session_stale_artifacts(view, NULL, 64, 1000, 10, NULL, &provenance_page, &error));
if (pp_object_query_set_count(provenance_page)) goto cleanup;
pp_object_query_set_release(provenance_page); provenance_page = NULL;
CHECK(pp_read_session_activities_producing_page(view, representation, 10, NULL, &activities, &error));
if (pp_activity_set_count(activities)) goto cleanup;
pp_activity_set_release(activities); activities = NULL;
CHECK(pp_read_session_activities_consuming_page(view, representation, 10, NULL, &activities, &error));
if (pp_activity_set_count(activities)) goto cleanup;
CHECK(pp_read_session_evaluate_artifact(view, representation, 64, 1000, &evaluation, &error));
pp_representation_id_t evaluated;
uint32_t state, visited;
uint8_t truncated;
uint64_t reasons;
CHECK(pp_artifact_evaluation_get(evaluation, &evaluated, &state, &visited, &truncated, &reasons, &error));
if (memcmp(evaluated.bytes, representation.bytes, 16) || state != PP_ARTIFACT_INDETERMINATE || reasons == 0) goto cleanup;
CHECK(pp_read_session_artifact_reproducibility(view, representation, &report, &error));
uint8_t reproducible, has_activity;
pp_activity_id_t activity;
const char *activity_kind;
uint64_t issues;
CHECK(pp_artifact_reproducibility_get(report, &evaluated, &reproducible, &has_activity, &activity, &activity_kind, &issues, &error));
if (reproducible || has_activity || issues == 0) goto cleanup;
pp_read_session_release(view); view = NULL;
/* Copied sets and a detached base survive closing the pinned view. */
CHECK(pp_revision_event_set_get(events, 0, &first_event, &error));
CHECK(pp_production_begin_edit(production, &base, NULL, &edit, &error));
CHECK(pp_transaction_commit(edit, &receipt, &error));
if (receipt.outcome != PP_COMMIT_NO_CHANGE || pp_asset_set_count(assets) != 1 ||
pp_locator_query_set_count(locator_page) != 1 ||
pp_object_query_set_count(resource_page) != 1)
goto cleanup;
pp_transaction_release(edit); edit = NULL;
CHECK(pp_production_read_session(production, &view, &error));
CHECK(pp_read_session_begin_edit(view, NULL, &edit, &error));
pp_job_id_t job;
CHECK(pp_transaction_request_job(edit, "example:proxy", &representation, 1,
asset, PP_REPRESENTATION_PROXY, NULL, &job, &error));
CHECK(pp_transaction_commit(edit, &receipt, &error));
CHECK(pp_read_session_jobs(view, PP_JOB_REQUESTED, "example:proxy", 10, NULL, &jobs, &error));
if (pp_job_set_count(jobs) != 0) goto cleanup;
pp_job_set_release(jobs); jobs = NULL;
if (pp_read_session_job(view, job, &jobs, &error) != PP_ERROR_NOT_FOUND || jobs != NULL)
goto cleanup;
pp_error_release(error); error = NULL;
pp_read_session_release(view); view = NULL;
CHECK(pp_production_read_session(production, &view, &error));
CHECK(pp_read_session_job(view, job, &jobs, &error));
if (pp_job_set_count(jobs) != 1) goto cleanup;
pp_job_set_release(jobs); jobs = NULL;
CHECK(pp_read_session_jobs(view, PP_JOB_REQUESTED, "example:proxy", 10, NULL, &jobs, &error));
if (pp_job_set_count(jobs) != 1) goto cleanup;
result = 0;
cleanup:
if (result) fprintf(stderr, "%s\n", error ? pp_error_message(error) : "read-view assertion");
pp_error_release(error);
pp_string_release(token);
pp_string_release(revision_text);
pp_string_release(transaction_text);
pp_asset_set_release(assets);
pp_job_set_release(jobs);
pp_regeneration_plan_set_release(plans);
pp_activity_set_release(activities);
pp_revision_set_release(journal);
pp_revision_event_set_release(events);
pp_revision_event_set_release(next_events);
pp_dependency_set_release(dependencies);
pp_dependency_query_set_release(dependency_page);
pp_artifact_evaluation_release(evaluation);
pp_artifact_reproducibility_release(report);
pp_representation_set_release(representations);
pp_object_query_set_release(resource_page);
pp_object_query_set_release(provenance_page);
pp_locator_query_set_release(locator_page);
pp_metadata_input_release(title);
pp_transaction_release(edit);
pp_read_session_release(empty);
pp_read_session_release(view);
pp_production_release(production);
return result;
#undef CHECK
}
static void exercise(const std::string &path, const std::string &media) {
auto production = postproject::Production::create(path).value();
auto empty = production.readSession().value();
const auto revision_identity = postproject::RevisionId::fromString(
"00000000-0000-0000-0000-000000000001").value();
const auto transaction_identity = postproject::TransactionId::fromString(
revision_identity.toString().value()).value();
if (revision_identity.bytes() != transaction_identity.asUuid().bytes())
throw std::runtime_error("explicit identity interchange");
if (!empty.planRegeneration({}).value().empty())
throw std::runtime_error("empty planning request");
auto edit = empty.edit().value();
const auto asset = edit.importMedia(media).value().asset_id;
edit.addExternalIdentifier(postproject::ObjectRef::asset(asset),
{"https://example.com/id", "camera", std::nullopt}).value();
edit.addMetadataValue(postproject::ObjectRef::asset(asset),
"https://example.com/editorial", "title",
postproject::MetadataValue::plainString("Camera")).value();
const auto receipt = edit.commit().value();
if (!receipt.revision || receipt.revision->sequence != 1 ||
!empty.assets(10).value().items.empty())
throw std::runtime_error("coherent empty view or receipt");
if (empty.latestRevision().value() || !empty.changesSince(0, 10).value().empty())
throw std::runtime_error("coherent empty journal");
auto view = production.readSession().value();
const auto latest = view.latestRevision().value();
const auto filtered = view.changesSinceFiltered(0, {postproject::RevisionEventKind::asset_imported}, 10).value();
if (filtered.revisions.size() != 1 || filtered.through_sequence != receipt.revision->sequence)
throw std::runtime_error("coherent filtered journal");
if (!latest || latest->id != receipt.revision->id || view.changesSince(0, 10).value().size() != 1)
throw std::runtime_error("coherent journal head");
const auto events = view.revisionEvents(latest->id, 1).value();
if (events.items.size() != 1 || !events.next_cursor)
throw std::runtime_error("bounded revision events");
const auto next_events = view.revisionEvents(latest->id, 1000, events.next_cursor).value();
const auto live_events = production.revisionEvents(latest->id, 1000).value();
if (next_events.items.empty() || next_events.next_cursor ||
next_events.items[0].position <= events.items[0].position ||
live_events.items.size() != next_events.items.size() + 1)
throw std::runtime_error("ordered revision event continuation");
if (!view.unresolvedMedia(10).value().items.empty() ||
view.objectsChangedSince(0, 10).value().items.empty() ||
view.representationsUnderMediaRoot("missing", 10).error().code() != postproject::ErrorCode::not_found)
throw std::runtime_error("coherent object filters");
const auto copied = view.asset(asset).value();
const auto representations = view.representations(asset, 10).value();
if (representations.items.size() != 1)
throw std::runtime_error("representation page");
const auto representation = view.representation(representations.items[0].id).value();
if (view.evaluateArtifact(representation.id).value().state !=
postproject::ArtifactKnowledgeState::indeterminate ||
view.artifactReproducibility(representation.id).value().reproducible)
throw std::runtime_error("coherent artifact reports");
if (view.dependencySet(representation.id).value().has_value() ||
!view.dependencies(representation.id, 64, 1000, 10).value().items.empty() ||
!view.dependents(postproject::ObjectRef::asset(asset), 64, 1000, 10).value().items.empty())
throw std::runtime_error("coherent dependency queries");
if (!view.ancestors(representation.id, 64, 1000, 10).value().items.empty() ||
!view.descendants(representation.id, 64, 1000, 10).value().items.empty() ||
!view.staleArtifacts(64, 1000, 10).value().items.empty())
throw std::runtime_error("coherent provenance and staleness");
if (!view.outputsByActivityKind("example:render", 10).value().items.empty() ||
!view.outputsByTool({"Example", std::nullopt, std::nullopt}, 10).value().items.empty())
throw std::runtime_error("coherent activity output filters");
if (!view.activitiesProducing(representation.id, 10).value().items.empty() ||
!view.activitiesConsuming(representation.id, 10).value().items.empty())
throw std::runtime_error("coherent activity pages");
const auto resources = view.resources(representation.id, 10).value();
if (resources.items.size() != 1)
throw std::runtime_error("resource page");
const auto options = postproject::ResolutionOptions::create().value();
if (view.verifyResource(resources.items[0], media).value() !=
postproject::ContentVerification::matches ||
view.resolveAsset(asset).value().size() != 1 ||
view.resolveAsset(asset, options).value().size() != 1 ||
view.resolveAssets({asset}).value().size() != 1 ||
view.resolveAssets({asset}, options).value().size() != 1)
throw std::runtime_error("coherent resolution and verification");
const auto locators = view.locators(resources.items[0], 10).value();
const auto owners = view.representationsUsingResource(resources.items[0], 10).value();
if (locators.items.size() != 1 || owners.items.size() != 1 ||
owners.items[0].id != representation.id)
throw std::runtime_error("locator and resource ownership pages");
const auto base = view.decisionBase().value();
const auto token = base.toToken().value();
const auto parsed = postproject::DecisionBase::fromToken(token).value();
if (parsed.production_id != base.production_id ||
parsed.revision->id != base.revision->id ||
postproject::DecisionBase::fromToken(token + ":extra").has_value())
throw std::runtime_error("scoped decision token");
const postproject::ObjectRef target = postproject::ObjectRef::asset(asset);
const auto title = view.queryMetadata("https://example.com/editorial", "title", 10).value();
if (title.items.size() != 1 ||
title.items[0].value.getIf<postproject::MetadataString>()->value != "Camera" ||
view.queryMetadata("https://example.com/editorial", "title",
postproject::MetadataValue::plainString("Other"), 10).value().items.size() != 0)
throw std::runtime_error("coherent metadata queries");
if (!view.mediaRoots().value().empty() ||
view.externalIdentifiers(target).value()[0].value != "camera" ||
view.findByExternalIdentifier("https://example.com/id", "camera").value().size() != 1 ||
view.findKnownMedia(std::filesystem::path(media), 10).value().items[0].asset_id != asset ||
view.findKnownMedia(postproject::fingerprintFile(media).value(), 10).value().items[0].asset_id != asset)
throw std::runtime_error("coherent lookup projections");
auto later = production.edit(base).value();
const auto noop = later.commit().value();
if (noop.revision || copied.id != asset || representation.asset_id != asset)
throw std::runtime_error("no-change receipt or copied identity");
auto before_job = production.readSession().value();
auto request = before_job.edit().value();
const auto job = request.requestJob({"example:proxy", {representation.id}, asset,
postproject::RepresentationKind::proxy, std::nullopt}).value();
request.commit().value();
if (!before_job.jobs(10).value().items.empty() ||
before_job.job(job).error().code() != postproject::ErrorCode::not_found)
throw std::runtime_error("coherent job view");
auto after_job = production.readSession().value();
if (after_job.job(job).value().id != job ||
after_job.jobs(10, std::nullopt, postproject::JobState::requested,
"example:proxy").value().items.size() != 1)
throw std::runtime_error("coherent job point and filtered page");
}
def exercise(path: Path, media: Path) -> None:
with Production.create(path) as production:
with production.read_session() as empty:
assert empty.plan_regeneration(()) == ()
with empty.edit() as edit:
asset = edit.import_media(media).asset_id
edit.add_external_identifier(
AssetRef(asset),
ExternalIdentifier("https://example.com/id", "camera"),
)
edit.add_metadata(
AssetRef(asset),
MetadataProperty("https://example.com/editorial", "title"),
("Camera"),
)
receipt = edit.commit()
assert receipt.revision is not None and receipt.revision.sequence == 1
assert empty.assets_page(limit=10).items == ()
assert empty.latest_revision is None
assert empty.changes_since(0, 10) == ()
with production.read_session() as view:
latest = view.latest_revision
assert latest is not None and latest.id == receipt.revision.id
assert view.changes_since(0, 10) == (latest,)
assert tuple(view.revisions(page_size=1)) == (latest,)
filtered = view.changes_since_filtered(0, (AssetImportedEvent,), 10)
assert filtered.revisions == (latest,)
assert filtered.through_sequence == latest.sequence
events = view.revision_events_page(latest.id, limit=1)
assert len(events.items) == 1 and events.next_cursor is not None
next_events = view.revision_events_page(
latest.id, limit=1000, cursor=events.next_cursor
)
assert next_events.next_cursor is None
assert next_events.items[0].position > events.items[0].position
assert production.revision_events_page(latest.id, limit=1000).items == (
events.items + next_events.items
)
assert view.unresolved_media(limit=10).items == ()
assert view.objects_changed_since(0, limit=10).items
try:
view.representations_under_media_root("missing", limit=10)
except NotFoundError:
pass
else:
raise AssertionError("unknown logical root should be rejected")
copied = view.asset(asset)
page = view.representations_page(asset, limit=10)
assert len(page.items) == 1
representation = view.representation(page.items[0].id)
title = MetadataProperty("https://example.com/editorial", "title")
metadata = view.metadata(AssetRef(asset))
assert metadata[0].value == ("Camera")
assert view.metadata_by_property(title) == metadata
assert view.query_metadata(title, limit=10).items == metadata
assert (
view.evaluate_artifact(representation.id).state
== ArtifactKnowledgeState.INDETERMINATE
)
assert not view.artifact_reproducibility(representation.id).reproducible
assert view.dependency_set(representation.id) is None
assert (
view.dependencies(
representation.id, max_depth=64, max_representations=1000, limit=10
).items
== ()
)
assert (
view.dependents(
AssetRef(asset), max_depth=64, max_representations=1000, limit=10
).items
== ()
)
assert (
view.provenance_ancestors_page(
representation.id, max_depth=64, max_representations=1000, limit=10
).items
== ()
)
assert (
view.provenance_descendants_page(
representation.id, max_depth=64, max_representations=1000, limit=10
).items
== ()
)
assert (
view.stale_artifacts(
max_depth=64, max_representations=1000, limit=10
).items
== ()
)
assert view.outputs_by_activity_kind("example:render", limit=10).items == ()
assert view.outputs_by_tool(ToolIdentity("Example"), limit=10).items == ()
assert (
view.activities_producing_page(representation.id, limit=10).items == ()
)
assert (
view.activities_consuming_page(representation.id, limit=10).items == ()
)
resources = view.resources_page(representation.id, limit=10)
assert len(resources.items) == 1
resource = resources.items[0]
assert view.verify_resource(resource, media) == ContentVerification.MATCHES
assert len(view.resolve(asset)) == 1
assert len(view.locators_page(resource, limit=10).items) == 1
assert view.representations_using_resource(resource, limit=10).items == (
representation,
)
assert view.media_roots == ()
assert view.external_identifiers(AssetRef(asset))[0].value == "camera"
assert view.find_by_external_identifier(
"https://example.com/id", "camera", None
) == (AssetRef(asset),)
assert (
view.find_known_media(LocatorIdentity(file_locator(media)), limit=10)
.items[0]
.asset_id
== asset
)
assert (
view.find_known_media(fingerprint_file(media), limit=10)
.items[0]
.asset_id
== asset
)
base = view.decision_base
assert DecisionBase.from_token(base.to_token()) == base
with production.edit(base) as later:
assert later.commit().revision is None
# Exiting an uncommitted edit rolls back, even without an exception.
with production.edit(base) as discarded:
discarded.add_media_root("discarded")
assert production.media_roots == ()
assert copied.id == asset and representation.asset_id == asset
with production.read_session() as before_job:
with before_job.edit() as request:
job = request.request_job(
JobRequest(
"example:proxy",
(representation.id,),
asset,
RepresentationKind.PROXY,
)
)
request.commit()
assert before_job.jobs(limit=10).items == ()
with production.read_session() as after_job:
assert after_job.job(job).id == job
assert (
len(
after_job.jobs(
limit=10, state=JobState.REQUESTED, kind="example:proxy"
).items
)
== 1
)
production.close()
view.close() # close is idempotent
fn read_then_edit(production: &mut SqliteProduction, media: &Path) -> Result<()> {
let empty = production.read_session()?;
let prepared = prepare_original_media(media, None, None)?;
let asset_id = prepared.asset().id();
let receipt = {
let mut edit = empty.edit(production)?;
edit.import_original(&prepared)?;
edit.commit()?
};
assert_eq!(receipt.revision().expect("import revision").sequence(), 1);
assert!(empty.assets(10)?.next().is_none());
drop(empty);
let view = production.read_session()?;
let assets = view.assets(10)?.collect::<Result<Vec<_>>>()?;
assert_eq!(assets[0].id(), asset_id);
let revisions = view.revisions(0, 10)?.collect::<Result<Vec<_>>>()?;
assert_eq!(revisions.len(), 1);
let revision_id = receipt.revision().expect("import revision").id();
let events = view
.read()
.events_for_revision_page(revision_id, &QueryPageRequest::new(1, None)?)?;
let continuation = view.read().events_for_revision_page(
revision_id,
&QueryPageRequest::new(1000, events.next_cursor().cloned())?,
)?;
assert_eq!(events.items().len(), 1);
assert!(events.next_cursor().is_some());
assert!(continuation.next_cursor().is_none());
assert!(continuation.items()[0].position() > events.items()[0].position());
assert_eq!(
view.read().plan_regeneration(&[])?,
[] as [postproject_core::RegenerationJobPlan; 0]
);
let copied = view.read().asset(asset_id)?;
let page = view
.read()
.representations_page(asset_id, &QueryPageRequest::new(10, None)?)?;
let representation = view.read().representation(page.items()[0].id())?;
let base = view.decision_base();
drop(view);
let mut edit = production.begin_edit(base)?;
assert!(edit.commit()?.revision().is_none());
assert_eq!(copied.id(), asset_id);
assert_eq!(representation.asset_id(), asset_id);
Ok(())
}
postproject init views.pproj
BASE=$(postproject --json inspect views.pproj --limit 10 | jq -r .decision_base)
postproject --json --decision-base "$BASE" root add views.pproj rushes >created.json
jq -e '.commit_receipt.revision.sequence == 1' created.json >/dev/null
ROOT=$(jq -r .id created.json)
# The initial empty base must not silently overwrite the newly created root.
if postproject --json --decision-base "$BASE" root disable views.pproj "$ROOT" \
>conflict.json; then
echo "stale edit unexpectedly succeeded" >&2
exit 1
fi
jq -e '.error.transaction_conflict.base_revision_id == null' conflict.json >/dev/null
# Inspect again, decide again, then pass the refreshed scoped token.
BASE=$(postproject --json inspect views.pproj --limit 10 | jq -r .decision_base)
postproject --json --decision-base "$BASE" root disable views.pproj "$ROOT" >updated.json
jq -e '.commit_receipt.revision.sequence == 2' updated.json >/dev/null
BASE=$(postproject --json inspect views.pproj --limit 10 | jq -r .decision_base)
postproject --json --decision-base "$BASE" root disable views.pproj "$ROOT" >noop.json
jq -e '.commit_receipt.revision == null' noop.json >/dev/null
Commit returns a receipt identifying this edit’s production and newly created
revision. A missing revision means no new journal entry. Do not query the
latest revision to attribute a commit: another writer may already have advanced
it. Commit attempts, rollback, and disposal end the edit. Python Edit contexts
require explicit commit and roll back on normal exit as well as on exceptions.
CLI JSON includes commit_receipt for explicit media, representation, locator,
root, identifier, metadata, dependency, activity and job writes. Use its
production_id and optional revision (id, sequence) to attribute the
command. An observation that changes no recorded facts returns revision: null; read commands
have no commit receipt.
The development C/C++/Python read-session surface currently provides asset and representation point reads and bounded pages, resources, locators, metadata, logical roots, external identifiers, known-media lookup, jobs, dependencies, artifact reports, bounded activities/provenance, output filters, unresolved media, changed objects, journal heads/pages/filters, bounded revision events, regeneration planning, resolution and verification. Rust exposes the full domain read interface. Existing production queries read current state separately. Python and Rust also offer lazy asset/revision scans inside a retained view. The page size bounds memory; it does not limit the complete result. Python checks view lifetime on each advance; Rust borrows the view. Errors propagate, and stopping a scan leaves closing the view to its caller. Media-root pages use the same ordering and cursor rules. Convenience root reads are capped at 1000 and fail above that count; use pages for larger productions. Live getters load current roots rather than roots cached at open.
static int root_pages(const char *path) {
pp_production_t *production = NULL;
pp_transaction_t *transaction = NULL;
pp_read_session_t *view = NULL;
pp_media_root_set_t *live = NULL, *first = NULL, *last = NULL;
pp_error_t *error = NULL;
pp_media_root_id_t root;
int result = 1;
#define CHECK_ROOT(call) do { if ((call) != PP_OK) goto cleanup; } while (0)
CHECK_ROOT(pp_production_create(path, "Root pages", &production, &error));
CHECK_ROOT(pp_production_begin_edit(production, NULL, NULL, &transaction, &error));
CHECK_ROOT(pp_transaction_add_media_root(transaction, "first", NULL, -1, &root, &error));
CHECK_ROOT(pp_transaction_add_media_root(transaction, "second", NULL, 0, &root, &error));
pp_commit_receipt_t commit_receipt;
CHECK_ROOT(pp_transaction_commit(transaction, &commit_receipt, &error));
CHECK_ROOT(pp_production_media_roots_page(production, 1, NULL, &live, &error));
if (pp_media_root_set_count(live) != 1 || !pp_media_root_set_next_cursor(live)) goto cleanup;
CHECK_ROOT(pp_production_read_session(production, &view, &error));
CHECK_ROOT(pp_read_session_media_roots_page(view, 1, NULL, &first, &error));
const char *cursor = pp_media_root_set_next_cursor(first);
if (pp_media_root_set_count(first) != 1 || !cursor) goto cleanup;
/* Keep first alive while borrowing its cursor. The next set owns its rows. */
CHECK_ROOT(pp_read_session_media_roots_page(view, 1, cursor, &last, &error));
if (pp_media_root_set_count(last) != 1 || pp_media_root_set_next_cursor(last)) goto cleanup;
pp_media_root_set_t *invalid = first;
if (pp_production_media_roots_page(production, 0, NULL, &invalid, &error) != PP_ERROR_INVALID_ARGUMENT || invalid != NULL) goto cleanup;
pp_error_release(error); error = NULL;
result = 0;
cleanup:
if (result && error) fprintf(stderr, "%s\n", pp_error_message(error));
pp_error_release(error);
pp_media_root_set_release(last);
pp_media_root_set_release(first);
pp_media_root_set_release(live);
pp_read_session_release(view);
pp_transaction_release(transaction);
pp_production_release(production);
return result;
#undef CHECK_ROOT
}
static void root_pages(const std::string &path) {
auto production = postproject::Production::create(path).value();
auto transaction = production.edit().value();
transaction.addMediaRoot("first", -1).value();
transaction.addMediaRoot("second").value();
transaction.commit().value();
const auto live = production.mediaRoots(1).value();
if (live.items.size() != 1 || !live.next_cursor)
throw std::runtime_error("root page continuation");
auto view = production.readSession().value();
const auto first = view.mediaRoots(1).value();
const auto last = view.mediaRoots(1, first.next_cursor).value();
if (first.items[0].name != "first" || last.items[0].name != "second" || last.next_cursor)
throw std::runtime_error("coherent root page order");
if (view.mediaRoots(0).error().code() != postproject::ErrorCode::invalid_argument ||
view.mediaRoots(1, live.next_cursor).error().code() != postproject::ErrorCode::invalid_argument)
throw std::runtime_error("root page bounds and cursor scope");
}
def root_pages(path: Path) -> None:
with Production.create(path) as production:
with production.edit() as transaction:
transaction.add_media_root("first", priority=-1)
transaction.add_media_root("second")
transaction.commit()
live = production.media_roots_page(limit=1)
assert len(live.items) == 1 and live.next_cursor is not None
with production.read_session() as view:
first = view.media_roots_page(limit=1)
last = view.media_roots_page(limit=1, cursor=first.next_cursor)
assert first.items[0].name == "first"
assert last.items[0].name == "second" and last.next_cursor is None
fn root_pages(production: &mut SqliteProduction) -> Result<()> {
{
let mut transaction = production.begin_transaction()?;
transaction.add_media_root(MediaRoot::new(
MediaRootId::new(),
"first",
None,
None,
-1,
true,
)?)?;
transaction.add_media_root(MediaRoot::new(
MediaRootId::new(),
"second",
None,
None,
0,
true,
)?)?;
transaction.commit()?;
}
let live = production.media_roots_page(&QueryPageRequest::new(1, None)?)?;
assert!(live.next_cursor().is_some());
let view = production.read_session()?;
let first = view
.read()
.media_roots_page(&QueryPageRequest::new(1, None)?)?;
let last = view
.read()
.media_roots_page(&QueryPageRequest::new(1, first.next_cursor().cloned())?)?;
assert_eq!(first.items()[0].name(), "first");
assert_eq!(last.items()[0].name(), "second");
assert!(last.next_cursor().is_none());
assert_eq!(view.read().media_roots()?.len(), 2);
Ok(())
}
postproject init roots.pproj
postproject root add roots.pproj first --priority=-1
postproject root add roots.pproj second
postproject --json root page roots.pproj --limit 1 >first.json
jq -e '.items | length == 1' first.json >/dev/null
CURSOR=$(jq -r .next_cursor first.json)
postproject --json root page roots.pproj --limit 1 --cursor "$CURSOR" >last.json
jq -e '.items[0].name == "second" and .next_cursor == null' last.json >/dev/null
# CLI continuations resume a live query; they do not retain a previous view.
The CLI’s inspect --limit N emits a bounded coherent summary and a
--decision-base token; a truncated summary requires a new inspection rather
than resuming a closed view.
On a semantic conflict, read fresh facts and make the decision again before starting a new edit. See Base revisions and semantic conflicts for conflict fields and which independent changes can merge. Sessions use SQLite WAL for concurrent local readers and writers. Copy or move a production only after closing its handles; an active WAL database includes sidecar state.
To retain a decision across processes, export its base as a canonical token:
C uses pp_decision_base_format/pp_decision_base_parse, C++ uses
DecisionBase::toToken/fromToken, and Python uses to_token/from_token.
The CLI accepts the same token with --decision-base. A token retains neither
a view nor a lock; beginning an edit checks its production and revision.