SDKs & ToolsRust SDK

Commits

Publish Table writes and name changes as one Project commit, handle a moved branch, and recover a commit whose outcome is unknown.

In development

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.

MethodStages
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:

VariantMeaningWhat 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:

  1. Stage all changes, then call commit.freeze(). It returns a FrozenCommit, which accepts no more changes.
  2. Save frozen.save() to durable storage. The SavedCommit it returns serializes with any serde format.
  3. Call frozen.publish().
  4. After a restart, call project.resume_commit(saved) with the saved value. It returns the same FrozenCommit.
  5. Call recover, and call publish only if recover returns None.

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.

Found a problem? Edit this page or report an issue.

On this page