Repository: edgedb/edgedb

Issue:
Error in `constraint expression on (...)` when using ranges
<!-- 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: 3.1+1f030a7
- EdgeDB CLI Version: EdgeDB CLI 3.4.0+160d07d
- OS Version:

Steps to Reproduce:

1. Create a very simple schema (shown below);
2. Run `edgedb migration create -I<you_edgedb_instance>`;
3. Run `edgedb migrate -I<you_edgedb_instance>`

Gives an error:
> edgedb error: InternalServerError: function edgedbstd.c9f35653-1c46-11ee-b1be-95bee0448ec8(boolean, boolean, boolean, bigint, bigint) does not exist
>   Hint: This is most likely a bug in EdgeDB. Please consider opening an issue ticket at ...
>   Server traceback:
>       edb.errors.InternalServerError: function edgedbstd.c9f35653-1c46-11ee-b1be-95bee0448ec8(boolean, boolean, boolean, bigint, bigint) does not exist
> edgedb error: error in one of the migrations

<!-- If the issue is about a query error, please also provide your schema -->

Schema:
```
module default {
    type User {
        name: str {
            constraint expression on ( contains(range(2, 30), len(__subject__)) );
        };
    };
};
```

But if you replace **_range(...)_** with simple string for experimentation - migration is created and applied perfectly well:
`constraint expression on ( contains("abc", __subject__) );`


Discussion:
Probably this is because of how range is special-cased in `relgen`.

If we can fix it by using a `simple_special_case` instead so it works in singleton mode, that's probably best?

Either way, we should produce a real error from somewhere when we need to compile a function call in singleton mode and it is marked `func_sql_expr` and we don't have a specialization

Resolving patch:
diff --git a/edb/ir/ast.py b/edb/ir/ast.py
--- a/edb/ir/ast.py
+++ b/edb/ir/ast.py
@@ -890,7 +890,8 @@ class Call(ImmutableExpr):
     force_return_cast: bool
 
     # Bound arguments.
-    # Named arguments will come first, followed by positional arguments.
+    # Named arguments will come first, sorted by name,
+    # followed by positional arguments, in order of declaration.
     args: typing.List[CallArg]
 
     # Typemods of parameters.  This list corresponds to ".args"
diff --git a/edb/pgsql/compiler/expr.py b/edb/pgsql/compiler/expr.py
--- a/edb/pgsql/compiler/expr.py
+++ b/edb/pgsql/compiler/expr.py
@@ -647,9 +647,15 @@ def compile_FunctionCall(
         expr: irast.FunctionCall, *,
         ctx: context.CompilerContextLevel) -> pgast.BaseExpr:
 
-    if sfunc := relgen._SIMPLE_SPECIAL_FUNCTIONS.get(str(expr.func_shortname)):
+    fname = str(expr.func_shortname)
+    if sfunc := relgen._SIMPLE_SPECIAL_FUNCTIONS.get(fname):
         return sfunc(expr, ctx=ctx)
 
+    if expr.func_sql_expr:
+        raise errors.UnsupportedFeatureError(
+            f'unimplemented function for singleton mode: {fname}'
+        )
+
     if expr.typemod is ql_ft.TypeModifier.SetOfType:
         raise errors.UnsupportedFeatureError(
             'set returning functions are not supported in simple expressions')
diff --git a/edb/pgsql/compiler/relgen.py b/edb/pgsql/compiler/relgen.py
--- a/edb/pgsql/compiler/relgen.py
+++ b/edb/pgsql/compiler/relgen.py
@@ -2800,12 +2800,12 @@ def process_set_as_std_min_max(
     return new_stmt_set_rvar(ir_set, ctx.rel, ctx=ctx)
 
 
-@_special_case('std::range', only_as_fallback=True)
+@simple_special_case('std::range')
 def process_set_as_std_range(
-    ir_set: irast.Set,
+    expr: irast.FunctionCall,
     *,
     ctx: context.CompilerContextLevel,
-) -> SetRVars:
+) -> pgast.BaseExpr:
     # Generic range constructor implementation
     #
     #   std::range(
@@ -2828,8 +2828,6 @@ def process_set_as_std_range(
     #         [inc_lower::int + 1][inc_upper::int + 1]
     #     )
     #   end
-    expr = ir_set.expr
-    assert isinstance(expr, irast.FunctionCall)
 
     # N.B: kwargs go first and are sorted by name
     empty = dispatch.compile(expr.args[0].expr, ctx=ctx)
@@ -2915,9 +2913,7 @@ def process_set_as_std_range(
         defresult=non_empty_range,
     )
 
-    pathctx.put_path_value_var(ctx.rel, ir_set.path_id, set_expr)
-
-    return new_stmt_set_rvar(ir_set, ctx.rel, ctx=ctx)
+    return set_expr
 
 
 @_special_case('std::multirange', only_as_fallback=True)


Test patch: