Mix.install([
{:typedb, "~> 0.2"},
{:kino, "~> 0.14"}
])A server to talk to
This notebook needs a TypeDB 3.12 or newer on localhost:8000. If you have
Docker:
docker run --rm -p 8000:8000 -p 1729:1729 typedb/typedb:3.12.1Everything below creates its own database and drops it at the end, so it is safe against a scratch server — and only a scratch server.
Connecting
A connection is a supervised process with a registered name. In an application it goes in your supervision tree; here, starting it directly is enough.
{:ok, _pid} =
TypeDB.start_link(
name: :notebook,
url: "http://localhost:8000",
username: "admin",
password: "password"
)
conn = :notebook
TypeDB.version!(conn)A database and a schema
database = "livebook_#{System.unique_integer([:positive])}"
:ok = TypeDB.Database.create(conn, database)
schema = """
define
attribute name, value string;
attribute age, value integer;
attribute since, value datetime;
entity person,
owns name @key,
owns age,
plays friendship:friend;
relation friendship,
relates friend @card(2..2),
owns since;
"""
TypeDB.query!(conn, database, schema)query/4 runs each query in a transaction of its own. This one left the default
transaction_type: :schema, which is right for define — and wrong for
everything else, because it takes a database-wide lock. Every query below says
what it is.
Writing
TypeDB.query!(
conn,
database,
"""
insert
$alice isa person, has name "Alice", has age 31;
$bob isa person, has name "Bob", has age 27;
""",
transaction_type: :write
)Reading
An answer is enumerable, and a row knows how to turn a variable into a native Elixir term.
conn
|> TypeDB.query!(database, "match $p isa person, has name $name, has age $age;",
transaction_type: :read
)
|> Enum.map(fn row ->
%{
name: TypeDB.ConceptRow.typed_value(row, "name"),
age: TypeDB.ConceptRow.typed_value(row, "age")
}
end)
|> Kino.DataTable.new()Or straight into a struct of your own:
defmodule Person do
defstruct [:name, :age]
end
conn
|> TypeDB.query!(database, "match $p isa person, has name $name, has age $age;",
transaction_type: :read
)
|> Enum.map(&TypeDB.ConceptRow.to_struct(&1, Person))to_struct/2 raises for a variable that names no field, which is the failure you
want — Kernel.struct/2 on a string-keyed map silently hands back a struct full
of nils.
Parameterised queries
Never interpolate user input into TypeQL. TypeDB's given stage takes values as
data, and :given_rows encodes them in the tagged wire form, so a value
containing a quote is a value rather than a parse error.
dangerous = ~S|Robert"); delete $p isa person; #|
conn
|> TypeDB.query!(
database,
"""
given $n: string;
match $p isa person, has name == $n;
""",
transaction_type: :read,
given_rows: [%{"n" => dangerous}]
)
|> Enum.count()Zero rows, no error, and everybody still in the database. Try the same query with the name interpolated into the string and watch TypeDB reject it as a syntax error — which is the good outcome, and not one to rely on.
Several statements, one commit
TypeDB.transaction(conn, database, :write, fn tx ->
TypeDB.Transaction.query!(tx, ~s|insert $p isa person, has name "Carol", has age 44;|)
TypeDB.Transaction.query!(tx, """
match
$a isa person, has name "Alice";
$c isa person, has name "Carol";
insert
(friend: $a, friend: $c) isa friendship, has since 2024-01-01T00:00:00;
""")
end)The block commits on success and abandons on failure — an {:error, _}, a
raise, a throw or an exit.
conn
|> TypeDB.query!(
database,
"""
match
$f isa friendship (friend: $a, friend: $b), has since $when;
$a has name $an;
$b has name $bn;
select $an, $bn, $when;
""",
transaction_type: :read
)
|> Enum.map(&TypeDB.ConceptRow.to_map/1)When it goes wrong
TypeDB.query(conn, database, "match $p isa unicorn;", transaction_type: :read){:error, %TypeDB.Error{kind: :server, code: "INF2", status: 400}}. Branch on
:kind and :code; the message is TypeDB's and it changes.
{:error, error} = TypeDB.Database.get(conn, "no_such_database")
{error.kind, error.code, error.status, TypeDB.Error.retryable?(error)}retryable?/1 answers whether retrying could help — for a job you would requeue
or a transaction you would re-run. A missing database is not that.
Watching it work
TypeDB.Telemetry.attach_default_logger(:info)
TypeDB.query!(conn, database, "match $p isa person;", transaction_type: :read)
TypeDB.Telemetry.detach_default_logger()One line per operation, with the route, the duration and how many HTTP requests
it took. TypeDB.Telemetry has the three levels of span and what to graph.
Tidying up
:ok = TypeDB.Database.delete(conn, database)
TypeDB.stop(:notebook)