Search
ctrl/
Ask AI
Light
Dark
System

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.

Copy
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.

Copy
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.

Copy
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.

Copy
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 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.

Copy
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.

Copy
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.

Copy
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.

Copy
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.

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.