prolly-store-spanner 0.3.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.

## Installation

The client dependency is listed explicitly because applications construct the
`ClientConfig` passed to the adapter:

```toml
[dependencies]
prolly-map = "0.4"
prolly-store-spanner = "0.3.0"
google-cloud-spanner = { package = "gcloud-spanner", version = "=1.8.1" }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```

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

```sql
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:

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

Create the tables using `gcloud`:

```bash
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

```rust
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

```rust
# 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.
- Strict commits validate named-root preconditions and apply node and root
  mutations in one 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:

```bash
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.

## Testing

The integration test runs when `PROLLY_STORE_SPANNER_DATABASE` is set. Set
`PROLLY_STORE_SPANNER_AUTH=1` to use application-default authentication; omit it
when the client environment already supplies the intended emulator or channel
configuration:

```bash
cargo test --manifest-path stores/prolly-store-spanner/Cargo.toml
```

Use a dedicated test database or distinct named-root prefix. The adapter does
not provide a backend-wide key prefix or cleanup helper.

See the [`prolly-map` API documentation](https://docs.rs/prolly-map) for the
async map, transaction, diff, and merge APIs used with this backend.