Repository: edgedb/edgedb

Issue:
ISE when migrating to a schema
```
        File "/home/aljaz/EdgeDB/edgedb/edb/edgeql/compiler/setgen.py", line 472, in compile_path
          path_tip = tuple_indirection_set(
                     ^^^^^^^^^^^^^^^^^^^^^^
        File "/home/aljaz/EdgeDB/edgedb/edb/edgeql/compiler/setgen.py", line 1142, in tuple_indirection_set
          el_type = source.get_subtype(ctx.env.schema, el_name)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/home/aljaz/EdgeDB/edgedb/edb/schema/types.py", line 1730, in get_subtype
          raise errors.InvalidReferenceError(
      edb.errors.InvalidReferenceError: foo is not a member of tuple<__type__: std::bool, id: std::bool, bar: std::bool>
```

Steps to Reproduce:

1. `edb cli migration create`

Schema:

```
module default {

    type User {
        property foo -> bool {
          rewrite insert using (__specified__.foo and .foo);
        };

        property bar -> bool {
          rewrite insert using (__specified__.foo);
        };
    };

}
```



Discussion:
#7373 does improve the error message for this, but it does not solve the core issue yet.

The provided schema is valid - we should be able to generate a migration to that schema.

Resolving patch:
diff --git a/edb/schema/expr.py b/edb/schema/expr.py
--- a/edb/schema/expr.py
+++ b/edb/schema/expr.py
@@ -20,6 +20,7 @@
 from __future__ import annotations
 from typing import (
     Any,
+    Callable,
     Optional,
     Tuple,
     Type,
@@ -210,6 +211,9 @@ def compiled(
         options: Optional[qlcompiler.CompilerOptions] = None,
         as_fragment: bool = False,
         detached: bool = False,
+        find_extra_refs: Optional[
+            Callable[[irast_.Set], set[so.Object]]
+        ] = None
     ) -> CompiledExpression:
 
         from edb.ir import ast as irast_
@@ -238,7 +242,12 @@ def compiled(
         assert isinstance(ir, irast_.Statement)
 
         # XXX: ref stuff - why doesn't it go into the delta tree? - temporary??
-        srefs = {ref for ref in ir.schema_refs if schema.has_object(ref.id)}
+        srefs: set[so.Object] = {
+            ref for ref in ir.schema_refs if schema.has_object(ref.id)
+        }
+
+        if find_extra_refs is not None:
+            srefs |= find_extra_refs(ir.expr)
 
         return CompiledExpression(
             text=self.text,
diff --git a/edb/schema/rewrites.py b/edb/schema/rewrites.py
--- a/edb/schema/rewrites.py
+++ b/edb/schema/rewrites.py
@@ -139,13 +139,15 @@ def compile_expr_field(
         track_schema_ref_exprs: bool = False,
     ) -> s_expr.CompiledExpression:
         if field.name == 'expr':
+            from edb.common import ast
+            from edb.ir import ast as irast
             from edb.ir import pathid
             from . import pointers as s_pointers
             from . import objtypes as s_objtypes
             from . import links as s_links
 
             parent_ctx = self.get_referrer_context_or_die(context)
-            pointer = parent_ctx.op.get_object(schema, context)
+            pointer = parent_ctx.op.scls
             assert isinstance(pointer, s_pointers.Pointer)
 
             source = pointer.get_source(schema)
@@ -205,6 +207,55 @@ def compile_expr_field(
 
             singletons = frozenset(anchors.values())
 
+            # If the `__specified__` anchor is used, create references to the
+            # matching pointers.
+            #
+            # These references are necessary in order to compute the dependency
+            # and ordering of Rewrite commands when producing DDL.
+            #
+            # If creating Type T with two properties, A and B, such that
+            # A has a Rewrite containing `__specified__.B`.
+            #
+            # Without the references, the DDL may look like:
+            # - Create Type T
+            #   - Create Property A
+            #     - Create Rewrite using __specified__.B
+            #   - Create Property B
+            #
+            # This will cause an issue when compiling the Rewrite. At that
+            # point, the schema will not know about B and so the tuple will not
+            # have element `.B`.
+            #
+            # The reference will cause the reordering of commands and the DDL
+            # may instead look like:
+            # - Create Object O
+            #   - Create Property A
+            #   - Create Property B
+            #   - Alter Property A
+            #     - Create Rewrite using __specified__.B
+            #
+            # With Create Rewrite ordered after Property B, the tuple for
+            # `__specified__` will correctly have element `.B`.
+            def find_extra_refs(ir_expr: irast.Set) -> set[so.Object]:
+                def find_specified(node: irast.TupleIndirectionPointer) -> bool:
+                    return node.source.anchor == '__specified__'
+
+                ref_ptr_names: set[str] = set()
+                for tuple_node in ast.find_children(
+                    ir_expr,
+                    irast.TupleIndirectionPointer,
+                    test_func=find_specified,
+                ):
+                    ref_ptr_names.add(tuple_node.ptrref.name.name)
+
+                ref_ptrs: set[so.Object] = set(
+                    pointer
+                    for pointer in subject.get_pointers(schema).objects(schema)
+                    if pointer.get_shortname(schema).name in ref_ptr_names
+                )
+
+                return ref_ptrs
+
             return type(value).compiled(
                 value,
                 schema=schema,
@@ -219,6 +270,7 @@ def compile_expr_field(
                     # in_ddl_context_name=in_ddl_context_name,
                     detached=True,
                 ),
+                find_extra_refs=find_extra_refs,
             )
         else:
             return super().compile_expr_field(


Test patch:
diff --git a/tests/test_schema.py b/tests/test_schema.py
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -1641,6 +1641,21 @@ def test_schema_rewrite_order_01(self):
             }
         """
 
+    def test_schema_rewrite_order_02(self):
+        # One of the properties is going to reference the other property
+        # before it is created in its rewrite via __specified__.
+        # Ensure that this gets ordered correctly.
+        """
+            type User {
+              property foo -> bool {
+                rewrite insert using (__specified__.bar);
+              };
+              property bar -> bool {
+                rewrite insert using (__specified__.foo);
+              };
+            };
+        """
+
     def test_schema_scalar_order_01(self):
         # Make sure scalar types account for base types when tracing SDL
         # dependencies.
@@ -9111,6 +9126,44 @@ def test_schema_migrations_rewrites_01(self):
             """,
         ])
 
+    def test_schema_migrations_rewrites_02(self):
+        self._assert_migration_equivalence([
+            r"""
+                type User {
+                    property foo -> bool;
+                    property bar -> bool;
+                };
+            """,
+            r"""
+                type User {
+                    property foo -> bool {
+                        rewrite insert using (
+                            __specified__.bar and __specified__.baz
+                        );
+                    };
+                    property bar -> bool {
+                        rewrite insert using (
+                            __specified__.foo and __specified__.baz
+                        );
+                    };
+                    property baz -> bool {
+                        rewrite insert using (
+                            __specified__.foo and __specified__.bar
+                        );
+                    };
+                };
+            """,
+            r"""
+                type User {
+                    property foo -> bool;
+                    property bar -> bool;
+                    property baz -> bool;
+                };
+            """,
+            r"""
+            """,
+        ])
+
     def test_schema_migrations_implicit_type_01(self):
         self._assert_migration_equivalence([
             r"""