Global, session, and query variables, oh my!

Handling variable conflicts in Triplit queries

There’s a few places where you can define variables in a Triplit query. You can pass global variables to the database constructor:

const db = new DB({ variables: { name: 'Philip J. Fry' } });
const query = db.query('employees').where('name', '=', '$name');

You can create a session on the database and pass in session variables:

const sessionDB = db.withSessionVars({ name: 'Turanga Leela' });
const query = sessionDB.query('employees').where('name', '=', '$name');

You can add specific query variables to a query:

const query = db.query('employees').vars({ name: 'Bender Bending Rodriguez' });
const filteredQuery = db.query('employees').where('name', '=', '$name');

And finally when using subqueries and relationships, data from other parts of the query have been accessible via variables:

const schema = {
  collections: {
    employees: {
      schema: S.Schema({
        id: S.Id(),
        name: S.String(),
        department_id: S.String(),
        department: S.RelationById('departments', '$department_id'),
      }),
    },
    departments: {
      schema: S.Schema({
        id: S.Id(),
        name: S.String(),
        members: S.RelationMany('employees', {
          where: [['department_id', '=', '$id']],
        }),
        head_id: S.String(),
        head: S.RelationById('employees', '$head_id'),
      }),
      rules: {
        read: {
          'head-in-dept': {
            filter: [['head.department_id', '=', '$id']],
          },
        },
      },
    },
  },
};

One issue has been that conflicting variable names will overwrite each other. If you took the examples above and combined them together, the query variable would arbitrarily win out and you would not have access to the global or session variable values:

const db = new DB({ variables: { name: 'Philip J. Fry' } });
const sessionDB = db.withSessionVars({ name: 'Turanga Leela' });
const query = sessionDB
  .query('employees')
  .vars({ name: 'Bender Bending Rodriguez' });

// $name is arbitrarily assigned to 'Bender Bending Rodriguez'
const filteredQuery = query.where('name', '=', '$name');

This conflicting variable problem is even more prominent with the use of subqueries and relationships adding variables to the query. However, variables are now accessible with a scope prefix of global , session , or query to avoid collisions. Going back to our example:

const db = new DB({ variables: { name: 'Philip J. Fry' } });
const sessionDB = db.withSessionVars({ name: 'Turanga Leela' });
const query = sessionDB
  .query('employees')
  .vars({ name: 'Bender Bending Rodriguez' });

// using global variable
const queryUsingGlobal = query.where('name', '=', '$global.name'); // Philip J. Fry
// using session variable
const queryUsingSession = query.where('name', '=', '$session.name'); // Turanga Leela
// using query variable
const queryUsingQuery = query.where('name', '=', '$query.name'); // Bender Bending Rodriguez

When using subqueries, a numerical prefix is used to reference the current entity or parent parts of the query. So 0 refers to the current entity, 1 refers to the parent, 2 refers to the grandparent, etc. Adding to the schema above:

const schema = {
  collections: {
    employees: {
      schema: S.Schema({
        id: S.Id(),
        name: S.String(),
        department_id: S.String(),
        // this is a query on 'departments', references the parent 'employees.department_id'
        department: S.RelationById('departments', '$1.department_id'),
      }),
    },
    departments: {
      schema: S.Schema({
        id: S.Id(),
        name: S.String(),
        // this is a query on 'employees', references the parent 'departments.id'
        members: S.RelationMany('employees', {
          where: [['department_id', '=', '$1.id']],
        }),
        head_id: S.String(),
        // this is a query on 'employees', references the parent 'departments.head_id'
        head: S.RelationById('employees', '$1.head_id'),
      }),
      rules: {
        write: {
          'head-in-dept': {
            // this is a rule filter on 'departments', references the current 'departments.id'
            filter: [['head.department_id', '=', '$0.id']],
          },
        },
      },
    },
  },
};

In our experience, you usually only need to use the 0 and 1 prefixes unless you’re writing a complicated query. With proper prefixing variable conflicts are completely avoidable. Although for backwards compatibility non-scoped variables are still accessible, we recommend using proper prefixing.

Nested paths in variables

Variables now support nested data which can be accessed with a . separated path. For example:

const query = db.query('employees').vars({ nested: { name: 'Dr. Zoidberg' } });
const filteredQuery = query.where('name', '=', '$query.nested.name');

When using subqueries and relationships, paths will also traverse through data.

// Query employees where the employee name == department name
const query = db.query('employees').where('name', '=', '$0.department.name');

Open-source integration with Excalidraw

We’re big fans of Excalidraw, and an integration for it was one of the first demos we built. Today we open-sourced it to show off how little code it takes to get an existing project migrated over to Triplit. It also adds support for multiple “pages” of work to keep your drawings organized. Check it out here.