Repository: edgedb/edgedb

Issue:
Mismatched extending constraint causes an internal error 
- EdgeDB Version: EdgeDB 1.0-alpha.3+dev.231.g0cfce63e
- OS Version: Llinux

Steps to Reproduce:
```
db> CREATE ABSTRACT CONSTRAINT `🚀🍿` EXTENDING max_len_value;
OK: CREATE
db> CREATE SCALAR TYPE foo EXTENDING str {
...     CREATE CONSTRAINT `🚀🍿`(10);
... };
ERROR: InternalServerError: 'NoneType' object has no attribute 'get_parameter_name'
  Hint: This is most likely a bug in EdgeDB. Please consider opening an issue ticket at https://github.com/edgedb/edgedb/issues/new?template=bug_report.md
  Server traceback:
      Traceback (most recent call last):
        File "/home/victor/dev/magicstack/edgedb/edb/server/procpool/worker.py", line 75, in worker
          res = await meth(*args)
        File "/home/victor/dev/magicstack/edgedb/edb/server/compiler/compiler.py", line 1480, in compile_eql_tokens
          return self._compile(ctx=ctx, tokens=eql_tokens)
        File "/home/victor/dev/magicstack/edgedb/edb/server/compiler/compiler.py", line 1141, in _compile
          comp: dbstate.BaseQuery = self._compile_dispatch_ql(ctx, stmt)
        File "/home/victor/dev/magicstack/edgedb/edb/server/compiler/compiler.py", line 1076, in _compile_dispatch_ql
          return self._compile_ql_ddl(ctx, ql)
        File "/home/victor/dev/magicstack/edgedb/edb/server/compiler/compiler.py", line 763, in _compile_ql_ddl
          cmd = s_ddl.delta_from_ddl(
        File "/home/victor/dev/magicstack/edgedb/edb/schema/ddl.py", line 466, in delta_from_ddl
          _, cmd = _delta_from_ddl(ddl_stmt, schema=schema, modaliases=modaliases,
        File "/home/victor/dev/magicstack/edgedb/edb/schema/ddl.py", line 496, in _delta_from_ddl
          schema = cmd.apply(schema, context)
        File "/home/victor/dev/magicstack/edgedb/edb/schema/delta.py", line 1531, in apply
          schema = self._create_innards(schema, context)
        File "/home/victor/dev/magicstack/edgedb/edb/schema/inheriting.py", line 622, in _create_innards
          return super()._create_innards(schema, context)
        File "/home/victor/dev/magicstack/edgedb/edb/schema/delta.py", line 1498, in _create_innards
          schema = op.apply(schema, context=context)
        File "/home/victor/dev/magicstack/edgedb/edb/schema/delta.py", line 1527, in apply
          schema = self._create_begin(schema, context)
        File "/home/victor/dev/magicstack/edgedb/edb/schema/constraints.py", line 641, in _create_begin
          constr_base, attrs, inh = Constraint.get_concrete_constraint_attrs(
        File "/home/victor/dev/magicstack/edgedb/edb/schema/constraints.py", line 226, in get_concrete_constraint_attrs
          args_map = qlutils.index_parameters(
        File "/home/victor/dev/magicstack/edgedb/edb/edgeql/utils.py", line 92, in index_parameters
          result[p.get_parameter_name(schema)] = e
      AttributeError: 'NoneType' object has no attribute 'get_parameter_name'
```



Discussion:
Well, this one is independent of emojis, it doesn't work with "normal" names either.
The problem is that you didn't specify any args and the ddl compiling pipeline didn't check that the new constraint is therefore incompatible with what it extends.
Oh. I tested it with plain `max_len_value`, but not the derived constraint. Not a unicode bug (which is probably a good thing), but a bug with validation for sure.
I'm on this.

Resolving patch:
diff --git a/edb/edgeql/parser/grammar/rust_lexer.py b/edb/edgeql/parser/grammar/rust_lexer.py
--- a/edb/edgeql/parser/grammar/rust_lexer.py
+++ b/edb/edgeql/parser/grammar/rust_lexer.py
@@ -1,3 +1,22 @@
+#
+# This source file is part of the EdgeDB open source project.
+#
+# Copyright 2020-present MagicStack Inc. and the EdgeDB authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
 from __future__ import annotations
 
 from typing import Deque, Optional, Union, List
diff --git a/edb/edgeql/parser/grammar/sdl.py b/edb/edgeql/parser/grammar/sdl.py
--- a/edb/edgeql/parser/grammar/sdl.py
+++ b/edb/edgeql/parser/grammar/sdl.py
@@ -344,7 +344,7 @@ def reduce_CreateConstraint(self, *kids):
         self.val = qlast.CreateConstraint(
             name=kids[2].val,
             subject=kids[3].val,
-            extends=kids[4].val,
+            bases=kids[4].val,
         )
 
     def reduce_CreateConstraint_CreateFunctionArgs(self, *kids):
@@ -354,7 +354,7 @@ def reduce_CreateConstraint_CreateFunctionArgs(self, *kids):
             name=kids[2].val,
             params=kids[3].val,
             subject=kids[4].val,
-            extends=kids[5].val,
+            bases=kids[5].val,
         )
 
 
diff --git a/edb/schema/constraints.py b/edb/schema/constraints.py
--- a/edb/schema/constraints.py
+++ b/edb/schema/constraints.py
@@ -614,6 +614,110 @@ def _get_param_desc_from_ast(
 
         return params
 
+    def _validate_params(
+        self,
+        schema: s_schema.Schema,
+        context: sd.CommandContext,
+    ) -> None:
+        # Must be set after _create_begin
+        assert self.scls is not None
+        scls: Constraint = self.scls
+        # Must be an abstract constraint
+        assert scls.generic(schema)
+
+        base_params: Optional[s_func.FuncParameterList] = None
+        base_with_params: Optional[Constraint] = None
+
+        for base in scls.get_bases(schema).objects(schema):
+            params = base.get_params(schema)
+            if params and len(params) > 1:
+                # All constraints have __subject__ parameter
+                # auto-injected, hence the "> 1" check.
+                if base_params is not None:
+                    raise errors.InvalidConstraintDefinitionError(
+                        f'{scls.get_verbosename(schema)} '
+                        f'extends multiple constraints '
+                        f'with parameters',
+                        context=self.source_context,
+                    )
+                base_params = params
+                base_with_params = base
+
+        if base_params:
+            assert base_with_params is not None
+
+            params = scls.get_params(schema)
+            if not params or len(params) == 1:
+                # All constraints have __subject__ parameter
+                # auto-injected, hence the "== 1" check.
+                raise errors.InvalidConstraintDefinitionError(
+                    f'{scls.get_verbosename(schema)} '
+                    f'must define parameters to reflect parameters of '
+                    f'the {base_with_params.get_verbosename(schema)} '
+                    f'it extends',
+                    context=self.source_context,
+                )
+
+            if len(params) < len(base_params):
+                raise errors.InvalidConstraintDefinitionError(
+                    f'{scls.get_verbosename(schema)} '
+                    f'has fewer parameters than the '
+                    f'{base_with_params.get_verbosename(schema)} '
+                    f'it extends',
+                    context=self.source_context,
+                )
+
+            # Skipping the __subject__ param
+            for base_param, param in zip(base_params.objects(schema)[1:],
+                                         params.objects(schema)[1:]):
+
+                param_name = param.get_parameter_name(schema)
+                base_param_name = base_param.get_parameter_name(schema)
+
+                if param_name != base_param_name:
+                    raise errors.InvalidConstraintDefinitionError(
+                        f'the {param_name!r} parameter of the '
+                        f'{scls.get_verbosename(schema)} '
+                        f'must be renamed to {base_param_name!r} '
+                        f'to match the signature of the base '
+                        f'{base_with_params.get_verbosename(schema)} ',
+                        context=self.source_context,
+                    )
+
+                param_type = param.get_type(schema)
+                base_param_type = base_param.get_type(schema)
+
+                if (
+                    not base_param_type.is_polymorphic(schema)
+                    and param_type.is_polymorphic(schema)
+                ):
+                    raise errors.InvalidConstraintDefinitionError(
+                        f'the {param_name!r} parameter of the '
+                        f'{scls.get_verbosename(schema)} cannot '
+                        f'be of generic type because the corresponding '
+                        f'parameter of the '
+                        f'{base_with_params.get_verbosename(schema)} '
+                        f'it extends has a concrete type',
+                        context=self.source_context,
+                    )
+
+                if (
+                    not base_param_type.is_polymorphic(schema) and
+                    not param_type.is_polymorphic(schema) and
+                    not param_type.implicitly_castable_to(
+                        base_param_type, schema)
+                ):
+                    raise errors.InvalidConstraintDefinitionError(
+                        f'the {param_name!r} parameter of the '
+                        f'{scls.get_verbosename(schema)} has type of '
+                        f'{param_type.get_displayname(schema)} that '
+                        f'is not implicitly castable to the '
+                        f'corresponding parameter of the '
+                        f'{base_with_params.get_verbosename(schema)} with '
+                        f'type {base_param_type.get_displayname(schema)}',
+                        context=self.source_context,
+                    )
+
     def _create_begin(
         self,
         schema: s_schema.Schema,
@@ -621,7 +725,10 @@ def _create_begin(
     ) -> s_schema.Schema:
         referrer_ctx = self.get_referrer_context(context)
         if referrer_ctx is None:
-            return super()._create_begin(schema, context)
+            schema = super()._create_begin(schema, context)
+            if not context.canonical:
+                self._validate_params(schema, context)
+            return schema
 
         subject = referrer_ctx.scls
         assert isinstance(subject, ConsistencySubject)


Test patch:
diff --git a/tests/schemas/dump02_default.esdl b/tests/schemas/dump02_default.esdl
--- a/tests/schemas/dump02_default.esdl
+++ b/tests/schemas/dump02_default.esdl
@@ -18,7 +18,7 @@
 
 abstract annotation `🍿`;
 
-abstract constraint `🚀🍿` extending max_len_value;
+abstract constraint `🚀🍿`(max: int64) extending max_len_value;
 
 function `💯`(NAMED ONLY `🙀`: int64) -> int64 {
     using (
@@ -40,8 +40,9 @@ type A {
 
 scalar type 你好 extending str;
 
-scalar type مرحبا extending 你好;
-    # constraint `🚀🍿`(10);
+scalar type مرحبا extending 你好 {
+    constraint `🚀🍿`(100);
+};
 
 scalar type `🚀🚀🚀` extending مرحبا;
 
@@ -53,7 +54,8 @@ type Łukasz {
 
     link `Ł💯` -> A {
         property `🙀🚀🚀🚀🙀` -> `🚀🚀🚀`;
-        property `🙀مرحبا🙀` -> مرحبا;
-            # constraint `🚀🍿`(2);
+        property `🙀مرحبا🙀` -> مرحبا {
+            constraint `🚀🍿`(200);
+        }
     };
 }
diff --git a/tests/test_dump02.py b/tests/test_dump02.py
--- a/tests/test_dump02.py
+++ b/tests/test_dump02.py
@@ -215,6 +215,32 @@ async def _ensure_schema_data_integrity(self):
             ]
         )
 
+        # Check that abstract constraint was applied properly
+        await self.assert_query_result(
+            r'''
+                WITH MODULE schema
+                SELECT Constraint {
+                    name,
+                    params: {
+                        @value
+                    } FILTER .num > 0
+                }
+                FILTER
+                    .name = 'default::🚀🍿' AND
+                    NOT .is_abstract AND
+                    Constraint.<constraints[IS ScalarType].name =
+                        'default::🚀🚀🚀';
+            ''',
+            [
+                {
+                    'name': 'default::🚀🍿',
+                    'params': [
+                        {'@value': '100'}
+                    ]
+                },
+            ]
+        )
+
         # Check the default value
         await self.con.execute(r'INSERT Łukasz')
         await self.assert_query_result(
diff --git a/tests/test_edgeql_ddl.py b/tests/test_edgeql_ddl.py
--- a/tests/test_edgeql_ddl.py
+++ b/tests/test_edgeql_ddl.py
@@ -4282,6 +4282,21 @@ async def test_edgeql_ddl_constraint_alter_04(self):
             }]
         )
 
+    async def test_edgeql_ddl_constraint_04(self):
+        # Regression test for #1441.
+        with self.assertRaisesRegex(
+            edgedb.InvalidConstraintDefinitionError,
+            "must define parameters"
+        ):
+            async with self._run_and_rollback():
+                await self.con.execute('''
+                    CREATE ABSTRACT CONSTRAINT aaa EXTENDING max_len_value;
+
+                    CREATE SCALAR TYPE foo EXTENDING str {
+                        CREATE CONSTRAINT aaa(10);
+                    };
+                ''')
+
     async def test_edgeql_ddl_drop_inherited_link(self):
         await self.con.execute(r"""
             CREATE TYPE test::Target;
diff --git a/tests/test_schema.py b/tests/test_schema.py
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -584,6 +584,111 @@ def test_schema_constraint_inheritance_04(self):
             'there should be no constraints on alias links or properties',
         )
 
+    @tb.must_fail(errors.InvalidConstraintDefinitionError,
+                  "abstract constraint 'test::aaa' must define parameters "
+                  "to reflect parameters of the abstract constraint "
+                  "'std::max_len_value' it extends",
+                  line=2, col=13)
+    def test_schema_constraint_inheritance_05(self):
+        """
+            abstract constraint aaa extending std::max_len_value;
+        """
+
+    @tb.must_fail(errors.InvalidConstraintDefinitionError,
+                  "abstract constraint 'test::zzz' extends multiple "
+                  "constraints with parameters",
+                  line=8, col=13)
+    def test_schema_constraint_inheritance_06(self):
+        """
+            abstract constraint aaa(max: std::int64)
+                extending std::max_len_value;
+
+            abstract constraint bbb(min: std::int64)
+                extending std::min_len_value;
+
+            abstract constraint zzz(max: std::int64)
+                extending aaa, bbb;
+        """
+
+    @tb.must_fail(errors.InvalidConstraintDefinitionError,
+                  "abstract constraint 'test::zzz' has fewer parameters "
+                  "than the abstract constraint 'test::aaa' it extends",
+                  line=5, col=13)
+    def test_schema_constraint_inheritance_07(self):
+        """
+            abstract constraint aaa(max: std::int64, foo: str)
+                extending std::max_len_value;
+
+            abstract constraint zzz(max: std::int64)
+                extending aaa;
+        """
+
+    def test_schema_constraint_inheritance_08(self):
+        """
+            abstract constraint aaa(max: std::int64)
+                extending std::max_len_value;
+
+            abstract constraint zzz(max: std::int64)
+                extending aaa;
+        """
+
+    @tb.must_fail(errors.InvalidConstraintDefinitionError,
+                  "abstract constraint 'test::zzz' has fewer parameters "
+                  "than the abstract constraint 'test::aaa' it extends",
+                  line=5, col=13)
+    def test_schema_constraint_inheritance_09(self):
+        """
+            abstract constraint aaa(max: std::int64, foo: str)
+                extending std::max_len_value;
+
+            abstract constraint zzz(max: std::str)
+                extending aaa;
+        """
+
+    @tb.must_fail(errors.InvalidConstraintDefinitionError,
+                  "the 'min' parameter of the abstract constraint 'test::zzz' "
+                  "must be renamed to 'max' to match the signature of the "
+                  "base abstract constraint 'test::aaa'",
+                  line=5, col=13)
+    def test_schema_constraint_inheritance_10(self):
+        """
+            abstract constraint aaa(max: std::int64)
+                extending std::max_len_value;
+
+            abstract constraint zzz(min: std::str)
+                extending aaa;
+        """
+
+    @tb.must_fail(errors.InvalidConstraintDefinitionError,
+                  "the 'max' parameter of the abstract constraint 'test::zzz' "
+                  "has type of std::str that is not implicitly castable to "
+                  "the corresponding parameter of the abstract constraint "
+                  "'test::aaa' with type std::int64",
+                  line=5, col=13)
+    def test_schema_constraint_inheritance_11(self):
+        """
+            abstract constraint aaa(max: std::int64)
+                extending std::max_len_value;
+
+            abstract constraint zzz(max: std::str)
+                extending aaa;
+        """
+
+    @tb.must_fail(errors.InvalidConstraintDefinitionError,
+                  "the 'max' parameter of the abstract constraint 'test::zzz' "
+                  "cannot be of generic type because the corresponding "
+                  "parameter of the abstract constraint 'test::aaa' it "
+                  "extends has a concrete type",
+                  line=5, col=13)
+    def test_schema_constraint_inheritance_12(self):
+        """
+            abstract constraint aaa(max: std::int64)
+                extending std::max_len_value;
+
+            abstract constraint zzz(max: anyscalar)
+                extending aaa;
+        """
+
     def test_schema_ref_diamond_inheritance(self):
         schema = tb._load_std_schema()