Examples

Examples

Small, complete programs. Each one compiles against the library; start a server with docker run -d -p 1729:1729 typedb/typedb:3.14.0 to run them.

A social network

The repository's example program uses every feature at once: entities, a subtype, relations, a relation that plays a role in another relation, migrations, ORM reads and writes, typed queries, streaming, polymorphic decoding and concurrent transactions. Run it with lake exe social.

@[typedb "person"]
structure Person where
  email : Key String
  name : String
  age : Option Int64 := none
  tags : Array String := #[]
  deriving Repr, Entity

structure Employee extends Person where
  company : String
  salary : Decimal
  deriving Repr, Entity

structure Friendship where
  a : Role "friend" (Ref Person)
  b : Role "friend" (Ref Person)
  since : Std.Time.PlainDate
  deriving Repr, Relation

/-- A relation whose `endorsed` player is itself a relation. -/
structure Endorsement where
  endorsed : Role "endorsed" (Ref Friendship)
  endorser : Role "endorser" (Ref Person)
  note : Option String := none
  deriving Repr, Relation

def seed : Tx .write Unit := do
  let ann ← Entity.insert { email := "ann@example.com", name := "Ann", age := some 34, tags := #["lean", "graphs"] : Person }
  let bob ← Entity.insert { email := "bob@example.com", name := "Bob", company := "Acme", salary := 4200 : Employee }
  discard <| Entity.insert { email := "cy@example.com", name := "Cy" : Person }
  let f ← Relation.insert { a := ann, b := bob.up, since := Std.Time.PlainDate.ofYearMonthDayClip 2021 3 14 : Friendship }
  discard <| Relation.insert { endorsed := f, endorser := Ref.key (α := Person) "cy@example.com", note := some "true friends" : Endorsement }

structure FriendOf where
  name : String
  friend : String
  deriving Repr, FromRow

def report : Tx .read Unit := do
  for r in stream (α := FriendOf) typeql!"match $f isa friendship, links (friend: $x, friend: $y);
      $x has name $name; $y has name $friend;" do
    IO.println s!"{r.name} ↔ {r.friend}"

The full program, with its main, migrations and concurrent reads, is in examples/social/Social.lean.

Bank transfer with conflict retry

Balances are exact Decimal values, so no money is lost to floating point. Two concurrent transfers that touch the same account conflict at commit; the retry policy re-runs the whole body, up to five more times, with exponential back-off starting at 20 ms.

@[typedb "account"]
structure Account where
  number : Key String
  owner : String
  balance : Decimal
  deriving Entity

def transfer (src dst : String) (amount : Decimal) : Tx .write Unit := do
  let a := Ref.key (α := Account) src
  let b := Ref.key (α := Account) dst
  let some from_ ← Entity.get? a | Tx.fail (.internal s!"no account {src}")
  let some to_ ← Entity.get? b | Tx.fail (.internal s!"no account {dst}")
  if from_.balance < amount then Tx.fail (.internal s!"{src}: insufficient funds")
  Entity.update a Account.field.balance (from_.balance - amount)
  Entity.update b Account.field.balance (to_.balance + amount)

def run (d : Driver) : EIO TypeDBError Unit := do
  d.ensureDatabase "bank"
  d.runSync "bank" .schema (Schema.defineTx [TypeSpec.of Account])
  d.runSync "bank" .write do
    discard <| Entity.insert { number := "A-1", owner := "Ann", balance := 100 : Account }
    discard <| Entity.insert { number := "B-2", owner := "Bob", balance := 20 : Account }
  d.runSync "bank" .write (retry := .conflicts 5 (.exponential 20)) (transfer "A-1" "B-2" 30)

If transfer fails, nothing it wrote is committed. Account.field.balance is a typed field handle: passing a String to it is a compile error.

Mode-generic helpers

A read helper that takes its mode as a parameter runs in read, write and schema transactions alike. Here a write transaction reuses it before updating.

structure PersonRow where
  name : String
  age : Option Int64
  deriving FromRow

def findByEmail {m : TxMode} [TxMode.Allows m .read] (email : String) : Tx m (Option PersonRow) :=
  query1? typeql!"match $p isa person, has email {email}, has name $name; try \{ $p has age $age; };"

def birthday (email : String) : Tx .write Unit := do
  let r := Ref.key (α := Person) email
  if let some p ← Entity.get? r then
    Entity.update r Person.field.age (p.age.map (· + 1))

def greet (email : String) : Tx .write Unit := do
  if let some p ← findByEmail email then
    IO.println s!"hello, {p.name}"
  birthday email

The email is interpolated with {email}, so an address like x"; delete … is just an odd string, never a second statement.

Streaming and folds

Neither of these holds the whole answer in memory. Rows arrive and decode a batch at a time.

def exportCsv (h : IO.FS.Handle) : Tx .read Nat := do
  let mut n := 0
  for p in stream (α := PersonRow) typeql!"match $p isa person, has name $name; try \{ $p has age $age; };" do
    h.putStrLn s!"{p.name},{(p.age.map toString).getD ""}"
    n := n + 1
  return n

def averageAge : Tx .read Float := do
  let (sum, count) ← queryFold (α := Col "a" Int64) typeql!"match $p isa person, has age $a;" (0, 0)
    fun (s, c) a => (s + a.val.toInt, c + 1)
  return if count == 0 then 0 else Float.ofInt sum / count.toFloat

Fetch documents

A TypeQL fetch returns JSON documents; fetch decodes them into any type with a Lean.FromJson instance.

structure PersonDoc where
  name : String
  email : String
  deriving Lean.FromJson

def docs : Tx .read (Array PersonDoc) :=
  fetch typeql!"match $p isa person; fetch \{ \"name\": $p.name, \"email\": $p.email };"

Polymorphic reads

One query for every person, each decoded into the constructor of its own type.

inductive AnyPerson where
  | person (p : Person)
  | employee (e : Employee)
  deriving Repr, Polymorphic

def people : Tx .read Unit := do
  for (_, p) in ← Polymorphic.all (α := AnyPerson) do
    match p with
    | .person p => IO.println s!"person   {p.name} <{p.email}>"
    | .employee e => IO.println s!"employee {e.name} at {e.company}, salary {e.salary}"

Concurrent transactions

Each Driver.run gets its own thread; one driver multiplexes all of them over its connection.

def dashboard (d : Driver) : EIO TypeDBError Unit := do
  let counts ← (Std.Async.EAsync.concurrentlyAll (#["eu", "us", "apac"].map fun db =>
    d.run db .read (Entity.count (α := Person)))).block
  IO.println s!"people per region: {counts}"

def peopleAndPosts (d : Driver) : EIO TypeDBError Unit := do
  let (people, posts) ← (Std.Async.EAsync.concurrently
    (d.run "social" .read (Entity.all (α := Person)))
    (d.run "social" .read (Entity.all (α := Post)))).block
  IO.println s!"{people.size} people, {posts.size} posts"

Migrations with backfill

Step 1 defines the ORM schema, step 2 adds an attribute in TypeQL, and step 3 is Lean code that backfills data. A schema transaction may also write, so step 3's change and its history record commit together or not at all.

def migrations : List Migration := [
  Migration.define 1 "social schema" schema,
  Migration.tql 2 "nicknames"
    "define attribute nickname, value string; person owns nickname;"
    (down := some "undefine owns nickname from person; nickname;"),
  { version := 3, name := "default nickname", fingerprint := "nickname := name, v1"
    up := exec typeql!"match $p isa person, has name $n; not \{ $p has nickname $k; }; insert $p has nickname == $n;"
    down := .step (exec typeql!"match $p isa person, has nickname $k; delete has $k of $p;") }
]

def main : IO Unit := do
  let r ← (Driver.withDriver { address := "127.0.0.1:1729" } fun d => do
    d.ensureDatabase "social"
    let applied ← d.migrateSync "social" migrations
    IO.println s!"applied: {applied.steps.toList}"
    d.runSync "social" .read (Schema.verify! schema)
    : EIO TypeDBError Unit).toBaseIO
  if let .error e := r then IO.eprintln e

What does not compile

These checks are part of the test suite (tests/CompileChecks.lean), with the compiler's exact messages pinned.

-- An insert in a read transaction
example : Tx .read Unit := exec typeql!"match $p isa person; insert $p has name \"x\";"
error: failed to synthesize instance of type class
  TxMode.read.Allows TxMode.write
-- Interpolating a value that has no TypeQL rendering
structure NotRenderable where
  x : Nat

example (v : NotRenderable) : Query .read := typeql!"match $p has id {v};"
error: failed to synthesize instance of type class
  ToTypeQL NotRenderable

And the other direction compiles, because weaker modes lift into stronger ones:

example : Tx .write (Array (Col "n" String)) := query typeql!"match $p isa person, has name $n;"
example : Tx .schema Unit := do
  exec typeql!"define entity person;"
  exec typeql!"insert $p isa person;"