Skip to main content

Cross-language Queries

DortDB's defining feature is that a single query can use more than one language. Because every language compiles to the same unified algebra, a nested language block is not an opaque call; it becomes part of one plan that is optimized and executed as a whole. That is what lets the optimizer, for example, push a filter from an outer SQL query down into an inner Cypher subquery.

To use more than one language, load them all on the engine:

const db = new DortDB({
mainLang: SQL(),
additionalLangs: [Cypher({ defaultGraph: 'social' }), XQuery()],
optimizer: { rules: defaultRules },
});

Switching languages with LANG

Inside a query, a LANG <name> block switches to another language. A block can appear anywhere a subquery or atomic expression is allowed, and it ends when its enclosing scope ends, typically at a closing parenthesis. You can also end a block explicitly with LANG EXIT.

SELECT people.name AS name, friends.cnt AS friend_count
FROM people
JOIN (
LANG cypher
MATCH (p:Person)-[:KNOWS]->(f)
RETURN p.id AS pid, count(f) AS cnt
) AS friends ON people.id = friends.pid

Here SQL is the outer language and a Cypher block computes a friend count per person; the result is joined back like any other subquery.

The outer language can just as easily be Cypher or XQuery. This Cypher query unwraps rows produced by an inner SQL query:

UNWIND (
LANG sql
SELECT id FROM people ORDER BY name
) AS pid
RETURN pid

It is also possible to specify the top-level language like so:

LANG sql
SELECT id FROM people ORDER BY name

Referencing the surrounding scope

An inner block can read values from the language scopes that surround it, which makes correlated subqueries across languages possible. This SQL query keeps only the people who have more than one friend in the graph, using the outer people.id inside the Cypher block:

SELECT people.name AS name
FROM people
WHERE (
LANG cypher
MATCH (p:Person {id: people.id})-[:KNOWS]->(f)
RETURN count(f)
) > 1

The nonlocal prefix (SQL)

In SQL, an unqualified column is assumed to belong to the current FROM table. When SQL is embedded in a language that does not prefix identifiers (such as Cypher), that assumption gets in the way, since you may need to refer to a value from an outer scope rather than the current table. Prefix the column with the nonlocal schema to do so; DortDB then resolves it against the nearest outer definition instead of the current table. See SQL restrictions.

Tuples vs. items

The unified model has two kinds of data, and it determines what a nested block gives back to its host:

  • Tuples are rows with named attributes (relational-style). A block that produces tuples can be treated as a table: joined, unwrapped, and so on.
  • Items are single opaque values (or sequences of them). A block that produces items behaves like a scalar or a list.

This is why an embedded SQL or Cypher block can stand in for a table, while an XQuery block yields a sequence of items rather than rows, and why a single-column, single-row subquery can be used where a scalar is expected. The formal treatment is in Object Representation.

Nesting depth

Language blocks nest to any depth, and each inner block can reference values from any of the scopes above it. The whole nested structure lowers to one operator tree, so cross-language optimization applies across every boundary rather than stopping at it.