Repository: edgedb/edgedb

Issue:
Prohibit inheriting scalar types from more than one non-abstract scalar type
Currently:

```
select> CREATE SCALAR TYPE test::myint EXTENDING std::int64, std::str;
OK: CREATE
```

We have to prohibit this by enforcing that there must be exactly one base non-abstract scalar type in the ancestry of a new non-abstract scalar type.


Discussion:
Why that rule instead of requiring a single root scalar type?
> Why that rule instead of requiring a single root scalar type?

Because multiple inheritance from different abstract scalar types is useful.  For example:

```
CREATE SCALAR TYPE std::bigint EXTENDING std::anynumeric, std::anyint;
```


Resolving patch:
diff --git a/edb/schema/scalars.py b/edb/schema/scalars.py
--- a/edb/schema/scalars.py
+++ b/edb/schema/scalars.py
@@ -222,7 +222,51 @@ class ScalarTypeCommand(
     schema_metaclass=ScalarType,
     context_class=ScalarTypeCommandContext,
 ):
-    pass
+    def validate_scalar_ancestors(
+        self,
+        ancestors: Sequence[so.SubclassableObject],
+        schema: s_schema.Schema,
+        context: sd.CommandContext,
+    ) -> None:
+        concrete_ancestors = {
+            ancestor for ancestor in ancestors
+            if not ancestor.get_is_abstract(schema)
+        }
+        # Filter out anything that has a subclass relation with
+        # every other concrete ancestor. This lets us allow chains
+        # of concrete scalar types while prohibiting diamonds (for
+        # example if X <: A, B <: int64 where A, B are concrete).
+        # (If we wanted to allow diamonds, we could instead filter out
+        # anything that has concrete bases.)
+        concrete_ancestors = {
+            c1 for c1 in concrete_ancestors
+            if not all(c1 == c2 or c1.issubclass(schema, c2)
+                       or c2.issubclass(schema, c1)
+                       for c2 in concrete_ancestors)
+        }
+
+        if len(concrete_ancestors) > 1:
+            raise errors.SchemaError(
+                f'scalar type may not have more than '
+                f'one concrete base type',
+                context=self.source_context,
+            )
+
+    def validate_scalar_bases(
+        self,
+        schema: s_schema.Schema,
+        context: sd.CommandContext,
+    ) -> None:
+        bases = self.get_resolved_attribute_value(
+            'bases', schema=schema, context=context)
+
+        if bases:
+            ancestors = []
+            for base in bases.objects(schema):
+                ancestors.append(base)
+                ancestors.extend(base.get_ancestors(schema).objects(schema))
+
+            self.validate_scalar_ancestors(ancestors, schema, context)
 
 
 class CreateScalarType(
@@ -295,6 +339,14 @@ def _cmd_tree_from_ast(
 
         return cmd
 
+    def validate_create(
+        self,
+        schema: s_schema.Schema,
+        context: sd.CommandContext,
+    ) -> None:
+        super().validate_create(schema, context)
+        self.validate_scalar_bases(schema, context)
+
     def _get_ast_node(
         self,
         schema: s_schema.Schema,
@@ -361,6 +413,24 @@ def apply(
         else:
             return super().apply(schema, context)
 
+    def validate_scalar_bases(
+        self,
+        schema: s_schema.Schema,
+        context: sd.CommandContext,
+    ) -> None:
+        super().validate_scalar_bases(schema, context)
+
+        bases = self.get_resolved_attribute_value(
+            'bases', schema=schema, context=context)
+        if bases:
+            obj = self.scls
+            # For each descendant, compute its new ancestors and check
+            # that they are valid for a scalar type.
+            new_schema = obj.set_field_value(schema, 'bases', bases)
+            for desc in obj.descendants(schema):
+                ancestors = so.compute_ancestors(new_schema, desc)
+                self.validate_scalar_ancestors(ancestors, schema, context)
+
     def _validate_enum_change(
         self,
         stype: s_types.Type,
@@ -396,6 +466,15 @@ class AlterScalarType(
 ):
     astnode = qlast.AlterScalarType
 
+    def validate_alter(
+        self,
+        schema: s_schema.Schema,
+        context: sd.CommandContext,
+    ) -> None:
+        super().validate_alter(schema, context)
+        for subcmd in self.get_subcommands(type=RebaseScalarType):
+            subcmd.validate_scalar_bases(schema, context)
+
 
 class DeleteScalarType(
     ScalarTypeCommand,


Test patch:
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
@@ -7508,3 +7508,15 @@ async def test_edgeql_migration_rename_type_03(self):
         """)
 
         await self.migrate("")
+
+    async def test_edgeql_migration_invalid_scalar_01(self):
+        with self.assertRaisesRegex(
+                edgedb.SchemaError,
+                r"may not have more than one concrete base type"):
+            await self.con.execute(r"""
+                START MIGRATION TO {
+                    abstract scalar type test::lol extending str;
+                    scalar type test::myint extending int64, test::lol;
+                };
+                POPULATE MIGRATION;
+            """)
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
@@ -2967,6 +2967,81 @@ async def test_edgeql_ddl_operator_11(self):
                 };
             ''')
 
+    async def test_edgeql_ddl_scalar_01(self):
+        with self.assertRaisesRegex(
+                edgedb.SchemaError,
+                r'may not have more than one concrete base type'):
+            await self.con.execute('''
+                CREATE SCALAR TYPE test::myint EXTENDING std::int64, std::str;
+            ''')
+
+    async def test_edgeql_ddl_scalar_02(self):
+        await self.con.execute('''
+            CREATE ABSTRACT SCALAR TYPE test::a EXTENDING std::int64;
+            CREATE ABSTRACT SCALAR TYPE test::b EXTENDING std::str;
+        ''')
+
+        with self.assertRaisesRegex(
+                edgedb.SchemaError,
+                r'may not have more than one concrete base type'):
+            await self.con.execute('''
+                CREATE SCALAR TYPE test::myint EXTENDING test::a, test::b;
+            ''')
+
+    async def test_edgeql_ddl_scalar_03(self):
+        await self.con.execute('''
+            CREATE ABSTRACT SCALAR TYPE test::a EXTENDING std::int64;
+            CREATE ABSTRACT SCALAR TYPE test::b EXTENDING std::str;
+            CREATE SCALAR TYPE test::myint EXTENDING test::a;
+        ''')
+
+        with self.assertRaisesRegex(
+                edgedb.SchemaError,
+                r'may not have more than one concrete base type'):
+            await self.con.execute('''
+                ALTER SCALAR TYPE test::myint EXTENDING test::b;
+            ''')
+
+    async def test_edgeql_ddl_scalar_04(self):
+        await self.con.execute('''
+            CREATE ABSTRACT SCALAR TYPE test::a;
+            CREATE SCALAR TYPE test::myint EXTENDING int64, test::a;
+        ''')
+
+        with self.assertRaisesRegex(
+                edgedb.SchemaError,
+                r'may not have more than one concrete base type'):
+            await self.con.execute('''
+                ALTER SCALAR TYPE test::a EXTENDING str;
+            ''')
+
+    async def test_edgeql_ddl_scalar_05(self):
+        await self.con.execute('''
+            CREATE ABSTRACT SCALAR TYPE test::a EXTENDING std::int64;
+            CREATE ABSTRACT SCALAR TYPE test::b EXTENDING std::int64;
+            CREATE SCALAR TYPE test::myint EXTENDING test::a, test::b;
+        ''')
+
+    async def test_edgeql_ddl_scalar_06(self):
+        await self.con.execute('''
+            CREATE SCALAR TYPE test::myint EXTENDING int64;
+            CREATE SCALAR TYPE test::myint2 EXTENDING test::myint;
+        ''')
+
+    async def test_edgeql_ddl_scalar_07(self):
+        await self.con.execute('''
+            CREATE SCALAR TYPE test::a EXTENDING std::str;
+            CREATE SCALAR TYPE test::b EXTENDING std::str;
+        ''')
+
+        # I think we want to prohibit this kind of diamond pattern
+        with self.assertRaisesRegex(
+                edgedb.SchemaError,
+                r'may not have more than one concrete base type'):
+            await self.con.execute('''
+                CREATE SCALAR TYPE test::myint EXTENDING test::a, test::b;
+            ''')
+
     async def test_edgeql_ddl_cast_01(self):
         await self.con.execute('''
             CREATE SCALAR TYPE test::type_a EXTENDING std::str;