prolly-store-spanner 0.1.0

Cloud Spanner store adapter for prolly-map.
Documentation

prolly-store-spanner

Cloud Spanner-backed remote store adapter for prolly-map.

This crate implements RemoteStoreBackend using gcloud-spanner. Use it through RemoteProllyStore and AsyncProlly when you need globally consistent SQL-backed Prolly tree storage on Google Cloud Spanner.

When To Use It

Use this adapter when your application already runs on Cloud Spanner or when named roots need strongly consistent, horizontally scalable SQL storage. It is a good fit for multi-region metadata, durable branch heads, and services where Spanner availability and transaction semantics are more important than local single-node latency.

Use PostgreSQL/MySQL for simpler single-region SQL deployments. Use Redis for cache-like state. Use DynamoDB or Cosmos DB when your cloud platform is AWS or Azure and you prefer their native NoSQL services.

Table Model

The adapter expects these GoogleSQL tables:

CREATE TABLE ProllyNodes (
  Cid BYTES(32) NOT NULL,
  Node BYTES(MAX) NOT NULL
) PRIMARY KEY (Cid);

CREATE TABLE ProllyHints (
  Namespace BYTES(MAX) NOT NULL,
  HintKey BYTES(MAX) NOT NULL,
  Value BYTES(MAX) NOT NULL
) PRIMARY KEY (Namespace, HintKey);

CREATE TABLE ProllyRoots (
  Name BYTES(MAX) NOT NULL,
  Manifest BYTES(MAX) NOT NULL
) PRIMARY KEY (Name);

The same DDL is exposed as SPANNER_SCHEMA.

Setup

Set the Spanner database resource name:

export PROLLY_STORE_SPANNER_DATABASE=projects/<project>/instances/<instance>/databases/<database>

Create the tables using gcloud:

gcloud spanner databases ddl update <database> \
  --instance=<instance> \
  --ddl='CREATE TABLE ProllyNodes (Cid BYTES(32) NOT NULL, Node BYTES(MAX) NOT NULL) PRIMARY KEY (Cid)'

gcloud spanner databases ddl update <database> \
  --instance=<instance> \
  --ddl='CREATE TABLE ProllyHints (Namespace BYTES(MAX) NOT NULL, HintKey BYTES(MAX) NOT NULL, Value BYTES(MAX) NOT NULL) PRIMARY KEY (Namespace, HintKey)'

gcloud spanner databases ddl update <database> \
  --instance=<instance> \
  --ddl='CREATE TABLE ProllyRoots (Name BYTES(MAX) NOT NULL, Manifest BYTES(MAX) NOT NULL) PRIMARY KEY (Name)'

Authentication depends on gcloud-spanner configuration. In the examples and tests, set PROLLY_STORE_SPANNER_AUTH=1 to call ClientConfig::with_auth().

Basic Usage

use google_cloud_spanner::client::ClientConfig;
use prolly::{AsyncProlly, Config, Mutation, RemoteProllyStore};
use prolly_store_spanner::SpannerBackend;

# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let database = "projects/my-project/instances/my-instance/databases/my-db";
let config = ClientConfig::default().with_auth().await?;
let backend = SpannerBackend::connect(database, config).await?;

let prolly = AsyncProlly::new(RemoteProllyStore::new(backend), Config::default());
let tree = prolly
    .batch(
        &prolly.create(),
        vec![Mutation::Upsert {
            key: b"account/1".to_vec(),
            val: b"active".to_vec(),
        }],
    )
    .await?;

prolly.publish_named_root(b"accounts/main", &tree).await?;
# Ok(())
# }

Diff And Merge

# use prolly::{AsyncProlly, Config, Mutation, RemoteProllyStore};
# use prolly_store_spanner::SpannerBackend;
# async fn run(backend: SpannerBackend) -> Result<(), Box<dyn std::error::Error>> {
# let prolly = AsyncProlly::new(RemoteProllyStore::new(backend), Config::default());
# let base = prolly.batch(&prolly.create(), vec![
#     Mutation::Upsert { key: b"account/1".to_vec(), val: b"active".to_vec() },
#     Mutation::Upsert { key: b"account/2".to_vec(), val: b"active".to_vec() },
# ]).await?;
let left = prolly
    .batch(
        &base,
        vec![Mutation::Upsert {
            key: b"account/1".to_vec(),
            val: b"suspended".to_vec(),
        }],
    )
    .await?;
let right = prolly
    .batch(
        &base,
        vec![Mutation::Upsert {
            key: b"account/2".to_vec(),
            val: b"closed".to_vec(),
        }],
    )
    .await?;

let diffs = prolly.diff(&base, &left).await?;
assert_eq!(diffs.len(), 1);

let merged = prolly.merge(&base, &left, &right, None).await?;
assert_eq!(
    prolly.get(&merged, b"account/2").await?,
    Some(b"closed".to_vec())
);
# Ok(())
# }

Operational Notes

  • The adapter does not create tables. Apply DDL before startup.
  • Named-root compare-and-swap uses a Spanner read-write transaction.
  • batch_put_nodes is applied as Spanner mutations.
  • There is no adapter-level key prefix. Use distinct named-root prefixes for tenants or environments, and isolate databases when you need full physical separation.
  • Node garbage collection should be coordinated at the application layer after deciding which named roots to retain.

Running The Example

From the standalone repository root:

export PROLLY_STORE_SPANNER_DATABASE=projects/<project>/instances/<instance>/databases/<database>
export PROLLY_STORE_SPANNER_AUTH=1
cargo run --manifest-path stores/prolly-store-spanner/Cargo.toml --example basic_usage

The example writes a base tree, diffs and merges branches, resolves a conflict, publishes a unique named root, and loads it back.