Path scoping
Beginning with EdgeDB 6.0, we are phasing out our historical (and somewhat notorious) “path scoping” algorithm in favor of a much simpler algorithm that nevertheless behaves identically on most idiomatic EdgeQL queries.
EdgeDB 6.0 will contain features to support migration to and testing of the new semantics. We expect the migration to be relatively painless for most users.
Discussion of rationale for this change is available in the RFC.
Legacy path scoping
This section describes the path scoping algorithm used exclusively until EdgeDB 5.0 and by default in EdgeDB 6.0. It will be removed in EdgeDB 7.0.
Element-wise operations with multiple arguments in EdgeDB are generally applied to the cartesian product of all the input sets.
db>
select {'aaa', 'bbb'} ++ {'ccc', 'ddd'};
{'aaaccc', 'aaaddd', 'bbbccc', 'bbbddd'}
However, in cases where multiple element-wise arguments share a common path
(User.
in this example), EdgeDB factors out the common path rather than
using cartesian multiplication.
db>
select User.first_name ++ ' ' ++ User.last_name;
{'Mina Murray', 'Jonathan Harker', 'Lucy Westenra', 'John Seward'}
We assume this is what you want, but if your goal is to get the cartesian
product, you can accomplish it one of three ways. You could use
detached
.
edgedb>
select User.first_name ++ ' ' ++ detached User.last_name;
{ 'Mina Murray', 'Mina Harker', 'Mina Westenra', 'Mina Seward', 'Jonathan Murray', 'Jonathan Harker', 'Jonathan Westenra', 'Jonathan Seward', 'Lucy Murray', 'Lucy Harker', 'Lucy Westenra', 'Lucy Seward', 'John Murray', 'John Harker', 'John Westenra', 'John Seward', }
You could use with to attach a different symbol to
your set of User
objects.
edgedb> .......
with U := User
select U.first_name ++ ' ' ++ User.last_name;
{ 'Mina Murray', 'Mina Harker', 'Mina Westenra', 'Mina Seward', 'Jonathan Murray', 'Jonathan Harker', 'Jonathan Westenra', 'Jonathan Seward', 'Lucy Murray', 'Lucy Harker', 'Lucy Westenra', 'Lucy Seward', 'John Murray', 'John Harker', 'John Westenra', 'John Seward', }
Or you could leverage the effect scopes have on path resolution. More on that in the Scopes section.
The reason with
works here even though the alias U
refers to the exact
same set is that we only assume you want the path factored in this way when you
use the same symbol to refer to a set. This means operations with
User.first_name
and User.last_name
do get the common path factored
while U.first_name
and User.last_name
do not and are resolved with
cartesian multiplication.
That may leave you still wondering why U
and User
did not get a common
path factored. U
is just an alias of select User
and User
is the
same symbol that we use in our name query. That’s true, but EdgeDB doesn’t
factor in this case because of the queries’ scopes.
Scopes
Scopes change the way path resolution works. Two sibling select queries — that is, queries at the same level — do not have their paths factored even when they use a common symbol.
edgedb>
select ((select User.first_name), (select User.last_name));
{ ('Mina', 'Murray'), ('Mina', 'Harker'), ('Mina', 'Westenra'), ('Mina', 'Seward'), ('Jonathan', 'Murray'), ('Jonathan', 'Harker'), ('Jonathan', 'Westenra'), ('Jonathan', 'Seward'), ('Lucy', 'Murray'), ('Lucy', 'Harker'), ('Lucy', 'Westenra'), ('Lucy', 'Seward'), ('John', 'Murray'), ('John', 'Harker'), ('John', 'Westenra'), ('John', 'Seward'), }
Common symbols in nested scopes are factored when they use the same symbol.
In this example, the nested queries both use the same User
symbol as the
top-level query. As a result, the User
in those queries refers to a single
object because it has been factored.
edgedb> ....... .......
select User {
name:= (select User.first_name) ++ ' ' ++ (select User.last_name)
};
{ default::User {name: 'Mina Murray'}, default::User {name: 'Jonathan Harker'}, default::User {name: 'Lucy Westenra'}, default::User {name: 'John Seward'}, }
If you have two common scopes and only one of them is in a nested scope, the paths are still factored.
edgedb>
select (Person.name, count(Person.friends));
{('Fran', 3), ('Bam', 2), ('Emma', 3), ('Geoff', 1), ('Tyra', 1)}
In this example, count
, like all aggregate function, creates a nested
scope, but this doesn’t prevent the paths from being factored as you can see
from the results. If the paths were not factored, the friend count would be
the same for all the result tuples and it would reflect the total number of
Person
objects that are in all
friends
links rather than the number
of Person
objects that are in the named Person
object’s friends
link.
If you have two aggregate functions creating sibling nested scopes, the paths are not factored.
edgedb>
select (array_agg(distinct Person.name), count(Person.friends));
{(['Fran', 'Bam', 'Emma', 'Geoff'], 3)}
This query selects a tuple containing two nested scopes. Here, EdgeDB assumes you want an array of all unique names and a count of the total number of people who are anyone’s friend.
Clauses & Nesting
Most clauses are nested and are subjected to the same rules described above: common symbols are factored and assumed to refer to the same object as the outer query. This is because clauses like filter and order by need to be applied to each value in the result.
The offset and limit clauses are not nested in the scope because they need to be applied globally to the entire result set of your query.