Commits
Publish Table writes and name changes as one Project commit, handle a moved branch, and recover a commit whose outcome is unknown.
A Commit publishes changes to one or more Tables as one Project
commit, so readers see all of
the changes or none of them. Your application never exposes a half-finished
update, even if it writes several Tables or crashes partway through.
For the step-by-step tasks, see Create a Table and Write rows. This page covers how the Rust types behave.
Write versions, then publish them
Pin the head of a branch with project.branch("main"). The BranchHead it
returns is a Snapshot of that head, so you open
Tables from it as from any Snapshot. Write new Table versions, stage them in a
Commit started from the same head, and publish:
let project = client.project("demo")?;
let main = project.branch("main").await?;
let appended = main.table("frames").await?.append(frames).await?;
let created = project.write_table(labels).await?;
let mut commit = main.commit().message("Add labels");
commit.update_table(&appended)?;
commit.create_table("labels", &created)?;
match commit.publish().await {
Ok(head) => println!("committed {}", head.commit_id()),
Err(PublishError::BranchMoved { .. } | PublishError::Conflict(_)) => {
println!("main moved; nothing was published")
}
Err(error) => return Err(error.into()),
}Each write uploads its rows straight away and
returns a new, immutable version of the Table, but nobody else can see it yet.
project.write_table does the same for the first version of a Table that has
no name yet. publish then records every staged version and name in one
commit on the branch, so readers switch to the new data together.
Stage changes
Staging records which versions and names change. It does no I/O, except for Connections and Enrichments.
| Method | Stages |
|---|---|
update_table(&table) | The Table's new version, in place of the version the head selects. |
create_table(name, &table) | A Table written by project.write_table or project.table_writer, bound as name at the Project root. |
rename_table(&table, name) | A new root name for a Table opened by path. The Table keeps its ID and version. |
drop_table(&table) | Retiring a Table opened by path, and unbinding its path. |
create_connection(path, uri, credential, description) | A new Connection and its name, on main only. This method is async: it reads the names at the head and calls the Connections service, which stores the credential. |
create_enrichment(&table, name, expression) | A new Enrichment of a Table opened at the head, named name under the Table. This method is async: it checks the expression against the Table's schema and calls the Tables API, which stores the definition. |
drop_connection(path) | Retiring a Connection and unbinding its name, on main only. This method is async, because it reads the names at the head. |
message(text) | A message recorded with the commit. |
Three rules keep the commit consistent:
- Open the Tables you change from the Commit's own head, or write them from a Table opened there. The Commit rejects a Table from another Snapshot, because the commit must describe changes to the state it read.
- Change each Table at most once per Commit. To add two batches to one Table, append to the result of the first append, and stage only the last version. The commit swaps the head's version for that one; the versions in between are never published.
- Start a Commit only from a
BranchHead.project.snapshot_at(commit_id)returns a read-only Snapshot, because an exact commit has no branch to publish to.
Read the result
publish returns the branch's new head as a BranchHead. Open Tables from it
to read what you published, or start the next Commit from it. To move a
Table session to the new head, call
session.with_head(head).
When publishing fails, publish returns a PublishError:
| Variant | Meaning | What to do |
|---|---|---|
BranchMoved { current } | Another writer moved the branch after you pinned it. Nothing was published. | Pin the branch again and redo the writes. |
Conflict(conflict) | A staged change conflicts with the head, for example creating a name that already exists. Nothing was published. | Change what you stage. |
Unknown { commit, .. } | The response was lost, so the commit may or may not have been published. | Recover commit. Do not redo the writes. |
Failed(error) | The Catalog rejected the commit, or the request was never sent. Nothing was published. | Read the wrapped Error. |
The ? operator converts a PublishError into Error::BranchMoved,
Error::Conflict or Error::OutcomeUnknown, and Failed into the Error it
wraps. Error::OutcomeUnknown does not carry the frozen commit, so match
PublishError::Unknown before converting when you want to recover.
When another writer got there first
publish succeeds only if the branch head is still the commit you pinned. If
another writer moved the branch, even by changing other Tables, publish
returns PublishError::BranchMoved and publishes nothing.
The SDK does not rebase your changes onto the new head. Your writes were
computed from the old rows, and only your application knows whether they are
still correct. To try again, pin the branch again, open the Tables again, and
redo the writes in a new Commit. This is the one case where you upload again.
A new Table from project.write_table is the exception: its rows do not
depend on the head, so stage the same Table in the new Commit.
When you do not know whether the commit landed
If the response to publish is lost, for example because the network dropped,
publish returns PublishError::Unknown with a FrozenCommit. Its
publication request is fixed: publishing it again sends exactly the same
request, and the Catalog publishes one request at most once. Do not redo the
writes; recover with that frozen commit:
let appended = main.table("frames").await?.append(frames).await?;
let mut commit = main.commit();
commit.update_table(&appended)?;
match commit.publish().await {
Err(PublishError::Unknown { commit, .. }) => recover(&commit).await,
result => Ok(result?),
}/// Publish a frozen commit unless a lost acknowledgement already published it.
async fn recover(commit: &FrozenCommit) -> Result<BranchHead, Error> {
match commit.recover().await? {
Some(head) => Ok(head),
None => Ok(commit.publish().await?),
}
}recover looks up the commit by its operation ID. If it returns a head, your
commit was published, and that is the new head. If it returns None, Spiral
has no record of it, and publishing again is safe. For the HTTP side of this,
see Retries.
Survive a process restart
To recover a commit after your process restarts, save it before you publish:
- Stage all changes, then call
commit.freeze(). It returns aFrozenCommit, which accepts no more changes. - Save
frozen.save()to durable storage. TheSavedCommitit returns serializes with any serde format. - Call
frozen.publish(). - After a restart, call
project.resume_commit(saved)with the saved value. It returns the sameFrozenCommit. - Call
recover, and callpublishonly ifrecoverreturnsNone.
The resumed commit keeps its original head and branch, so the check for a moved branch after the restart still compares against what you read.