From 131398758b31016ae2195143ca883472d1b987bc Mon Sep 17 00:00:00 2001 From: Greg Burd Date: Thu, 23 Jan 2020 14:31:28 -0500 Subject: [PATCH] Integrate https://github.com/mozilla/mentat/pull/806 --- docs/tutorial.md | 177 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 docs/tutorial.md diff --git a/docs/tutorial.md b/docs/tutorial.md new file mode 100644 index 00000000..8015e5ec --- /dev/null +++ b/docs/tutorial.md @@ -0,0 +1,177 @@ +# 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"} +]"#)?; +``` +