Platform

Quickstart

Build the smallest Spiral application: create a Project, write a Table from Rust, and read that exact version back.

In development
Time
15 minutes
Difficulty
beginner
Account
Spiral account required

This tutorial builds the smallest Spiral application. A Rust program writes three camera frames to a new Table called frames, publishes them in one commit, and reads them back. You finish with a version address, frames@v…, that reads exactly those three rows no matter what is written later. That address is how an application, or a training job, records the data it used.

Before you start

The Rust SDK is available as spiral-sdk on crates.io. The CLI is available as spiral-cli on PyPI.

You also need:

  • a Spiral account;
  • Rust and Cargo, with the nightly toolchain required by the SDK;
  • Python 3.11 or later with pip, to install the spiral CLI.

1. Sign in

Install the CLI and sign in:

pip install spiral-cli
spiral login

spiral login prints a code and a URL. Open the URL, approve the code in your browser, and the CLI finishes signing you in. If you belong to more than one Organization, it asks which one to use.

2. Choose an Organization

An Organization is the account that holds your team. If you do not have one yet, create it; the CLI switches to it:

spiral orgs create "Quickstart"

If you already have one, list your Organizations and switch:

spiral orgs ls
spiral orgs switch

3. Create a Project

A Project is the workspace that holds your Tables and their history. Create one in the active Organization:

spiral projects create --description "Quickstart"

The CLI prints Created project <project-id>. As the creator, you get the admin role on it. Save the ID for the Rust program:

export SPIRAL_PROJECT=<project-id>

4. Get a token

The Rust program authenticates with a bearer token. --show-token prints the access token of your current sign-in on the last line of its output, so keep only that line:

export SPIRAL_TOKEN="$(spiral login --show-token | tail -n 1)"

The token lasts at most one hour. Run the command again when it expires.

5. Connect from Rust

Add spiral-sdk to your application with the embedded feature, and an async runtime such as Tokio. The crate's library name is spiral, so your code imports spiral::....

cargo add spiral-sdk --features embedded
cargo add tokio --features macros,rt-multi-thread

Prepare the matching native archive before building, following Use the embedded backend. The SDK links that archive into your application at build time.

Build a client. It sends control-plane requests to the Spiral API, and it uses the linked native runtime so that reads and writes run in your own process. Signed storage requests go through https://spfs.spiraldb.dev:

let config = ScandalConfig {
    spfs_endpoint: Some(spfs_url.parse()?),
    local_files: false,
    // Storage the Tables API grants with a provider credential is read without SPFS.
    provider_roots: true,
    refuse_custom_endpoints: false,
};
let client = Client::builder()
    .api_url("https://api.spiraldb.dev")
    .bearer_token(token)
    .embedded_scandal(config)
    .build()?;

Set token to the value of SPIRAL_TOKEN and spfs_url to https://spfs.spiraldb.dev.

6. Make rows

Make one Arrow record batch with three frames:

let schema = Arc::new(Schema::new(vec![
    Field::new("frame_id", DataType::Int64, false),
    Field::new("camera", DataType::Utf8, false),
]));
let frame_ids: ArrayRef = Arc::new(Int64Array::from(vec![1, 2, 3]));
let cameras: ArrayRef = Arc::new(StringArray::from(vec!["front", "left", "front"]));
let frames = RecordBatch::try_new(schema, vec![frame_ids, cameras])?;

Import Arrow types through spiral::arrow_array and spiral::arrow_schema. That way your Arrow version always matches the SDK's.

7. Create the Table and commit

Pin the head of the main branch, write the Table, and publish it in a commit:

let project = client.project(project_id)?;
let main = project.branch("main").await?;
let table = project.write_table(frames).await?;
let mut commit = main.commit();
commit.create_table("frames", &table)?;
let committed = commit.publish().await?;
println!("committed {}", committed.commit_id());

Set project_id to SPIRAL_PROJECT.

write_table writes the rows to storage and registers the Table's first version, but nobody can see the Table yet. create_table stages the name frames for that version, and publish records both in one new commit on main. Pinning main first is what makes this safe: if another writer moved main after you pinned it, publish returns an error and publishes nothing, so you never overwrite a change you did not see.

8. Read the rows back

Read the Table from the new commit:

let table = committed.table("frames").await?;
if let Some(address) = table.resolved_address() {
    println!("reading {address}");
}
let mut batches = table.scan(ScanOptions::default()).await?.into_stream();
let mut rows = 0;
while let Some(batch) = batches.next_batch().await {
    let batch = batch?;
    rows += batch.num_rows();
    println!(
        "batch: {} rows, {} columns",
        batch.num_rows(),
        batch.num_columns()
    );
}
println!("read {rows} rows");

The program prints:

  • committed and the new commit ID;
  • reading frames@v and the 32-digit ID of the Table version;
  • batch: and the row and column counts of each record batch;
  • read 3 rows.

What you built

Your Project now has a Table, frames, and a commit on main that publishes it. The address frames@v<version> names this exact version of the Table. A later commit to main changes what frames@main reads, but never what frames@v<version> reads. See Commits and Branches for how that works.

The rows went straight from your program to object storage. Spiral's services saw only the name, the version ID and your permissions. Architecture follows that split step by step.

Where next

  • Write rows to append to or overwrite the Table.
  • Commits shows how to recover a commit whose response was lost.
Found a problem? Edit this page or report an issue.

On this page