Repository: edgedb/edgedb

Issue:
Our ancestor resolution order isn't *quite* C3
I think that our ancestor ordering is intended to be the C3 linearization ordering (https://en.wikipedia.org/wiki/C3_linearization), which is what Python uses, but we don't include the list of base classes in the list of orders to merge.

I believe this results in us not always having the second of the C properties, "preservation of local precedence order" (which makes our algorithm one of several possible "C2" algorithms :P).

I think, though I may be wrong, that the cases in which we *don't* preserve local precedence order are all cases that would be rejected by the C3 algorithm.

I'd like to fix this, but it might not be worth the compatibility break. No idea how common this will be in user code. Probably not that common.

```
with X := schema::InheritingObject
select X {
  name,
  bases: {name, @index} order by @index,
  ancestors: {name, @index} filter X.ancestors in X.bases order by @index,
  mismatch := (array_agg((select .bases order by @index).name) != array_agg((select .ancestors {name} filter X.ancestors in X.bases order by @index).name)),
}
filter .mismatch;
```
will find all the object types in a schema that don't obey the ordering.

There are a few in the schema, and most seem to be caused by the reflection machinery adding in base classes that weren't written in the schema and not being careful about where in the list they get added.


Discussion:
Ideally we should document and fix this.  The release notes can have a section on porting and we can go as far as introducing a pre-flight check for CLI-driven upgrades using the introspection query above.
Well, this'll be a good chance to put our new tools for rewriting and squashing migrations into practice, then :)
(Also that query takes almost 20s, which is way more than makes sense to me)

Resolving patch:
diff --git a/edb/schema/objects.py b/edb/schema/objects.py
--- a/edb/schema/objects.py
+++ b/edb/schema/objects.py
@@ -3326,7 +3326,7 @@ def _merge_lineage(
                 break
         else:
             raise errors.SchemaError(
-                f"Could not find consistent ancestor order for {subject_name}"
+                f"could not find consistent ancestor order for {subject_name}"
             )
 
         result.append(candidate)
@@ -3346,6 +3346,7 @@ def _compute_lineage(
 
     for base in bases:
         lineage.append(_compute_lineage(schema, base, subject_name))
+    lineage.append(list(bases))
 
     return _merge_lineage(lineage, subject_name)
 
@@ -3358,8 +3359,15 @@ def compute_lineage(
     lineage = []
     for base in bases:
         lineage.append(_compute_lineage(schema, base, subject_name))
+    lineage.append(list(bases))
 
-    return _merge_lineage(lineage, subject_name)
+    try:
+        return _merge_lineage(lineage, subject_name)
+    except errors.SchemaError as e:
+        sbases = ', '.join(str(base.get_name(schema)) for base in bases)
+        details = f'type has specified bases: {sbases}'
+        e.set_hint_and_details(hint=e.hint, details=details)
+        raise
 
 
 def compute_ancestors(


Test patch:
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
@@ -7930,7 +7930,7 @@ async def test_edgeql_ddl_anytype_06(self):
     async def test_edgeql_ddl_extending_01(self):
         with self.assertRaisesRegex(
                 edgedb.SchemaError,
-                r"Could not find consistent ancestor order for "
+                r"could not find consistent ancestor order for "
                 r"object type 'default::Merged1'"):
 
             await self.con.execute(r"""
@@ -7944,17 +7944,17 @@ async def test_edgeql_ddl_extending_01(self):
             """)
 
     async def test_edgeql_ddl_extending_02(self):
-        await self.con.execute(r"""
-            CREATE TYPE ExtA2;
-            # Create two types with a different position of Object
-            # in the bases. This doesn't impact the linearized
-            # bases because Object is already implicitly included
-            # as the first element of the base types.
-            CREATE TYPE ExtC2 EXTENDING ExtA2, Object;
-            CREATE TYPE ExtD2 EXTENDING Object, ExtA2;
-            # extending from both of these types
-            CREATE TYPE Merged2 EXTENDING ExtC2, ExtD2;
-        """)
+        with self.assertRaisesRegex(
+                edgedb.SchemaError,
+                r"could not find consistent ancestor order for "
+                r"object type 'default::ExtD2'"):
+            await self.con.execute(r"""
+                CREATE TYPE ExtA2;
+                CREATE TYPE ExtC2 EXTENDING ExtA2, Object;
+                # This is busted because no ordering can be consistent
+                # with the base ordering here.
+                CREATE TYPE ExtD2 EXTENDING Object, ExtA2;
+            """)
 
     async def test_edgeql_ddl_extending_03(self):
         # Check that ancestors are recomputed properly on rebase.
@@ -8211,6 +8211,18 @@ async def test_edgeql_ddl_extending_06(self):
                 ALTER TYPE SomeObject6 EXTENDING FreeObject;
             """)
 
+    async def test_edgeql_ddl_extending_07(self):
+        await self.con.execute(r"""
+            create type A;
+            create type B extending A;
+        """)
+        with self.assertRaisesRegex(
+                edgedb.SchemaError,
+                r"could not find consistent ancestor order"):
+            await self.con.execute(r"""
+                create type C extending A, B;
+            """)
+
     async def test_edgeql_ddl_modules_01(self):
         try:
             await self.con.execute(r"""
diff --git a/tests/test_schema.py b/tests/test_schema.py
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -1384,7 +1384,7 @@ def test_schema_ref_diamond_inheritance(self):
             CREATE MODULE default;
             CREATE TYPE default::A;
             CREATE TYPE default::B EXTENDING A;
-            CREATE TYPE default::C EXTENDING A, B;
+            CREATE TYPE default::C EXTENDING B, A;
         ''')
 
         orig_get_children = type(schema).get_children
@@ -7775,7 +7775,7 @@ def test_schema_migrations_half_diamonds_03(self):
                 abstract type C extending B;
                 abstract type C2 extending C;
                 abstract type D;
-                type F extending C, C2, B;
+                type F extending C2, C, B;
             """,
             r"""
             """,
@@ -7807,8 +7807,8 @@ def test_schema_migrations_half_diamonds_05(self):
                 abstract type C extending B;
                 abstract type C2 extending C;
                 abstract type D;
-                type F extending C, C2, B;
-                type F2 extending C, C2, B, F;
+                type F extending C2, C, B;
+                type F2 extending F, C2, C, B;
             """,
             r"""
             """,