# Rust quickstart

Stand up a live, incrementally-maintained SQLite query with the open engine — register a source, hydrate a view, and watch a write-through push update it by the delta.

Stand up a live, incrementally-maintained SQLite query in a few minutes, using
only the **open-source** engine. You open a SQLite database, register a table as a
*source*, build a query, hydrate a materialized **view** over it, and push changes
— and each push is *write-through*: it persists to SQLite **and** updates the view
by the delta, never re-running the query.

> **Open source.** This quickstart uses only `rindle` (the IVM engine) and
> `rindle-sqlite` (the SQLite backend) — both **Apache-2.0**, at
> [`github.com/rindle-sh/rindle`](https://github.com/rindle-sh/rindle). Link them
> freely. The turnkey version — write *ordinary SQL* through one controlled writer
> and let a `preupdate` hook derive the deltas for you, plus multi-threaded reader
> scale-out — is the [live-replica runtime](/docs/replica-and-views), Rindle's
> commercial high-concurrency layer on top of this same engine.

## Add the crates

The engine crates aren't on crates.io yet — depend on them from git (or a local
checkout). `rindle-sqlite` links SQLite through `rusqlite`'s `bundled` feature; use
the same `rusqlite` version so the `Connection` type lines up.

```toml
# Cargo.toml
[dependencies]
rindle = { git = "https://github.com/rindle-sh/rindle" }
rindle-sqlite = { git = "https://github.com/rindle-sh/rindle" }
rusqlite = { version = "0.32", features = ["bundled"] }
```

You build queries with `rindle`'s fluent builder, register a SQLite table with
`rindle-sqlite`'s `TableSource`, and drive everything through a `rindle::Graph`.

## Open a database and register a source

Open a SQLite database and create your ordinary schema. The source uses the
primary key for row identity, so it requires a **UNIQUE index** on it — create one
explicitly (the [live-replica runtime](/docs/replica-and-views) does this for you).
Then wrap the table in a `TableSource` and add it to a `Graph`.

```rust
use std::collections::HashMap;
use std::rc::Rc;
use rindle::{Graph, SourceSchema, ValueType};
use rindle_sqlite::{ColumnDef, GraphTableSourceExt, TableSource};
use rusqlite::Connection;

// 1. An ordinary SQLite database + schema, with a UNIQUE index on the primary key.
let db = Rc::new(Connection::open("app.db")?);
db.execute_batch(
    "CREATE TABLE issues (id INTEGER NOT NULL, title TEXT, open BOOLEAN);
     CREATE UNIQUE INDEX issues_pk ON issues (id);",
)?;

// 2. Describe the columns (name, engine type, nullable) and the primary key.
let columns = vec![
    ColumnDef { name: "id".into(),    ty: ValueType::Number,  optional: false },
    ColumnDef { name: "title".into(), ty: ValueType::String,  optional: true  },
    ColumnDef { name: "open".into(),  ty: ValueType::Boolean, optional: true  },
];
// The SourceSchema (columns, primary-key indices, default sort) is what queries
// resolve against — reused below for both lowering and the view.
let schema = SourceSchema::new(vec!["id", "title", "open"], vec![0], vec![(0, true)]);

// 3. Register the table as a source on the graph.
let mut graph = Graph::new();
let issues_src = graph.add_table_source(
    TableSource::new_with_schema(db.clone(), "issues", columns, vec![0], schema.clone()),
);

// `resolve` maps each table name to (source NodeId, SourceSchema).
let mut sources = HashMap::new();
sources.insert("issues", (issues_src, schema));
let resolve = |name: &str| sources.get(name).cloned();
```

## Define a query

A query is a `rindle::Ast`, built with the fluent builder. The engine watches the
source and keeps the result up to date as rows change — no polling, no re-running.

```rust
use rindle::table;

// All OPEN issues.
let ast = table("issues").r#where("open", true).build();
```

### More shapes: nesting, aggregates, projection

The builder also nests a correlated child, counts it, and projects columns — all
detailed on [supported queries](/docs/supported-queries):

```rust
// nested — every issue with its comments (correlate the child via `r.col`)
let with_comments = table("issues")
    .sub_as("comments", |r| table("comments").r#where("issue_id", r.col("id")))
    .build();

// aggregate — each issue with a live scalar count of its comments
let counted = table("issues")
    .count_as("commentCount", |r| table("comments").r#where("issue_id", r.col("id")))
    .build();

// projection — sync only selected columns
let slim = table("issues").select("id").select("title").build();
```

(A nested or joined query resolves every table it names, so register a
`TableSource` for each — `comments` as well as `issues` — the same way.)

## Build the pipeline and hydrate a view

`build_pipeline` lowers the `Ast` into a wired dataflow graph; `view_schema`
derives the view's hierarchical schema from the same `resolve`. Add a `View` over
the pipeline's top, wire the final edge, and `hydrate` to materialize the initial
result.

```rust
use rindle::{build_pipeline, view_schema};

let top = build_pipeline(&mut graph, &ast, &resolve).expect("build the pipeline");
let vschema = view_schema(&ast, &resolve).expect("derive the view schema");

let view = graph.add_view(top, vschema);
graph.set_sink_edge(top, view);
graph.try_hydrate(view)?; // materialize the initial result set

// Read the materialized result. `ViewData` is a reference-stable list of entries;
// each `Entry` carries its `row` (and, for nested queries, child `rels`). Cells are
// read positionally with `row.col(c)`, or as a `Vec` with `row.to_value_vec()`.
let data = graph.view_data(view);
for entry in &data.items {
    println!("{:?}", entry.row.to_value_vec());
}
```

In production prefer the fallible `try_*` entry points (`TableSource::try_new*`,
`try_hydrate`, `try_source_push` below) — they return `RindleError` instead of
panicking; their infallible peers `.expect` and are for tests and prototyping.

## Push a change — write-through

A mutation is a `rindle::SourceChange` — `Add`, `Remove`, or `Edit`. Pushing one
through the SQLite source is **write-through**: it runs the underlying
`INSERT` / `UPDATE` / `DELETE` **and** folds the delta into every view over that
source, in one call. The cost is proportional to the change, not the table size.

```rust
use rindle::{owned_row, OwnedValue, SourceChange};

// Add two open issues — each push writes SQLite AND folds the delta into the view.
graph.try_source_push(issues_src, SourceChange::Add(
    owned_row(vec![OwnedValue::Int(1), OwnedValue::str("first"),  OwnedValue::Bool(true)]),
))?;
graph.try_source_push(issues_src, SourceChange::Add(
    owned_row(vec![OwnedValue::Int(2), OwnedValue::str("second"), OwnedValue::Bool(true)]),
))?;

// Close issue 2 — an Edit that no longer matches `open = true`, so the view drops it.
graph.try_source_push(issues_src, SourceChange::Edit {
    old: owned_row(vec![OwnedValue::Int(2), OwnedValue::str("second"), OwnedValue::Bool(true)]),
    row: owned_row(vec![OwnedValue::Int(2), OwnedValue::str("second"), OwnedValue::Bool(false)]),
})?;

graph.flush_view(view);           // close the transaction and fire the view's listeners
let data = graph.view_data(view); // now holds exactly issue 1
```

The view now equals what a fresh `SELECT ... WHERE open = 1` would return — reached
by the delta alone, with no rescan. That equivalence — *view-after-write ==
fresh-query* — is the whole contract.

The complete program ships as a runnable example:

```bash
cargo run -p rindle-sqlite --example live_query
```

## What you get

| Property    | Guarantee                                               |
| ----------- | ------------------------------------------------------- |
| Correctness | The maintained view equals a freshly-hydrated query     |
| Latency     | Proportional to the change, not the table size          |
| Openness    | `rindle` + `rindle-sqlite` are Apache-2.0 — link freely |

## Next steps

- [Fold the delta stream yourself](/docs/example-rust) — subscribe to the raw
  `CaughtChange` deltas, maintain your own view, and prove it equals a fresh query.
- [How it works](/docs/how-it-works) — the build → lower → hydrate → push data path.
- [The change model](/docs/change-model) — the `Add` / `Remove` / `Edit` / `Child`
  deltas in depth.
- [The live-replica runtime](/docs/replica-and-views) — write ordinary SQL, get the
  deltas derived for you, and scale readers out.

---

[View this page on Rindle](https://rindle.sh/docs/quickstart)
