Repository: edgedb/edgedb

Issue:
Make behavior of explicit computable links consistent with shapes
<!-- Please search existing issues to avoid creating duplicates. -->
- EdgeDB Version: beta2

Steps to Reproduce:

Currently the following schema treats `User.tweets` as detached in queries:

```ts
module default {
  type User {
    property name -> str;
    multi link tweets := User.<author[IS Tweet];
    # treated differently than:
    # multi link tweets := .<author[IS Tweet];
  }
  type Tweet {
    link author -> User;
  }
}
```

Moreover, switching between the explicit and implicit "User" is treated as a schema change by `edgedb create-migration`.

These should be treated as equivalent for the sake of consistency with how the dot shortcut works inside query shapes: https://edgedb.slack.com/archives/C09QSK72B/p1621902198005900



Discussion:
For clarity, references to the source type in schema computables should be replaced with `__source__`, unless the expression is inside `DETACHED`.  This probably needs an AST visitor akin to `compiler/normalization.py`.

Resolving patch:
diff --git a/edb/edgeql/compiler/context.py b/edb/edgeql/compiler/context.py
--- a/edb/edgeql/compiler/context.py
+++ b/edb/edgeql/compiler/context.py
@@ -658,6 +658,7 @@ def __init__(
                 self.anchors = prevlevel.anchors.copy()
                 self.modaliases = prevlevel.modaliases.copy()
                 self.aliased_views = collections.ChainMap()
+                self.view_map = collections.ChainMap()
                 self.class_view_overrides = {}
                 self.expr_exposed = prevlevel.expr_exposed
 
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
@@ -1282,8 +1282,13 @@ def computable_ptr_set(
     newctx: Callable[[], ContextManager[context.ContextLevel]]
 
     if qlctx is None:
-        # Schema-level computable, completely detached context
-        newctx = ctx.detached
+        # Schema-level computed link or property, the context should
+        # still have a source.
+        newctx = _get_schema_computed_ctx(
+            rptr=rptr,
+            source=source_set,
+            ctx=ctx)
+
     else:
         newctx = _get_computable_ctx(
             rptr=rptr,
@@ -1341,6 +1346,38 @@ def computable_ptr_set(
     return comp_ir_set
 
 
+def _get_schema_computed_ctx(
+    *,
+    rptr: irast.Pointer,
+    source: irast.Set,
+    ctx: context.ContextLevel
+) -> Callable[[], ContextManager[context.ContextLevel]]:
+
+    @contextlib.contextmanager
+    def newctx() -> Iterator[context.ContextLevel]:
+        with ctx.detached() as subctx:
+            source_stype = get_set_type(source, ctx=ctx)
+
+            source_scope = pathctx.get_set_scope(rptr.source, ctx=ctx)
+            if source_scope and source_scope.namespaces:
+                subctx.path_id_namespace |= source_scope.namespaces
+
+            inner_path_id = pathctx.get_path_id(source_stype, ctx=subctx)
+
+            remapped_source = new_set_from_set(
+                rptr.source,
+                rptr=rptr.source.rptr,
+                preserve_scope_ns=True,
+                ctx=ctx,
+            )
+            key = inner_path_id.strip_namespace(inner_path_id.namespace)
+            subctx.view_map[key] = ((inner_path_id, remapped_source),)
+
+            yield subctx
+
+    return newctx
+
+
 def _get_computable_ctx(
     *,
     rptr: irast.Pointer,


Test patch:
diff --git a/tests/test_edgeql_data_migration.py b/tests/test_edgeql_data_migration.py
--- a/tests/test_edgeql_data_migration.py
+++ b/tests/test_edgeql_data_migration.py
@@ -2388,7 +2388,7 @@ async def test_edgeql_migration_describe_type_rename_01(self):
 
         await self.fast_forward_describe_migration()
 
-    async def test_edgeql_migration_computable_01(self):
+    async def test_edgeql_migration_computed_01(self):
         await self.migrate(r'''
             type Foo {
                 property val -> str;
@@ -2433,7 +2433,7 @@ async def test_edgeql_migration_computable_01(self):
             }],
         )
 
-    async def test_edgeql_migration_computable_02(self):
+    async def test_edgeql_migration_computed_02(self):
         await self.migrate(r'''
             type Foo { property foo := '1' };
             type Bar extending Foo;
@@ -2444,6 +2444,710 @@ async def test_edgeql_migration_computable_02(self):
             type Bar extending Foo;
         ''')
 
+    async def test_edgeql_migration_computed_03(self):
+        await self.migrate(r'''
+            type User {
+                property name -> str;
+                multi link tweets := Tweet;
+            }
+            type Tweet {
+                property text -> str;
+                link author -> User;
+            }
+        ''', module='default')
+
+        await self.con.execute("""
+            INSERT Tweet {
+                text := 'Hello',
+                author := (
+                    INSERT User {name := 'Alice'}
+                )
+            };
+            INSERT Tweet {
+                text := 'Hi',
+                author := (
+                    INSERT User {name := 'Billie'}
+                )
+            };
+        """)
+
+        # Validate our structures
+        await self.assert_query_result(
+            r"""
+                SELECT Tweet {
+                    text,
+                    author: {
+                        name
+                    },
+                } ORDER BY .text;
+            """,
+            [{
+                'text': 'Hello',
+                'author': {
+                    'name': 'Alice'
+                },
+            }, {
+                'text': 'Hi',
+                'author': {
+                    'name': 'Billie'
+                },
+            }],
+        )
+
+        await self.assert_query_result(
+            r"""
+                SELECT User {
+                    name,
+                    tweets: {
+                        text
+                    } ORDER BY .text,
+                } ORDER BY .name;
+            """,
+            [{
+                'name': 'Alice',
+                'tweets': [{
+                    'text': 'Hello'
+                }, {
+                    'text': 'Hi'
+                }],
+            }, {
+                'name': 'Billie',
+                'tweets': [{
+                    'text': 'Hello'
+                }, {
+                    'text': 'Hi'
+                }],
+            }],
+        )
+
+        await self.migrate(r'''
+            type User {
+                property name -> str;
+                multi link tweets := User.<author[IS Tweet];
+            }
+            type Tweet {
+                property text -> str;
+                link author -> User;
+            }
+        ''', module='default')
+
+        await self.assert_query_result(
+            r"""
+                SELECT User {
+                    name,
+                    tweets: {
+                        text
+                    } ORDER BY .text,
+                } ORDER BY .name;
+            """,
+            [{
+                'name': 'Alice',
+                'tweets': [{
+                    'text': 'Hello'
+                }],
+            }, {
+                'name': 'Billie',
+                'tweets': [{
+                    'text': 'Hi'
+                }],
+            }],
+        )
+
+        await self.migrate(r'''
+            type User {
+                property name -> str;
+                multi link tweets := .<author[IS Tweet];
+            }
+            type Tweet {
+                property text -> str;
+                link author -> User;
+            }
+        ''', module='default')
+
+        await self.assert_query_result(
+            r"""
+                SELECT User {
+                    name,
+                    tweets: {
+                        text
+                    } ORDER BY .text,
+                } ORDER BY .name;
+            """,
+            [{
+                'name': 'Alice',
+                'tweets': [{
+                    'text': 'Hello'
+                }],
+            }, {
+                'name': 'Billie',
+                'tweets': [{
+                    'text': 'Hi'
+                }],
+            }],
+        )
+
+        await self.migrate(r'''
+            type User {
+                property name -> str;
+                multi link tweets := (
+                    SELECT Tweet FILTER .author = User
+                );
+            }
+            type Tweet {
+                property text -> str;
+                link author -> User;
+            }
+        ''', module='default')
+
+        await self.assert_query_result(
+            r"""
+                SELECT User {
+                    name,
+                    tweets: {
+                        text
+                    } ORDER BY .text,
+                } ORDER BY .name;
+            """,
+            [{
+                'name': 'Alice',
+                'tweets': [{
+                    'text': 'Hello'
+                }],
+            }, {
+                'name': 'Billie',
+                'tweets': [{
+                    'text': 'Hi'
+                }],
+            }],
+        )
+
+        await self.migrate(r'''
+            type User {
+                property name -> str;
+                multi link tweets := (
+                    WITH U := User
+                    SELECT Tweet FILTER .author = U
+                );
+            }
+            type Tweet {
+                property text -> str;
+                link author -> User;
+            }
+        ''', module='default')
+
+        await self.assert_query_result(
+            r"""
+                SELECT User {
+                    name,
+                    tweets: {
+                        text
+                    } ORDER BY .text,
+                } ORDER BY .name;
+            """,
+            [{
+                'name': 'Alice',
+                'tweets': [{
+                    'text': 'Hello'
+                }],
+            }, {
+                'name': 'Billie',
+                'tweets': [{
+                    'text': 'Hi'
+                }],
+            }],
+        )
+
+        await self.migrate(r'''
+            type User {
+                property name -> str;
+                multi link tweets := (
+                    WITH U := DETACHED User
+                    SELECT Tweet FILTER .author = U
+                );
+            }
+            type Tweet {
+                property text -> str;
+                link author -> User;
+            }
+        ''', module='default')
+
+        await self.assert_query_result(
+            r"""
+                SELECT User {
+                    name,
+                    tweets: {
+                        text
+                    } ORDER BY .text,
+                } ORDER BY .name;
+            """,
+            [{
+                'name': 'Alice',
+                'tweets': [{
+                    'text': 'Hello'
+                }, {
+                    'text': 'Hi'
+                }],
+            }, {
+                'name': 'Billie',
+                'tweets': [{
+                    'text': 'Hello'
+                }, {
+                    'text': 'Hi'
+                }],
+            }],
+        )
+
+        await self.migrate(r'''
+            type User {
+                property name -> str;
+                multi link tweets := (
+                    WITH U := User
+                    SELECT U.<author[IS Tweet]
+                );
+            }
+            type Tweet {
+                property text -> str;
+                link author -> User;
+            }
+        ''', module='default')
+
+        await self.assert_query_result(
+            r"""
+                SELECT User {
+                    name,
+                    tweets: {
+                        text
+                    } ORDER BY .text,
+                } ORDER BY .name;
+            """,
+            [{
+                'name': 'Alice',
+                'tweets': [{
+                    'text': 'Hello'
+                }],
+            }, {
+                'name': 'Billie',
+                'tweets': [{
+                    'text': 'Hi'
+                }],
+            }],
+        )
+
+        await self.migrate(r'''
+            type User {
+                property name -> str;
+                multi link tweets := (
+                    WITH User := DETACHED User
+                    SELECT User.<author[IS Tweet]
+                );
+            }
+            type Tweet {
+                property text -> str;
+                link author -> User;
+            }
+        ''', module='default')
+
+        await self.assert_query_result(
+            r"""
+                SELECT User {
+                    name,
+                    tweets: {
+                        text
+                    } ORDER BY .text,
+                } ORDER BY .name;
+            """,
+            [{
+                'name': 'Alice',
+                'tweets': [{
+                    'text': 'Hello'
+                }, {
+                    'text': 'Hi'
+                }],
+            }, {
+                'name': 'Billie',
+                'tweets': [{
+                    'text': 'Hello'
+                }, {
+                    'text': 'Hi'
+                }],
+            }],
+        )
+
+    async def test_edgeql_migration_computed_04(self):
+        await self.migrate(r'''
+            type User {
+                property name -> str;
+                multi property tweets := Tweet.text;
+            }
+            type Tweet {
+                property text -> str;
+                link author -> User;
+            }
+        ''', module='default')
+
+        await self.con.execute("""
+            INSERT Tweet {
+                text := 'Hello',
+                author := (
+                    INSERT User {name := 'Alice'}
+                )
+            };
+            INSERT Tweet {
+                text := 'Hi',
+                author := (
+                    INSERT User {name := 'Billie'}
+                )
+            };
+        """)
+
+        # Validate our structures
+        await self.assert_query_result(
+            r"""
+                SELECT Tweet {
+                    text,
+                    author: {
+                        name
+                    },
+                } ORDER BY .text;
+            """,
+            [{
+                'text': 'Hello',
+                'author': {
+                    'name': 'Alice'
+                },
+            }, {
+                'text': 'Hi',
+                'author': {
+                    'name': 'Billie'
+                },
+            }],
+        )
+
+        await self.assert_query_result(
+            r"""
+                SELECT User {
+                    name,
+                    tweets,
+                } ORDER BY .name;
+            """,
+            [{
+                'name': 'Alice',
+                'tweets': {'Hello', 'Hi'},
+            }, {
+                'name': 'Billie',
+                'tweets': {'Hello', 'Hi'},
+            }],
+        )
+
+        await self.migrate(r'''
+            type User {
+                property name -> str;
+                multi property tweets := User.<author[IS Tweet].text;
+            }
+            type Tweet {
+                property text -> str;
+                link author -> User;
+            }
+        ''', module='default')
+
+        await self.assert_query_result(
+            r"""
+                SELECT User {
+                    name,
+                    tweets,
+                } ORDER BY .name;
+            """,
+            [{
+                'name': 'Alice',
+                'tweets': {'Hello'},
+            }, {
+                'name': 'Billie',
+                'tweets': {'Hi'},
+            }],
+        )
+
+        await self.migrate(r'''
+            type User {
+                property name -> str;
+                multi property tweets := .<author[IS Tweet].text;
+            }
+            type Tweet {
+                property text -> str;
+                link author -> User;
+            }
+        ''', module='default')
+
+        await self.assert_query_result(
+            r"""
+                SELECT User {
+                    name,
+                    tweets,
+                } ORDER BY .name;
+            """,
+            [{
+                'name': 'Alice',
+                'tweets': {'Hello'},
+            }, {
+                'name': 'Billie',
+                'tweets': {'Hi'},
+            }],
+        )
+
+        await self.migrate(r'''
+            type User {
+                property name -> str;
+                multi property tweets := (
+                    SELECT Tweet FILTER .author = User
+                ).text;
+            }
+            type Tweet {
+                property text -> str;
+                link author -> User;
+            }
+        ''', module='default')
+
+        await self.assert_query_result(
+            r"""
+                SELECT User {
+                    name,
+                    tweets,
+                } ORDER BY .name;
+            """,
+            [{
+                'name': 'Alice',
+                'tweets': {'Hello'},
+            }, {
+                'name': 'Billie',
+                'tweets': {'Hi'},
+            }],
+        )
+
+        await self.migrate(r'''
+            type User {
+                property name -> str;
+                multi property tweets := (
+                    WITH U := User
+                    SELECT Tweet FILTER .author = U
+                ).text;
+            }
+            type Tweet {
+                property text -> str;
+                link author -> User;
+            }
+        ''', module='default')
+
+        await self.assert_query_result(
+            r"""
+                SELECT User {
+                    name,
+                    tweets,
+                } ORDER BY .name;
+            """,
+            [{
+                'name': 'Alice',
+                'tweets': {'Hello'},
+            }, {
+                'name': 'Billie',
+                'tweets': {'Hi'},
+            }],
+        )
+
+        await self.migrate(r'''
+            type User {
+                property name -> str;
+                multi property tweets := (
+                    WITH U := DETACHED User
+                    SELECT Tweet FILTER .author = U
+                ).text;
+            }
+            type Tweet {
+                property text -> str;
+                link author -> User;
+            }
+        ''', module='default')
+
+        await self.assert_query_result(
+            r"""
+                SELECT User {
+                    name,
+                    tweets,
+                } ORDER BY .name;
+            """,
+            [{
+                'name': 'Alice',
+                'tweets': {'Hello', 'Hi'},
+            }, {
+                'name': 'Billie',
+                'tweets': {'Hello', 'Hi'},
+            }],
+        )
+
+        await self.migrate(r'''
+            type User {
+                property name -> str;
+                multi property tweets := (
+                    WITH U := User
+                    SELECT U.<author[IS Tweet].text
+                );
+            }
+            type Tweet {
+                property text -> str;
+                link author -> User;
+            }
+        ''', module='default')
+
+        await self.assert_query_result(
+            r"""
+                SELECT User {
+                    name,
+                    tweets,
+                } ORDER BY .name;
+            """,
+            [{
+                'name': 'Alice',
+                'tweets': {'Hello'},
+            }, {
+                'name': 'Billie',
+                'tweets': {'Hi'},
+            }],
+        )
+
+        await self.migrate(r'''
+            type User {
+                property name -> str;
+                multi property tweets := (
+                    WITH User := DETACHED User
+                    SELECT User.<author[IS Tweet].text
+                );
+            }
+            type Tweet {
+                property text -> str;
+                link author -> User;
+            }
+        ''', module='default')
+
+        await self.assert_query_result(
+            r"""
+                SELECT User {
+                    name,
+                    tweets,
+                } ORDER BY .name;
+            """,
+            [{
+                'name': 'Alice',
+                'tweets': {'Hello', 'Hi'},
+            }, {
+                'name': 'Billie',
+                'tweets': {'Hello', 'Hi'},
+            }],
+        )
+
+    async def test_edgeql_migration_computed_05(self):
+        await self.migrate(r'''
+            type Bar {
+                multi link foo := Foo;
+                property name -> str;
+            };
+            type Foo {
+                link bar -> Bar;
+                property val -> str;
+            };
+        ''', module='default')
+
+        await self.con.execute("""
+            INSERT Foo {
+                val := 'foo0',
+                bar := (
+                    INSERT Bar {name := 'bar0'}
+                ),
+            };
+            INSERT Foo {
+                val := 'foo1',
+                bar := (
+                    INSERT Bar {name := 'bar1'}
+                ),
+            };
+        """)
+
+        await self.assert_query_result(
+            r"""
+                SELECT Foo {
+                    val,
+                    bar: {
+                        name,
+                        foo: {
+                            val
+                        } ORDER BY .val,
+                    },
+                } ORDER BY .val;
+            """,
+            [{
+                'val': 'foo0',
+                'bar': {
+                    'name': 'bar0',
+                    'foo': [{'val': 'foo0'}, {'val': 'foo1'}],
+                },
+            }, {
+                'val': 'foo1',
+                'bar': {
+                    'name': 'bar1',
+                    'foo': [{'val': 'foo0'}, {'val': 'foo1'}],
+                },
+            }],
+        )
+
+    async def test_edgeql_migration_computed_06(self):
+        await self.migrate(r'''
+            type Bar {
+                multi property foo := Foo.val;
+                property name -> str;
+            };
+            type Foo {
+                link bar -> Bar;
+                property val -> str;
+            };
+        ''', module='default')
+
+        await self.con.execute("""
+            INSERT Foo {
+                val := 'foo0',
+                bar := (
+                    INSERT Bar {name := 'bar0'}
+                ),
+            };
+            INSERT Foo {
+                val := 'foo1',
+                bar := (
+                    INSERT Bar {name := 'bar1'}
+                ),
+            };
+        """)
+
+        await self.assert_query_result(
+            r"""
+                SELECT Foo {
+                    val,
+                    bar: {
+                        name,
+                        foo,
+                    },
+                } ORDER BY .val;
+            """,
+            [{
+                'val': 'foo0',
+                'bar': {
+                    'name': 'bar0',
+                    'foo': {'foo0', 'foo1'},
+                },
+            }, {
+                'val': 'foo1',
+                'bar': {
+                    'name': 'bar1',
+                    'foo': {'foo0', 'foo1'},
+                },
+            }],
+        )
+
     async def test_edgeql_migration_eq_01(self):
         await self.migrate("""
             type Base;