Repository: edgedb/edgedb

Issue:
Unable to group by link property 
<!-- Please search existing issues to avoid creating duplicates. -->

<!--
For the EdgeDB Version: run `edgedb query 'select sys::get_version_as_str()'` from your project directory (or run `select sys::get_version_as_str();` in the EdgeDB interactive shell).
For the EdgeDB CLI Version: Run `edgedb --version` from anywhere
-->

- EdgeDB Version: 5.0-beta.1+15775e6

Schema:
```
multi user_reactions: User {
   emoji: str;
}
```

```
with grouped := (group .user_reactions by @emoji)
select grouped
```
`Unexpected '@'`


Discussion:
You should be able to do it by binding the link property to a variable with a `using` clause

Resolving patch:
diff --git a/edb/edgeql/desugar_group.py b/edb/edgeql/desugar_group.py
--- a/edb/edgeql/desugar_group.py
+++ b/edb/edgeql/desugar_group.py
@@ -95,7 +95,7 @@ def desugar_group(
     aliases: AliasGenerator,
 ) -> qlast.InternalGroupQuery:
     assert not isinstance(node, qlast.InternalGroupQuery)
-    alias_map: Dict[str, Tuple[str, qlast.Expr]] = {}
+    by_alias_map: Dict[str, Tuple[str, qlast.Path]] = {}
 
     def rewrite_atom(el: qlast.GroupingAtom) -> qlast.GroupingAtom:
         if isinstance(el, qlast.ObjectRef):
@@ -103,10 +103,21 @@ def rewrite_atom(el: qlast.GroupingAtom) -> qlast.GroupingAtom:
         elif isinstance(el, qlast.Path):
             assert isinstance(el.steps[0], qlast.Ptr)
             ptrname = el.steps[0].name
-            if ptrname not in alias_map:
+            ptrtype = el.steps[0].type
+            if ptrname not in by_alias_map:
                 alias = aliases.get(ptrname)
-                alias_map[ptrname] = (alias, el)
-            alias = alias_map[ptrname][0]
+                by_alias_map[ptrname] = (alias, el)
+            else:
+                alias = by_alias_map[ptrname][0]
+                aliased_el = by_alias_map[ptrname][1]
+                assert isinstance(aliased_el.steps[0], qlast.Ptr)
+                aliased_el_ptrtype = aliased_el.steps[0].type
+                if ptrtype != aliased_el_ptrtype:
+                    raise errors.QueryError(
+                        f"BY clause cannot refer to link property and object "
+                        f"property with the same name",
+                        span=el.span,
+                    )
             return qlast.ObjectRef(name=alias)
         else:
             return qlast.GroupingIdentList(
@@ -132,6 +143,10 @@ def rewrite(el: qlast.GroupingElement) -> qlast.GroupingElement:
     # with any bindings for pointers the by clause refers to directly.
     by = [rewrite(by_el) for by_el in node.by]
 
+    alias_map: Dict[str, Tuple[str, qlast.Expr]] = {
+        k: v for k, v in by_alias_map.items()
+    }
+
     for using_clause in (node.using or ()):
         if using_clause.alias in alias_map:
             # TODO: This would be a great place to allow multiple spans!
diff --git a/edb/edgeql/parser/grammar/expressions.py b/edb/edgeql/parser/grammar/expressions.py
--- a/edb/edgeql/parser/grammar/expressions.py
+++ b/edb/edgeql/parser/grammar/expressions.py
@@ -120,6 +120,18 @@ def reduce_DOT_Identifier(self, *kids):
             steps=[qlast.Ptr(name=kids[1].val)],
         )
 
+    def reduce_AT_Identifier(self, *kids):
+        self.val = qlast.Path(
+            partial=True,
+            steps=[
+                qlast.Ptr(
+                    name=kids[1].val,
+                    type='property',
+                    span=kids[1].span,
+                )
+            ]
+        )
+
 
 class GroupingIdentList(ListNonterm, element=GroupingIdent,
                         separator=tokens.T_COMMA):


Test patch:
diff --git a/tests/schemas/cards.esdl b/tests/schemas/cards.esdl
--- a/tests/schemas/cards.esdl
+++ b/tests/schemas/cards.esdl
@@ -54,6 +54,7 @@ type Bot extending User;
 type Card extending Named {
     required element: str;
     required cost: int64;
+    optional text: str;
     multi owners := .<deck[IS User];
     # computable property
     elemental_cost := <str>.cost ++ ' ' ++ .element;
diff --git a/tests/test_edgeql_group.py b/tests/test_edgeql_group.py
--- a/tests/test_edgeql_group.py
+++ b/tests/test_edgeql_group.py
@@ -510,7 +510,7 @@ async def test_edgeql_group_grouping_sets_02(self):
             ]
         )
 
-    async def test_edgeql_group_duplicate_rejected(self):
+    async def test_edgeql_group_duplicate_rejected_01(self):
         async with self.assertRaisesRegexTx(
             edgedb.QueryError,
             "used directly in the BY clause",
@@ -521,6 +521,37 @@ async def test_edgeql_group_duplicate_rejected(self):
                 by cube(.element, element)
             ''')
 
+    async def test_edgeql_group_duplicate_rejected_02(self):
+        async with self.assertRaisesRegexTx(
+            edgedb.QueryError,
+            "BY clause cannot refer to link property and object property with "
+            "the same name",
+        ):
+            await self.con.execute('''
+                WITH MODULE cards
+                SELECT Card {
+                    invalid := (
+                        GROUP .avatar
+                        BY @text, .text
+                    )
+                }
+            ''')
+
+        async with self.assertRaisesRegexTx(
+            edgedb.QueryError,
+            "BY clause cannot refer to link property and object property with "
+            "the same name",
+        ):
+            await self.con.execute('''
+                WITH MODULE cards
+                SELECT Card {
+                    invalid := (
+                        GROUP .avatar
+                        BY .text, @text
+                    )
+                }
+            ''')
+
     async def test_edgeql_group_for_01(self):
         await self.assert_query_result(
             r'''
@@ -1382,6 +1413,73 @@ async def test_edgeql_group_backlink(self):
             [{"a": {}}, {"a": {}}],
         )
 
+    async def test_edgeql_group_link_property_01(self):
+        await self.assert_query_result(
+            r'''
+            with module cards
+            select User {
+              cards_by_count := (group .deck by @count) {
+                key : {count},
+                elements: {name},
+              }
+            }
+            filter .name = 'Alice';
+            ''',
+            [
+                {
+                    "cards_by_count": [
+                        {
+                            "key": {"count": 2},
+                            "elements": [
+                                {"name": "Imp"},
+                                {"name": "Dragon"}
+                            ]
+                        },
+                        {
+                            "key": {"count": 3},
+                            "elements": [
+                                {"name": "Bog monster"},
+                                {"name": "Giant turtle"}
+                            ]
+                        }
+                    ]
+                }
+            ],
+        )
+
+        await self.assert_query_result(
+            r'''
+            with module cards
+            select User {
+              cards_by_count := (group .deck by (@count, @count)) {
+                key : {count},
+                elements: {name},
+              }
+            }
+            filter .name = 'Alice';
+            ''',
+            [
+                {
+                    "cards_by_count": [
+                        {
+                            "key": {"count": 2},
+                            "elements": [
+                                {"name": "Imp"},
+                                {"name": "Dragon"}
+                            ]
+                        },
+                        {
+                            "key": {"count": 3},
+                            "elements": [
+                                {"name": "Bog monster"},
+                                {"name": "Giant turtle"}
+                            ]
+                        }
+                    ]
+                }
+            ],
+        )
+
     async def test_edgeql_group_destruct_immediately_01(self):
         await self.assert_query_result(
             r'''