mentat/docs/tutorial.md

178 lines
6.8 KiB
Markdown
Raw Permalink Normal View History

# Introduction
Mentat is a transactional, relational storage system built on top of SQLite. The abstractions it offers allow you to easily tackle some things that are tricky in other storage systems:
- Have multiple components share storage and collaborate.
- Evolve schema.
- Track change over time.
- Synchronize data correctly.
- Store data with rich, checked types.
Mentat offers a programmatic Rust API for managing stores, retrieving data, and _transacting_ new data. It offers a Datalog-based query engine, with queries expressed in EDN, a rich textual data format similar to JSON. And it offers an EDN data format for transacting new data.
This tutorial covers all of these APIs, along with defining vocabulary.
We'll begin by introducing some concepts, and then we'll walk through some examples.
## What does Mentat store?
Depending on your perspective, Mentat looks like a relational store, a graph store, or a tuple store.
Mentat stores relationships between _entities_ and other entities or _values_. An entity is related to other things by an _attribute_.
All entities have an _entity ID_ (abbreviated to _entid_).
Some entities additionally have an identifier called an _ident_, which is a keyword. That looks something like `:bookmark/title`.
A value is a primitive piece of data. Mentat supports the following:
* Strings
* Long integers
* Double-precision floating point numbers
* Millisecond-precision timestamps
* UUIDs
* Booleans
* Keywords (a special kind of string that we use for idents).
There are two special kinds of entities: _attributes_ and _transactions_.
Attributes are themselves entities with a particular set of properties that define their meaning. They have identifiers, so you can refer to them easily. They have a _value type_, which is the type of value Mentat expects to be on the right hand side of the relationship. And they have a _cardinality_ (whether one or many values can exist for a particular entity), whether values are _unique_, a documentation string, and some indexing options.
An attribute looks something like this:
```edn
{:db/ident :bookmark/title
:db/cardinality :db.cardinality/one
:db/valueType :db.type/string
:db/fulltext true
:db/doc "The title of a bookmark."}
```
Transactions are special entities that can be described however you wish. By default they track the timestamp at which they were written.
The relationship between an entity, an attribute, and a value, occurring in a _transaction_ (which is just another kind of entity!) — a tuple of five values — is called a _datom_.
A single datom might look something like this:
```
[:db/add 65536 :bookmark/title "mozilla.org" 268435456]
^ ^ ^ ^ ^
\ Add or retract. | | | |
\ The entity. | | |
\ The attribute. | |
\ The value, a string. |
\ The transaction ID.
```
which is equivalent to saying "in transaction 268435456 we assert that entity 65536 is a bookmark with the title 'mozilla.org'".
When we transact that — which means to add it as a fact to the store — Mentat also describes the transaction itself on our behalf:
```edn
[:db/add 268435456 :db/txInstant "2018-01-25 20:07:04.408358 UTC" 268435456]
```
# A simple app
Let's get started with some Rust code.
First, the imports we'll need. The comments here briefly explain what each thing is.
```rust
// So you can define keywords with neater syntax.
#[macro_use(kw)]
extern crate mentat;
use mentat::{
Store, // A single database connection and in-memory metadata.
}
use mentat::vocabulary::attribute; // Properties of attributes.
```
## Defining a simple vocabulary
All data in Mentat — even the terms we used above, like `:db/cardinality` — are defined in the store itself. So that's where we start. In Rust, we define a _vocabulary definition_, and ask the store to ensure that it exists.
```rust
fn set_up(mut store: Store) -> Result<()> {
// Start a write transaction.
let mut in_progress = store.begin_transaction()?;
// Make sure the core vocabulary exists. This is good practice if a user,
// an external tool, or another component might have touched the file
// since you last wrote it.
in_progress.verify_core_schema()?;
// Make sure our vocabulary is installed, and install if necessary.
// This defines some attributes that we can use to describe people.
in_progress.ensure_vocabulary(&Definition {
name: kw!(:example/people),
version: 1,
attributes: vec![
(kw!(:person/name),
vocabulary::AttributeBuilder::default()
.value_type(ValueType::String)
.multival(true)
.build()),
(kw!(:person/age),
vocabulary::AttributeBuilder::default()
.value_type(ValueType::Long)
.multival(false)
.build()),
(kw!(:person/email),
vocabulary::AttributeBuilder::default()
.value_type(ValueType::String)
.multival(true)
.unique(attribute::Unique::Identity)
.build()),
],
})?;
in_progress.commit()?;
Ok(())
}
```
We open a store and configure its vocabulary like this:
```rust
let path = "/path/to/file.db";
let store = Store::open(path)?;
set_up(store)?;
```
If this code returns successfully, we're good to go.
## Transactions
You'll see in our `set_up` function that we begin and end a transaction, which we call `in_progress`. A read-only transaction is begun via `begin_read`. The resulting objects — `InProgress` and `InProgressRead` support various kinds of read and write operations. Transactions are automatically rolled back when dropped, so remember to call `commit`!
## Adding some data
There are two ways to add data to Mentat: programmatically or textually.
The textual form accepts EDN, a simple relative of JSON that supports richer types and more flexible syntax. You saw this in the introduction. Here's an example:
```rust
in_progress.transact(r#"[
{:person/name "Alice"
:person/age 32
:person/email "alice@example.org"}
]"#)?;
```
You can implicitly _upsert_ data when you have a unique attribute to use:
```rust
// Alice's age is now 33. Note that we don't need to find out an entid,
// nor explicitly INSERT OR REPLACE or UPDATE OR INSERT or similar.
in_progress.transact(r#"[
{:person/age 33
:person/email "alice@example.org"}
]"#)?;
```