Repository: edgedb/edgedb

Issue:
InternalServerError of KeyError when filtering on a composite exclusive constraint
<!-- Please search existing issues to avoid creating duplicates. -->
- EdgeDB Version: `1.0+9ecadfc`
- OS Version: `Ubuntu 21.04`

Steps to Reproduce:

1. Create an instance and apply initial migration with schema below
2. Run the query `SELECT Account FILTER .provider = 'google' and .providerAccountId = '123456789';`

<!-- If the issue is about a query error, please also provide your schema -->
Schema:
```
module default {
	type Account {
		property provider -> str;
		property providerAccountId -> str;

		constraint exclusive on ( ( .provider, .providerAccountId ) );
	}
}
```

### Example stacktrace
```
Server traceback:
      Traceback (most recent call last):
        File "/home/cory/.local/share/edgedb/portable/1.0/lib/python3.10/site-packages/edb/server/compiler_pool/worker.py", line 309, in worker
          res = meth(*args)
        File "/home/cory/.local/share/edgedb/portable/1.0/lib/python3.10/site-packages/edb/server/compiler_pool/worker.py", line 177, in compile
          units, cstate = COMPILER.compile(
        File "/home/cory/.local/share/edgedb/portable/1.0/lib/python3.10/site-packages/edb/server/compiler/compiler.py", line 2096, in compile
          units = self._compile(ctx=ctx, source=source)
        File "/home/cory/.local/share/edgedb/portable/1.0/lib/python3.10/site-packages/edb/server/compiler/compiler.py", line 1657, in _compile
          return self._try_compile(ctx=ctx, source=source)
        File "/home/cory/.local/share/edgedb/portable/1.0/lib/python3.10/site-packages/edb/server/compiler/compiler.py", line 1710, in _try_compile
          comp, capabilities = self._compile_dispatch_ql(
        File "/home/cory/.local/share/edgedb/portable/1.0/lib/python3.10/site-packages/edb/server/compiler/compiler.py", line 1631, in _compile_dispatch_ql
          query = self._compile_ql_query(ctx, ql)
        File "/home/cory/.local/share/edgedb/portable/1.0/lib/python3.10/site-packages/edb/server/compiler/compiler.py", line 567, in _compile_ql_query
          ir = qlcompiler.compile_ast_to_ir(
        File "/home/cory/.local/share/edgedb/portable/1.0/lib/python3.10/site-packages/edb/edgeql/compiler/__init__.py", line 179, in wrapper
          return func(*args, **kwargs)
        File "/home/cory/.local/share/edgedb/portable/1.0/lib/python3.10/site-packages/edb/edgeql/compiler/__init__.py", line 232, in compile_ast_to_ir
          ir_expr = stmtctx_mod.fini_expression(ir_set, ctx=ctx)
        File "/home/cory/.local/share/edgedb/portable/1.0/lib/python3.10/site-packages/edb/edgeql/compiler/stmtctx.py", line 128, in fini_expression
          cardinality = inference.infer_cardinality(
        File "/home/cory/.local/share/edgedb/portable/1.0/lib/python3.10/site-packages/edb/edgeql/compiler/inference/cardinality.py", line 1272, in infer_cardinality
          result = _infer_set(
        File "/home/cory/.local/share/edgedb/portable/1.0/lib/python3.10/site-packages/edb/edgeql/compiler/inference/cardinality.py", line 492, in _infer_set
          result = _infer_set_inner(
        File "/home/cory/.local/share/edgedb/portable/1.0/lib/python3.10/site-packages/edb/edgeql/compiler/inference/cardinality.py", line 516, in _infer_set_inner
          expr_card = infer_cardinality(ir.expr, scope_tree=new_scope, ctx=ctx)
        File "/home/cory/.local/share/edgedb/portable/1.0/lib/python3.10/site-packages/edb/edgeql/compiler/inference/cardinality.py", line 1276, in infer_cardinality
          result = _infer_cardinality(ir, scope_tree=scope_tree, ctx=ctx)
        File "/home/cory/.local/share/edgedb/portable/1.0/lib/python3.10/functools.py", line 889, in wrapper
          return dispatch(args[0].__class__)(*args, **kw)
        File "/home/cory/.local/share/edgedb/portable/1.0/lib/python3.10/site-packages/edb/edgeql/compiler/inference/cardinality.py", line 1086, in __infer_select_stmt
          stmt_card = _infer_stmt_cardinality(ir, scope_tree=scope_tree, ctx=ctx)
        File "/home/cory/.local/share/edgedb/portable/1.0/lib/python3.10/site-packages/edb/edgeql/compiler/inference/cardinality.py", line 1064, in _infer_stmt_cardinality
          result_card = _analyse_filter_clause(
        File "/home/cory/.local/share/edgedb/portable/1.0/lib/python3.10/site-packages/edb/edgeql/compiler/inference/cardinality.py", line 1017, in _analyse_filter_clause
          if extract_exclusive_filters(result_set, filter_clause, scope_tree, ctx):
        File "/home/cory/.local/share/edgedb/portable/1.0/lib/python3.10/site-packages/edb/edgeql/compiler/inference/cardinality.py", line 966, in extract_exclusive_filters
          tuple((ptr, filtered_ptrs_map[ptr]) for ptr in obj_exc_ptrs))
        File "/home/cory/.local/share/edgedb/portable/1.0/lib/python3.10/site-packages/edb/edgeql/compiler/inference/cardinality.py", line 966, in <genexpr>
          tuple((ptr, filtered_ptrs_map[ptr]) for ptr in obj_exc_ptrs))
      KeyError: <Property f894ee58-8ef1-11ec-9de7-2b4118803a47 at 0x0x7ff1b6275900>
```

Removing the constraint results in the expected behaviour of the `filter` and no error.

You can get around it by using a `with` block such as:
```
WITH accounts := (SELECT Account FILTER .provider = 'google')
SELECT accounts FILTER .providerAccountId = '123456789';
```

<br><br>

On a side note: I'm loving EdgeDB so far and came across this issue as I am attempting to create my own next-auth database adapter.


Discussion:

Resolving patch:
diff --git a/edb/edgeql/compiler/inference/cardinality.py b/edb/edgeql/compiler/inference/cardinality.py
--- a/edb/edgeql/compiler/inference/cardinality.py
+++ b/edb/edgeql/compiler/inference/cardinality.py
@@ -940,14 +940,15 @@ def extract_exclusive_filters(
 
     results: List[Tuple[Tuple[s_pointers.Pointer, irast.Set], ...]] = []
     if filtered_ptrs:
-        filtered_ptrs_map = dict(filtered_ptrs)
         schema = ctx.env.schema
-        ptr_set = set()
+        filtered_ptrs_map = {
+            ptr.get_nearest_non_derived_parent(schema): expr
+            for ptr, expr in filtered_ptrs
+        }
+        ptr_set = set(filtered_ptrs_map)
         # First look at each referenced pointer and see if it has
         # an exclusive constraint.
-        for ptr, expr in filtered_ptrs:
-            ptr = ptr.get_nearest_non_derived_parent(schema)
-            ptr_set.add(ptr)
+        for ptr, expr in filtered_ptrs_map.items():
             for constr in ptr.get_exclusive_constraints(schema):
                 # Bingo, got an equality filter on a pointer with a
                 # unique constraint


Test patch:
diff --git a/tests/schemas/constraints.esdl b/tests/schemas/constraints.esdl
--- a/tests/schemas/constraints.esdl
+++ b/tests/schemas/constraints.esdl
@@ -257,3 +257,9 @@ type PropertyContainer {
         constraint exclusive
     }
 }
+
+type Pair {
+    required property x -> str;
+    required property y -> str;
+    constraint exclusive on (( .x, .y ));
+}
diff --git a/tests/test_constraints.py b/tests/test_constraints.py
--- a/tests/test_constraints.py
+++ b/tests/test_constraints.py
@@ -368,6 +368,19 @@ async def test_constraints_exclusive_delegation(self):
                     };
                 """)
 
+    async def test_constraints_exclusive_pair(self):
+        await self.assert_query_result(
+            r'''
+                select {
+                    single z := (
+                        select Pair {x, y} filter .x = 'a' and .y = 'b')
+                }
+            ''',
+            [
+                {"z": None}
+            ],
+        )
+
     async def test_constraints_exclusive_multi_property_distinct(self):
         await self.con.execute("""
             INSERT PropertyContainer {
diff --git a/tests/test_edgeql_ir_card_inference.py b/tests/test_edgeql_ir_card_inference.py
--- a/tests/test_edgeql_ir_card_inference.py
+++ b/tests/test_edgeql_ir_card_inference.py
@@ -543,7 +543,7 @@ def test_edgeql_ir_card_inference_56(self):
 
     def test_edgeql_ir_card_inference_57(self):
         """
-        SELECT Person FILTER .p = 7 AND .q = 3
+        SELECT Person { first } FILTER .p = 7 AND .q = 3
 % OK %
         AT_MOST_ONE
         """