Gunnar

nordisk/knut

.nornir/graphar-guide.md

# GraphAr gar/v1 substrate — guide

> **Class:** `guide` · audience: a techie reading, writing, or validating a GraphAr graph.
> The *what* is in [`graphar-design.md`](graphar-design.md); the *why* in [`graphar-reasoning.md`](graphar-reasoning.md).
> This file is the API and worked examples.

## f0 — a graph in one round trip (the whole workflow)

Four moves take Arrow batches to a validated gar/v1 directory and back:

1. **Write chunks.** `io::write_chunk(path, &batches, &FileType::Parquet)` — pick any of the
   five formats; the Arrow schema is preserved either way.
2. **Read them back.** `io::read_chunk(path, &FileType::Parquet)` → the same `Vec<RecordBatch>`.
3. **Build a real graph.** `VerticesBuilder`/`EdgesBuilder` accumulate typed rows and `dump()`
   the whole chunk layout; `VertexReader`/`EdgeReader` stream it back.
4. **Validate.** `validate::validate_graph_dir(graph_yml)` — empty `Vec` means gar/v1-conformant.

Put a graph on S3/GCS/Azure by swapping the file calls for a `ChunkStore`; everything else
is identical.

## f1 — chunk io (the raw seam)

```rust
use graphar::{io, types::FileType};

// Write one chunk file (parent dirs created for you):
io::write_chunk("vertex/person/id/chunk0.parquet", &batches, &FileType::Parquet)?;
// Read it back to the same Arrow batches:
let batches = io::read_chunk("vertex/person/id/chunk0.parquet", &FileType::Parquet)?;
```

`FileType` is `Parquet | Orc | Csv | Json | ArrowIpc`; `FileType::extension()` gives the file
extension. Same schema in, same schema out — format is a storage decision, not a data one.

## f2 — builders & readers

```rust
use graphar::{VerticesBuilder, EdgesBuilder, Vertex, Edge, PropertyValue, VertexReader};

// Vertices: accumulate, then dump the chunk layout described by `info`.
let mut vb = VerticesBuilder::new(vertex_info, "graph_dir");
vb.add_vertex(Vertex::new().add_property("name", PropertyValue::from("Ada")));
let n = vb.dump()?;                 // -> u64 vertex count; each row's index is its _gar_id

// Edges reference endpoints by _gar_id:
let mut eb = EdgesBuilder::new(edge_info, "graph_dir", /* ... */);
eb.add_edge(Edge::new(0, 1).add_property("since", PropertyValue::from(2020)));
eb.dump()?;

// Read back — read_merged() row-aligns every property group into one batch:
let vr = VertexReader::new(vertex_info, "graph_dir", n);
let batch = vr.read_merged()?;
```

Standalone row-aligned join (when you already hold group batches):
`merge_property_groups(&[group_a_batches, group_b_batches]) -> RecordBatch`.

## f3 — ChunkStore over object storage

```rust
use graphar::storage::{ChunkStore, RetryOptions, MultipartOptions};
use graphar::types::FileType;
use std::time::Duration;

// Local, S3/GCS/Azure by URL, or S3-compatible (SeaweedFS/RustFS/MinIO):
let store = ChunkStore::from_url("s3://my-bucket/graphs/social")?;      // default retry policy

// Explicit retry/backoff, and a tuned multipart policy:
let store = ChunkStore::from_url_with_retry(
        "s3://my-bucket/graphs/social",
        RetryOptions::new(5, Duration::from_millis(200), Duration::from_secs(10)),
    )?
    .with_multipart(MultipartOptions::new(8 << 20, 5 << 20)); // 8 MiB threshold, 5 MiB parts

// Async chunk ops, keyed by a relative path under the store prefix:
store.write_chunk("vertex/person/id/chunk0.parquet", &batches, &FileType::Parquet).await?;
let batches = store.read_chunk("vertex/person/id/chunk0.parquet", &FileType::Parquet).await?;
```

`RetryOptions::none()` disables retries; `RetryOptions::default()` reproduces the library's
policy (10 retries, 100 ms→15 s, 3-min budget). Writes larger than
`MultipartOptions::threshold` (default 16 MiB) automatically stream via `put_multipart`;
smaller ones use a single `put`.

## f4 — validation

```rust
use graphar::validate::{validate_graph_dir, validate_graph_dir_with, ValidateOptions, Severity};

let issues = validate_graph_dir("graph_dir/social.graph.yml");
assert!(issues.iter().all(|i| i.severity != Severity::Error));  // errors = non-conformant

// Upstream corpus (extensionless chunks, optional chunk dirs):
let issues = validate_graph_dir_with("graph_dir/social.graph.yml", ValidateOptions::corpus());
```

Or validate from already-loaded info structs: `GraphInfo::validate(base)`,
`VertexInfo::validate_dir(base)`, `EdgeInfo::validate_dir(base)`. Validation is read-only and
offline — it walks the directory and reports every deviation from `gar/v1`; an empty
`Vec<ValidationIssue>` (or a list with only warnings) means the graph is conformant.

## Surfaces (Rust API)

| Call | Does |
|---|---|
| `io::write_chunk(path, &batches, &FileType)` | Write Arrow batches as one chunk file |
| `io::read_chunk(path, &FileType)` | Read a chunk file back to Arrow batches |
| `VerticesBuilder::new(info, dir).add_vertex(..).dump()` | Build chunked vertex files (returns count) |
| `EdgesBuilder::new(...).add_edge(..).dump()` | Build chunked edge files (endpoints by `_gar_id`) |
| `VertexReader` / `EdgeReader` | Stream a graph's chunks as Arrow; `read_merged` joins groups |
| `merge_property_groups(&[group_batches])` | Row-aligned join of property groups |
| `ChunkStore::{from_url, s3_compatible, local, new}` | Read/write chunks over `object_store` backends |
| `ChunkStore::{from_url,s3_compatible}_with_retry(.., RetryOptions)` | Same constructors with an explicit retry/backoff policy |
| `RetryOptions{ max_retries, init_backoff, max_backoff, retry_timeout }` | Typed front for `object_store::RetryConfig`; `::none()` opts out |
| `ChunkStore::with_multipart(MultipartOptions)` (+ `multipart_options()`) | Tune the multipart upload policy on any store |
| `MultipartOptions{ threshold, part_size }` | Encoded-size threshold (default 16 MiB) and per-part size (default 8 MiB) |
| `validate::validate_graph_dir(graph_yml)` (+ `_with(.., ValidateOptions)`) | Structurally validate a written dir; `Vec<ValidationIssue>` (empty = conformant) |
| `GraphInfo::validate(base)` / `Vertex`/`EdgeInfo::validate_dir(base)` | Same, from already-loaded info structs |
| `ValidateOptions{ extensionless_chunks, require_chunks }` (+ `::corpus()`) | Relax chunk-name/required-chunk checks for the upstream corpus |