Repository: edgedb/edgedb

Issue:
Can't create exclusive constraint on tuple properties
I got this crash when applying a migration from a schema change I just made. I'm not sure I typed invalid EdgeQL code somehow, or if the code simply triggered a bug code path in the CLI tool.

<!-- Please search existing issues to avoid creating duplicates. -->
- EdgeDB Version: EdgeDB CLI 1.1.0+8bc2c20
- OS Version: macOS 10.14.6.

Related issues:

- https://github.com/edgedb/edgedb/issues/3521. This one was fixed some days ago, but my bug is still crashing on the same line of code (line 309, in `worker.py`).

Steps to Reproduce:

1. Apply a certain migration.
2. Get crash: `InternalServerError: 'ColumnRef' object has no attribute 'args'`.

<details>
<summary>Full error</summary >

```
edgedb error: InternalServerError: 'ColumnRef' object has no attribute 'args'
  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 "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/server/compiler_pool/worker.py", line 309, in worker
          res = meth(*args)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/server/compiler_pool/worker.py", line 202, in compile_in_tx
          units, cstate = COMPILER.compile_in_tx(cstate, *args, **kwargs)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/server/compiler/compiler.py", line 2137, in compile_in_tx
          return self._compile(ctx=ctx, source=source), ctx.state
        File "/Users/Johan/Library/Application Support/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 "/Users/Johan/Library/Application Support/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 "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/server/compiler/compiler.py", line 1593, in _compile_dispatch_ql
          query = self._compile_ql_migration(ctx, ql)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/server/compiler/compiler.py", line 943, in _compile_ql_migration
          query = self._compile_and_apply_ddl_stmt(ctx, ql)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/server/compiler/compiler.py", line 860, in _compile_and_apply_ddl_stmt
          block, new_types = self._process_delta(ctx, delta)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/server/compiler/compiler.py", line 341, in _process_delta
          schema = pgdelta.apply(schema, context)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/pgsql/delta.py", line 5791, in apply
          schema = super().apply(schema, context)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/schema/delta.py", line 1506, in apply
          schema = self.apply_subcommands(schema, context)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/pgsql/delta.py", line 143, in apply_subcommands
          schema = super().apply_subcommands(schema, context)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/schema/delta.py", line 1533, in apply_subcommands
          schema = objop.apply(schema, context)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/schema/delta.py", line 2953, in apply
          schema = self._create_innards(schema, context)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/schema/delta.py", line 2921, in _create_innards
          return self.apply_subcommands(schema, context)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/pgsql/delta.py", line 143, in apply_subcommands
          schema = super().apply_subcommands(schema, context)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/schema/delta.py", line 946, in apply_subcommands
          schema = op.apply(schema, context=context)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/pgsql/delta.py", line 2956, in apply
          schema = super().apply(schema, context=context)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/schema/delta.py", line 3421, in apply
          schema = self._alter_innards(schema, context)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/schema/delta.py", line 3050, in _alter_innards
          return self.apply_subcommands(schema, context)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/pgsql/delta.py", line 143, in apply_subcommands
          schema = super().apply_subcommands(schema, context)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/schema/delta.py", line 946, in apply_subcommands
          schema = op.apply(schema, context=context)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/schema/delta.py", line 2953, in apply
          schema = self._create_innards(schema, context)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/schema/referencing.py", line 1105, in _create_innards
          return super()._create_innards(schema, context)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/schema/inheriting.py", line 711, in _create_innards
          return super()._create_innards(schema, context)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/schema/delta.py", line 2921, in _create_innards
          return self.apply_subcommands(schema, context)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/pgsql/delta.py", line 143, in apply_subcommands
          schema = super().apply_subcommands(schema, context)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/schema/delta.py", line 946, in apply_subcommands
          schema = op.apply(schema, context=context)
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/pgsql/delta.py", line 1767, in apply
          op = self.create_constraint(
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/pgsql/delta.py", line 1730, in create_constraint
          bconstr = schemac_to_backendc(
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/pgsql/schemamech.py", line 249, in schema_constraint_to_backend_constraint
          exprdata = cls._edgeql_ref_to_pg_constr(
        File "/Users/Johan/Library/Application Support/edgedb/portable/1.0/lib/python3.10/site-packages/edb/pgsql/schemamech.py", line 128, in _edgeql_ref_to_pg_constr
          for elem in sql_expr.args:
      AttributeError: 'ColumnRef' object has no attribute 'args'
```

</details>

This is the migration applied:

```edgeql
CREATE MIGRATION m1vnhv5wflmooetol65tflzojfs4suaorwvezlqmp2ozbgde5xrrxa
    ONTO m1fse57hjkh43rvap24cfduz5b4uzxm5zqjcm364ryhzqihcjloehq
{
  ALTER TYPE default::Grp {
      DROP PROPERTY label;
  };
  CREATE SCALAR TYPE default::Label EXTENDING std::str {
      CREATE CONSTRAINT std::max_len_value(1);
      CREATE CONSTRAINT std::regexp('[A-Z]');
  };
  ALTER TYPE default::Grp {
      CREATE REQUIRED PROPERTY meta -> tuple<label: default::Label, recurr: default::Recurr> {
          SET REQUIRED USING (SELECT
              ('A', 'Daily')
          );
          CREATE CONSTRAINT std::exclusive;
      };
  };
  ALTER TYPE default::Grp {
      CREATE PROPERTY label := (.meta.label);
      ALTER PROPERTY recurr {
          DROP CONSTRAINT std::exclusive;
      };
      ALTER PROPERTY recurr {
          USING (.meta.recurr);
          RESET OPTIONALITY;
      };
  };
};
```

<details>
<summary>Full schema (which triggered the migration)</summary>

```
module default {
    scalar type Recurr extending enum<Daily, Weekly, Monthly, HalfYearly, Yearly>;

    # A, B, C, ...
    scalar type Label extending str {
        constraint max_len_value(1);
        constraint regexp(r'[A-Z]');
    }

    type Contact {
        required property name -> str;
        required link grp -> Grp;
    }

    # "Group" is a reserved word, so "Grp" it is
    type Grp {
        required property meta -> tuple<label: Label, recurr: Recurr> {
            constraint exclusive;
        };

        # Computed
        property label := .meta.label;
        property recurr := .meta.recurr;
        multi link contacts := .< grp[is Contact]
    }
}
```

</details>


Discussion:
Ok, through trial and error I've nailed down the bug to be triggered by this schema change:

```diff
module default {
    scalar type Recurr extending enum<Daily, Weekly, Monthly, HalfYearly, Yearly>;
    scalar type Label extending str;

    type Grp {
+       required property meta -> tuple<label: Label, recurr: Recurr> {
+           constraint exclusive;
+       };
    }
}
```

causing this final migration:

```
CREATE MIGRATION m1ectij6w7r23ueokv5ux5mz6d3iaii27jhcuteubvluaa6wwar3pa
    ONTO m1ixvlq4zenyzwlbviqgtfdy5umldxh3clqu7s7scklq5nwnffjuaq
{
  ALTER TYPE default::Grp {
      CREATE REQUIRED PROPERTY meta -> tuple<label: default::Label, recurr: default::Recurr> {
          SET REQUIRED USING (SELECT
              ('A', 'Daily')
          );
          CREATE CONSTRAINT std::exclusive;
      };
  };
};
```
> This one was fixed some days ago, but my bug is still crashing on the same line of code (line 309, in worker.py).

Just in case it ever might be useful to know: the crashing line is the *bottommost* of the stack trace (line 128 in schemamech.py). Basically every edgedb compiler bug will have its topmost frame be in the worker.

Resolving patch:
diff --git a/edb/pgsql/schemamech.py b/edb/pgsql/schemamech.py
--- a/edb/pgsql/schemamech.py
+++ b/edb/pgsql/schemamech.py
@@ -87,14 +87,15 @@ def _edgeql_ref_to_pg_constr(cls, subject, origin_subject, tree, schema):
         if isinstance(tree.expr, irast.SelectStmt):
             tree = tree.expr.result
 
-        is_multicol = irtyputils.is_tuple(tree.typeref)
+        is_multicol = isinstance(sql_expr, (
+            pg_ast.RowExpr, pg_ast.ImplicitRowExpr))
 
         # Determine if the sequence of references are all simple refs, not
         # expressions.  This influences the type of Postgres constraint used.
         #
         is_trivial = (
             isinstance(sql_expr, pg_ast.ColumnRef) or (
-                isinstance(sql_expr, pg_ast.ImplicitRowExpr) and all(
+                is_multicol and all(
                     isinstance(el, pg_ast.ColumnRef)
                     for el in sql_expr.args)))
 


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
@@ -8392,6 +8392,26 @@ async def test_edgeql_ddl_constraint_15(self):
                     INSERT Bar { children := Foo };
                 """)
 
+    async def test_edgeql_ddl_constraint_16(self):
+        await self.con.execute(r"""
+            create type Foo {
+                create property x -> tuple<x: str, y: str> {
+                    create constraint exclusive;
+                }
+             };
+        """)
+
+        await self.con.execute("""
+            INSERT Foo { x := ('1', '2') };
+        """)
+
+        async with self.assertRaisesRegexTx(
+                edgedb.ConstraintViolationError,
+                r'x violates exclusivity constraint'):
+            await self.con.execute("""
+                INSERT Foo { x := ('1', '2') };
+            """)
+
     async def test_edgeql_ddl_constraint_alter_01(self):
         await self.con.execute(r"""
             CREATE TYPE ConTest01 {