Skip to main content

Usage

Creating a Table to store data

This is analogous to a database table. To create a table call datalog.newTable<T>(schema: Schema). Bonus points for adding types to your schema.

import * as datalog from '@datalogui/datalog'

const People = datalog.newTable<{id: number, name: string}>({
id: datalog.NumberType,
name: datalog.StringType,
})

Adding data

Add data to the table with table.assert(datum: T).

People.assert({id: 0, name: "Alice"})
People.assert({id: 1, name: "Bob"})

Removing data

Remove data from the table with table.retract(datum: T).

People.retract({id: 1, name: "Bob"})

Querying data

Query data from the table with the datalog.query function:


// Builds the structure of the query
const Query = datalog.query<{name: string}>(({name}) => {
People({name})
})

// Read the values of the query by creating a view into it
Query.view().readAllData()
// => [{ name: "Alice" }]

Querying data – With Joins

Let's try a more complex example. How about listing every person's name and their manager's name.

// ... People table is same as before

type ID = number
const Manages = datalog.newTable<{manager: ID, managee: ID}>({
manager: datalog.NumberType,
managee: datalog.NumberType,
})

// Find everyone who has a manager, and return their name and their manager's name
datalog.query<{
managerName: string,
personName: string,
personID: number,
managerID: number
}>(({managerName, personName, managerID, personID}) => {
People({id: personID, name: personName})
Manages({managee: personID, manager: managerID})
People({id: managerID, name: managerName})
})

Query.view().readAllData()
// =>
/*
[{
managerID: 0,
managerName: "Alice",
personID: 1,
personName: "Bob",
}]
*/

Querying data with Anti-Joins

It's also possible to say that something should not exist. This is declared with the .not method on tables inside queries. For example, let's find out who does not have a manager.

const Query = datalog.query<{ personID: number, personName: string }>(
({ personName, personID }) => {
People({ id: personID })
// Here we say: Only find personID for which this condition does `not`
// hold. i.e. Find a personID such that there is
// not a {managee: personID} datum inside the Manages table.
Manages.not({ managee: personID })
})

Query.view().readAllData()
/*
[{
personID: 0,
personName: "Alice",
}]
*/

Differential Updates

This Datalog implementation works off of differences. When you run a query, it doesn't run the query over the whole dataset every time. It only runs the query on new data. Let's see an example

// Create our People table as before
const People = datalog.newTable<{ id: number, name: string }>({
id: datalog.NumberType,
name: datalog.StringType,
})

People.assert({ id: 0, name: "Alice" })
const Query = datalog.query<{ name: string }>(({ name }) => {
People({ name })
})

const queryView = Query.view()

// Ask only for recent data
queryView.recentData()
// =>
/*
[{
kind: datalog.Added,
datum: { name: "Alice" }
}]
*/

// Nothing has changed so we get nothing back
queryView.recentData()
// => null

recentData returns either null or any changes that happened on this view since the last time you called recentData. Changes are either an addition of a datum (represented by the datalog.Added symbol) or a removal of a datum (represented by the datalog.Removed symbol).

Let's continue the example:

People.assert({ id: 2, name: "Eve" })
queryView.recentData()
// => null

Huh? What gives? Why did we get back null? It's because queries are a kind of lazy. Much like me, they won't do anything unless you ask them to. In this case, we have to ask it to run the query again because we know the dependencies (The People table) have changed. Let's try that again.

// People.assert({ id: 2, name: "Eve" }) // We did this already
// Hey Query! go ahead and run yourself
Query.runQuery()
queryView.recentData()
/* =>
[{
kind: datalog.Added,
datum: { name: "Eve" }
}]
*/

That's better. We see the effect of our assertion in the People table reflected in the output of the query. But remembering to call runQuery() every time can be annoying. Thankfully we can add a small change to have the query automatically run when any of its dependencies change.

Query.onDependencyChange(() => Query.runQuery())

Now what happens when we remove something from the People table?

People.retract({ id: 2, name: "Eve" })
// Note: runQuery is omitted here because it's happening
// automatically because we ran
// Query.onDependencyChange(() => Query.runQuery())

queryView.recentData()
// =>
/*
[{
kind: datalog.Removed,
datum: { name: "Eve" }
}]
*/

UIs don't change the whole state of the world very often. So it's much faster to run queries on differences rather than the full dataset every time. With DatalogUI, you get expressive query syntax and good performance.