Repository: edgedb/edgedb

Issue:
Error message when function variant cannot be found
This is another quick error message change on my wishlist based on my experiences. Take the following:

```
CREATE FUNCTION give_string() -> str USING ('Hi guyz');
CREATE FUNCTION give_string(input: str) -> str USING (input);
CREATE FUNCTION give_string(input: int64) -> str USING (<str>input);
SELECT give_string(8.6);
```

That gives the following error message:

```
error: could not find a function variant give_string
```

I remember missing the word 'variant' in the message and thinking I got the wrong function a few times.

A change to something like this seems easiest:

```
error: function give_string exists but not with this input. Hint: type **DESCRIBE FUNCTION give_string** to see existing variants.
```


Discussion:
Agreed, @Dhghomon .
While working with EdgeDB, I encountered a similar problem and found it helpful to change coding patterns.

```sql
CREATE MODULE from_str;
CREATE MODULE from_nil;
CREATE MODULE from_i64;

CREATE FUNCTION from_nil::give_string() -> str USING ('Hi guyz');
CREATE FUNCTION from_str::give_string(input: str) -> str USING (input);
CREATE FUNCTION from_i64::give_string(input: int64) -> str USING (<str>input);
```

Resolving patch:
diff --git a/edb/edgeql/compiler/func.py b/edb/edgeql/compiler/func.py
--- a/edb/edgeql/compiler/func.py
+++ b/edb/edgeql/compiler/func.py
@@ -97,15 +97,49 @@ def compile_FunctionCall(
     args, kwargs, arg_ctxs = compile_call_args(expr, funcname, ctx=ctx)
     matched = polyres.find_callable(funcs, args=args, kwargs=kwargs, ctx=ctx)
     if not matched:
+        alts = [f.get_signature_as_str(env.schema) for f in funcs]
+        sig: List[str] = []
+        # This is used to generate unique arg names.
+        argnum = 0
+        for argtype, _ in args:
+            # Skip any name colliding with kwargs.
+            while f'arg{argnum}' in kwargs:
+                argnum += 1
+            sig.append(
+                f'arg{argnum}: {argtype.get_displayname(env.schema)}'
+            )
+            argnum += 1
+        for kwname, (kwtype, _) in kwargs.items():
+            sig.append(
+                f'NAMED ONLY {kwname}: {kwtype.get_displayname(env.schema)}'
+            )
+
+        signature = f'{funcname}({", ".join(sig)})'
+
+        if not funcs:
+            hint = None
+        elif len(alts) == 1:
+            hint = f'Did you want "{alts[0]}"?'
+        else:  # Multiple alternatives
+            hint = (
+                f'Did you want one of the following functions instead:\n' +
+                ('\n'.join(alts))
+            )
+
         raise errors.QueryError(
-            f'could not find a function variant {funcname}',
+            f'function "{signature}" does not exist',
+            hint=hint,
             context=expr.context)
     elif len(matched) > 1:
         if in_abstract_constraint:
             matched_call = matched[0]
         else:
+            alts = [m.func.get_signature_as_str(env.schema) for m in matched]
             raise errors.QueryError(
                 f'function {funcname} is not unique',
+                hint=f'Please disambiguate between the following '
+                     f'alternatives:\n' +
+                     ('\n'.join(alts)),
                 context=expr.context)
     else:
         matched_call = matched[0]
diff --git a/edb/schema/functions.py b/edb/schema/functions.py
--- a/edb/schema/functions.py
+++ b/edb/schema/functions.py
@@ -702,6 +702,9 @@ def get_return_type(self, schema: s_schema.Schema) -> s_types.Type:
     def get_return_typemod(self, schema: s_schema.Schema) -> ft.TypeModifier:
         raise NotImplementedError
 
+    def get_signature_as_str(self, schema: s_schema.Schema) -> str:
+        raise NotImplementedError
+
     def get_verbosename(self, schema: s_schema.Schema) -> str:
         raise NotImplementedError
 
@@ -1150,15 +1153,21 @@ def has_inlined_defaults(self, schema: s_schema.Schema) -> bool:
         return bool(self.get_language(schema) is qlast.Language.EdgeQL and
                     self.get_params(schema).find_named_only(schema))
 
+    def get_signature_as_str(
+        self,
+        schema: s_schema.Schema,
+    ) -> str:
+        params = self.get_params(schema)
+        sn = self.get_shortname(schema)
+        return f"{sn}{params.as_str(schema)}"
+
     def get_verbosename(
         self,
         schema: s_schema.Schema,
         *,
         with_parent: bool=False,
     ) -> str:
-        params = self.get_params(schema)
-        sn = self.get_shortname(schema)
-        return f"function '{sn}{params.as_str(schema)}'"
+        return f"function '{self.get_signature_as_str(schema)}'"
 
     def get_dummy_body(self, schema: s_schema.Schema) -> expr.Expression:
         """Return a minimal function body that satisfies its return type."""
@@ -1490,8 +1499,7 @@ def _create_begin(
                     f'cannot create `{signature}` function: '
                     f'overloading another function with different '
                     f'named only parameters: '
-                    f'"{func.get_shortname(schema)}'
-                    f'{func_params.as_str(schema)}"',
+                    f'"{func.get_signature_as_str(schema)}"',
                     context=self.source_context)
 
             if ((has_polymorphic or func_params.has_polymorphic(schema)) and (


Test patch:
diff --git a/tests/test_edgeql_calls.py b/tests/test_edgeql_calls.py
--- a/tests/test_edgeql_calls.py
+++ b/tests/test_edgeql_calls.py
@@ -139,7 +139,7 @@ async def test_edgeql_calls_03(self):
         for c in cases:
             with self.assertRaisesRegex(
                     edgedb.QueryError,
-                    r'could not find a function variant'):
+                    r'function .+ does not exist'):
                 async with self.con.transaction():
                     await self.con.execute(c)
 
@@ -315,7 +315,7 @@ async def test_edgeql_calls_07(self):
         for c in cases:
             with self.assertRaisesRegex(
                     edgedb.QueryError,
-                    r'could not find a function variant'):
+                    r'function .+ does not exist'):
                 async with self.con.transaction():
                     await self.con.execute(c)
 
@@ -507,7 +507,7 @@ async def test_edgeql_calls_11(self):
         for c in cases:
             with self.assertRaisesRegex(
                     edgedb.QueryError,
-                    r'could not find a function variant'):
+                    r'function .+ does not exist'):
                 async with self.con.transaction():
                     await self.con.execute(c)
 
@@ -766,7 +766,7 @@ async def test_edgeql_calls_18(self):
 
         with self.assertRaisesRegex(
                 edgedb.QueryError,
-                r'could not find a function variant'):
+                r'function .+ does not exist'):
 
             async with self.con.transaction():
                 await self.con.execute('SELECT test::call18(1, 2, "a");')
@@ -826,7 +826,7 @@ async def test_edgeql_calls_20(self):
 
         with self.assertRaisesRegex(
                 edgedb.QueryError,
-                r'could not find a function variant'):
+                r'function .+ does not exist'):
             async with self.con.transaction():
                 await self.con.execute('SELECT test::call20_1(1, "1");')
 
@@ -974,7 +974,7 @@ async def test_edgeql_calls_26(self):
 
         with self.assertRaisesRegex(
                 edgedb.QueryError,
-                r'could not find a function variant'):
+                r'function .+ does not exist'):
             async with self.con.transaction():
                 await self.con.execute('SELECT test::call26([(1, 2)]);')
 
@@ -1008,7 +1008,7 @@ async def test_edgeql_calls_27(self):
         for c in cases:
             with self.assertRaisesRegex(
                     edgedb.QueryError,
-                    r'could not find a function variant'):
+                    r'function .+ does not exist'):
                 async with self.con.transaction():
                     await self.con.execute(c)
 
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
@@ -5011,7 +5011,7 @@ async def test_edgeql_migration_eq_function_11(self):
         # make sure that the old one is gone
         with self.assertRaisesRegex(
                 edgedb.QueryError,
-                r'could not find a function variant hello11'):
+                r'function "hello11\(arg0: std::int64\)" does not exist'):
             await self.con.execute(
                 r"""SELECT hello11(1);"""
             )
@@ -5099,7 +5099,7 @@ async def test_edgeql_migration_eq_function_13(self):
         # make sure that the other one is gone
         with self.assertRaisesRegex(
                 edgedb.QueryError,
-                r'could not find a function variant hello13'):
+                r'function "hello13\(arg0: std::str\)" does not exist'):
             await self.con.execute(
                 r"""SELECT hello13(' world');"""
             )
@@ -5137,7 +5137,8 @@ async def test_edgeql_migration_eq_function_14(self):
         # make sure that the old one is gone
         with self.assertRaisesRegex(
                 edgedb.QueryError,
-                r'could not find a function variant hello14'):
+                r'function "hello14\(arg0: std::str, arg1: std::str\)" '
+                r'does not exist'):
             await self.assert_query_result(
                 r"""SELECT hello14('hello', '14');""",
                 ['hello14'],
@@ -5176,7 +5177,8 @@ async def test_edgeql_migration_eq_function_15(self):
         # make sure that the old one is gone
         with self.assertRaisesRegex(
                 edgedb.QueryError,
-                r'could not find a function variant hello15'):
+                r'function "hello15\(arg0: std::str, arg1: std::str\)" '
+                r'does not exist'):
             await self.assert_query_result(
                 r"""SELECT hello15('hello', '15');""",
                 ['hello15'],
diff --git a/tests/test_edgeql_functions.py b/tests/test_edgeql_functions.py
--- a/tests/test_edgeql_functions.py
+++ b/tests/test_edgeql_functions.py
@@ -595,7 +595,7 @@ async def test_edgeql_functions_array_get_02(self):
     async def test_edgeql_functions_array_get_03(self):
         with self.assertRaisesRegex(
                 edgedb.QueryError,
-                r'could not find a function variant array_get'):
+                r'function "array_get.+" does not exist'):
 
             await self.con.query(r'''
                 SELECT array_get([1, 2, 3], 2^40);
diff --git a/tests/test_edgeql_select.py b/tests/test_edgeql_select.py
--- a/tests/test_edgeql_select.py
+++ b/tests/test_edgeql_select.py
@@ -2742,8 +2742,9 @@ async def test_edgeql_select_func_05(self):
             ]}]
         )
 
-        with self.assertRaisesRegex(edgedb.QueryError,
-                                    'could not find a function variant'):
+        with self.assertRaisesRegex(
+                edgedb.QueryError,
+                r'function .+ does not exist'):
             async with self.con.transaction():
                 await self.con.query(
                     "SELECT test::concat1('aaa', 'bbb', 2);")
@@ -2758,8 +2759,9 @@ async def test_edgeql_select_func_06(self):
                 USING SQL FUNCTION 'concat';
         ''')
 
-        with self.assertRaisesRegex(edgedb.QueryError,
-                                    'could not find a function'):
+        with self.assertRaisesRegex(
+                edgedb.QueryError,
+                r'function .+ does not exist'):
             await self.con.execute(r'SELECT test::concat2(123);')
 
     async def test_edgeql_select_func_07(self):
@@ -2826,13 +2828,15 @@ async def test_edgeql_select_func_07(self):
             }]
         )
 
-        with self.assertRaisesRegex(edgedb.QueryError,
-                                    'could not find a function'):
+        with self.assertRaisesRegex(
+                edgedb.QueryError,
+                r'function .+ does not exist'):
             async with self.con.transaction():
                 await self.con.query(r'SELECT test::concat3(123);')
 
-        with self.assertRaisesRegex(edgedb.QueryError,
-                                    'could not find a function'):
+        with self.assertRaisesRegex(
+                edgedb.QueryError,
+                r'function .+ does not exist'):
             async with self.con.transaction():
                 await self.con.query(r'SELECT test::concat3("a", 123);')