Repository: edgedb/edgedb

Issue:
Can't select link property via backlink

- EdgeDB Version: 2.7+a86e274
- EdgeDB CLI Version: 2.2.6+7eabbf9
- OS Version: MacOS 13.0.1 (22A400)

When trying to select a link property via a backlink the following error is returned:

`InternalServerError: column actors~1.25ee9fda-6af9-11ed-af36-2b332b67fd94 does not exist
Hint: This is most likely a bug in EdgeDB. Please consider opening an issue ticket at https://github.com/edgedb/edgedb/issues/new?template=bug_report.md`

Steps to Reproduce:

1. Run this query on the EdgeDB example database:
`
select Person {
    name,
    roles := .<actors[is Movie] {
        title,
        @character_name
    }
}
filter .name = "Samuel L. Jackson"
`

Using EdgeDB Example DB Schema



Discussion:
The bug has to do with inheritance. Running
```
select Person { name, roles := .<actors[is Content] { title, @character_name } } 
```
works fine.

The issue is that for the backlink traversal in the original query, we are joining in the link table view for `Content.actors` (which is correct (since we still join it against Movie), though suboptimal) but then try to read the column name for the `Movie` link property (which has a different name for `Movie` than for `Content`, since it is the id).
The somewhat unfortunate
```
select Person { name, roles := (select .<actors[is Content] { title, @character_name } filter Person.<actors[is Content] is Movie) } 
```
works for now, I guess

Resolving patch:
diff --git a/edb/edgeql/compiler/setgen.py b/edb/edgeql/compiler/setgen.py
--- a/edb/edgeql/compiler/setgen.py
+++ b/edb/edgeql/compiler/setgen.py
@@ -613,7 +613,7 @@ def ptr_step_set(
         source_context: Optional[parsing.ParserContext],
         ignore_computable: bool=False,
         ctx: context.ContextLevel) -> irast.Set:
-    ptrcls = resolve_ptr(
+    ptrcls, path_id_ptrcls = resolve_ptr_with_intersections(
         source,
         ptr_name,
         upcoming_intersections=upcoming_intersections,
@@ -624,6 +624,7 @@ def ptr_step_set(
 
     return extend_path(
         path_tip, ptrcls, direction,
+        path_id_ptrcls=path_id_ptrcls,
         ignore_computable=ignore_computable, ctx=ctx)
 
 
@@ -646,6 +647,23 @@ def _add_target_schema_refs(
 
 
 def resolve_ptr(
+    near_endpoint: s_obj.Object,
+    pointer_name: str,
+    *,
+    direction: s_pointers.PointerDirection = (
+        s_pointers.PointerDirection.Outbound
+    ),
+    source_context: Optional[parsing.ParserContext] = None,
+    track_ref: Optional[Union[qlast.Base, Literal[False]]],
+    ctx: context.ContextLevel,
+) -> s_pointers.Pointer:
+    return resolve_ptr_with_intersections(
+        near_endpoint, pointer_name,
+        direction=direction, source_context=source_context,
+        track_ref=track_ref, ctx=ctx)[0]
+
+
+def resolve_ptr_with_intersections(
     near_endpoint: s_obj.Object,
     pointer_name: str,
     *,
@@ -657,7 +675,15 @@ def resolve_ptr(
     source_context: Optional[parsing.ParserContext] = None,
     track_ref: Optional[Union[qlast.Base, Literal[False]]],
     ctx: context.ContextLevel,
-) -> s_pointers.Pointer:
+) -> tuple[s_pointers.Pointer, s_pointers.Pointer]:
+    """Resolve a pointer, taking into account upcoming intersections.
+
+    The key trickiness here is that *two* pointers are returned:
+      * one that (for backlinks) includes just the pointers that actually
+        may be used
+      * one for use in path ids, that does not do that filtering, so that
+        path factoring works properly.
+    """
 
     if not isinstance(near_endpoint, s_sources.Source):
         # Reference to a property on non-object
@@ -667,7 +693,7 @@ def resolve_ptr(
     ptr: Optional[s_pointers.Pointer] = None
 
     if direction is s_pointers.PointerDirection.Outbound:
-        ptr = near_endpoint.maybe_get_ptr(
+        path_id_ptr = ptr = near_endpoint.maybe_get_ptr(
             ctx.env.schema,
             s_name.UnqualName(pointer_name),
         )
@@ -685,21 +711,25 @@ def resolve_ptr(
         if ptrs:
             # If this reverse pointer access is followed by
             # intersections, we filter out any pointers that
-            # couldn't be picked up by the intersections. This avoids
-            # creating spurious dependencies when reverse
-            # links are used in schemas.
-            dep_ptrs = {
-                ptr for ptr in ptrs
-                if (src := ptr.get_source(ctx.env.schema))
-                and all(
-                    src.issubclass(ctx.env.schema, typ)
-                    or any(
-                        dsrc.issubclass(ctx.env.schema, typ)
-                        for dsrc in src.descendants(ctx.env.schema)
-                    )
-                    for typ in upcoming_intersections
-                )
-            }
+            # couldn't be picked up by the intersections.
+            # If a pointer doesn't get picked up, we look to see
+            # if any of its children might.
+            #
+            # This both allows us to avoid creating spurious
+            # dependencies when reverse links are used in schemas
+            # and to generate a precise set of possible pointers.
+            dep_ptrs = set()
+            wl = list(ptrs)
+            while wl:
+                ptr = wl.pop()
+                if (src := ptr.get_source(ctx.env.schema)):
+                    if all(
+                        src.issubclass(ctx.env.schema, typ)
+                        for typ in upcoming_intersections
+                    ):
+                        dep_ptrs.add(ptr)
+                    else:
+                        wl.extend(ptr.children(ctx.env.schema))
 
             if track_ref is not False:
                 for p in dep_ptrs:
@@ -729,7 +759,7 @@ def resolve_ptr(
                     )
 
             opaque = not far_endpoints
-            ptr = schemactx.get_union_pointer(
+            concrete_ptr = schemactx.get_union_pointer(
                 ptrname=s_name.UnqualName(pointer_name),
                 source=near_endpoint,
                 direction=direction,
@@ -738,9 +768,25 @@ def resolve_ptr(
                 modname=ctx.derived_target_module,
                 ctx=ctx,
             )
+            path_id_ptr = ptr = concrete_ptr
+            # If we have an upcoming intersection that has actual
+            # pointer targets, we want to put the filtered down
+            # version into the AST, so that we can more easily use
+            # that information in compilation.  But we still need the
+            # *full* union in the path_ids, for factoring.
+            if dep_ptrs and upcoming_intersections:
+                ptr = schemactx.get_union_pointer(
+                    ptrname=s_name.UnqualName(pointer_name),
+                    source=near_endpoint,
+                    direction=direction,
+                    components=dep_ptrs,
+                    opaque=opaque,
+                    modname=ctx.derived_target_module,
+                    ctx=ctx,
+                )
 
-    if ptr is not None:
-        return ptr
+    if ptr and path_id_ptr:
+        return ptr, path_id_ptr
 
     if isinstance(near_endpoint, s_links.Link):
         vname = near_endpoint.get_verbosename(ctx.env.schema, with_parent=True)
@@ -775,6 +821,7 @@ def extend_path(
     ptrcls: s_pointers.Pointer,
     direction: PtrDir = PtrDir.Outbound,
     *,
+    path_id_ptrcls: Optional[s_pointers.Pointer] = None,
     ignore_computable: bool = False,
     same_computable_scope: bool = False,
     srcctx: Optional[parsing.ParserContext]=None,
@@ -798,14 +845,22 @@ def extend_path(
 
     orig_ptrcls = ptrcls
 
+    # If there is a particular specified ptrcls for the pathid, use
+    # it, otherwise use the actual ptrcls. This comes up with
+    # intersections on backlinks, where we want to use a precise ptr
+    # in the IR for compilation reasons but need a path_id that is
+    # independent of intersections.
+    path_id_ptrcls = path_id_ptrcls or ptrcls
+
     # Find the pointer definition site.
     # This makes it so that views don't change path ids unless they are
     # introducing some computation.
     ptrcls = ptrcls.get_nearest_defined(ctx.env.schema)
+    path_id_ptrcls = path_id_ptrcls.get_nearest_defined(ctx.env.schema)
 
     path_id = pathctx.extend_path_id(
         src_path_id,
-        ptrcls=ptrcls,
+        ptrcls=path_id_ptrcls,
         direction=direction,
         ns=ctx.path_id_namespace,
         ctx=ctx,
@@ -820,7 +875,7 @@ def extend_path(
         source=source_set,
         target=target_set,
         direction=direction,
-        ptrref=not_none(path_id.rptr()),
+        ptrref=typegen.ptr_to_ptrref(ptrcls, ctx=ctx),
         is_definition=False,
     )
 
@@ -1051,6 +1106,9 @@ def type_intersection_set(
             elif stype.issubclass(ctx.env.schema, component_endpoint):
                 assert isinstance(stype, s_objtypes.ObjectType)
                 if rptr.direction is s_pointers.PointerDirection.Inbound:
+                    # assert isinstance(component, irast.PointerRef)
+                    # rptr_specialization.append(component)
+
                     narrow_ptr = stype.getptr(
                         ctx.env.schema,
                         component.shortname.get_local_name(),
diff --git a/edb/edgeql/compiler/typegen.py b/edb/edgeql/compiler/typegen.py
--- a/edb/edgeql/compiler/typegen.py
+++ b/edb/edgeql/compiler/typegen.py
@@ -210,6 +210,18 @@ def ptrcls_from_ptrref(  # NoQA: F811
     return ptr
 
 
+def ptr_to_ptrref(
+    ptrcls: s_pointers.Pointer, *,
+    ctx: context.ContextLevel,
+) -> irast.BasePointerRef:
+    return irtyputils.ptrref_from_ptrcls(
+        schema=ctx.env.schema,
+        ptrcls=ptrcls,
+        cache=ctx.env.ptr_ref_cache,
+        typeref_cache=ctx.env.type_ref_cache,
+    )
+
+
 def collapse_type_intersection_rptr(
     ir_set: irast.Set, *,
     ctx: context.ContextLevel,


Test patch:
diff --git a/tests/test_edgeql_linkprops.py b/tests/test_edgeql_linkprops.py
--- a/tests/test_edgeql_linkprops.py
+++ b/tests/test_edgeql_linkprops.py
@@ -1284,6 +1284,22 @@ async def test_edgeql_props_back_07(self):
                 '''
             )
 
+    async def test_edgeql_props_back_08(self):
+        await self.assert_query_result(
+            r'''
+            select Card { name, z := .<deck[IS Bot] { name, x := @count }}
+            filter .name = 'Dragon';
+            ''',
+            [
+                {
+                    "name": "Dragon",
+                    "z": tb.bag([
+                        {"x": 1, "name": "Dave"},
+                    ])
+                }
+            ]
+        )
+
     async def test_edgeql_props_intersect_01(self):
         await self.assert_query_result(
             r'''