Repository: edgedb/edgedb

Issue:
Drop support for type union operator for scalars.
At the moment it's possible to apply type union operator to scalars, like `int64 | bigint`. This functions less intuitively than the same operation for object types.
```
edgedb> select 5 is int64;
{true}
edgedb> select 5 is (int64 | bigint);
{false}
edgedb> select (introspect (int64 | float32).name);
{'std::float64'}
```
The issue is that what the operation seems to do is answer the question of "what type would be the result set if you try to UNION values of these two disparate types?"

For object types, all objects are fundamentally related and therefore compatible enough to combine into a single set. The union type may not have all the links and properties directly accessible, but the underlying objects are of their specific types and remain unchanged. In practice it can meaningfully be used to denote a link which can validly take otherwise mostly unrelated object types (say `link attachment -> File | Link | Receipt`).

For scalar types we _cast_ the values, changing them, in order to produce a homogeneous set. So all values in the resulting set are strictly of a specific scalar type without any connection to their "former" type. This makes finding practical usage for scalar type unions much harder.

In addition to the above, we might want to introduce discriminated unions of scalars and the way the current `|` works with scalars could be counter-productive to that.

For all of the above reasons we can restrict `|` and `&` to only work for object types and potentially relax this in the future as the need arises.


Discussion:

Resolving patch:
diff --git a/edb/edgeql/compiler/typegen.py b/edb/edgeql/compiler/typegen.py
--- a/edb/edgeql/compiler/typegen.py
+++ b/edb/edgeql/compiler/typegen.py
@@ -92,8 +92,27 @@ def _ql_typeexpr_to_type(
 
     elif isinstance(ql_t, qlast.TypeOp):
         if ql_t.op == '|':
-            return (_ql_typeexpr_to_type(ql_t.left, ctx=ctx) +
-                    _ql_typeexpr_to_type(ql_t.right, ctx=ctx))
+            # We need to validate that type ops are applied only to
+            # object types. So we check the base case here, when the
+            # left or right operand is a single type, because if it's
+            # a longer list, then we know that it was already composed
+            # of "|" or "&", or it is the result of inference by
+            # "typeof" and is a list of object types anyway.
+            left = _ql_typeexpr_to_type(ql_t.left, ctx=ctx)
+            right = _ql_typeexpr_to_type(ql_t.right, ctx=ctx)
+
+            if len(left) == 1 and not left[0].is_object_type():
+                raise errors.UnsupportedFeatureError(
+                    f'cannot use type operator {ql_t.op!r} with non-object '
+                    f'type {left[0].get_displayname(ctx.env.schema)}',
+                    context=ql_t.left.context)
+            if len(right) == 1 and not right[0].is_object_type():
+                raise errors.UnsupportedFeatureError(
+                    f'cannot use type operator {ql_t.op!r} with non-object '
+                    f'type {right[0].get_displayname(ctx.env.schema)}',
+                    context=ql_t.right.context)
+
+            return left + right
 
         raise errors.UnsupportedFeatureError(
             f'type operator {ql_t.op!r} is not implemented',


Test patch:
diff --git a/tests/test_edgeql_expressions.py b/tests/test_edgeql_expressions.py
--- a/tests/test_edgeql_expressions.py
+++ b/tests/test_edgeql_expressions.py
@@ -4805,12 +4805,75 @@ async def test_edgeql_expr_static_eval_casts_01(self):
             [],
         )
 
-    async def test_edgeql_normalization_missmatch_01(self):
+    async def test_edgeql_normalization_mismatch_01(self):
         with self.assertRaisesRegex(
                 edgedb.EdgeQLSyntaxError, "Unexpected type expression"):
 
             await self.con.query('SELECT <tuple<"">>1;')
 
+    async def test_edgeql_typeop_01(self):
+        with self.assertRaisesRegex(
+                edgedb.UnsupportedFeatureError,
+                "type operator '&' is not implemented",
+        ):
+            await self.con.query('select <Named & Owned>{};')
+
+    async def test_edgeql_typeop_02(self):
+        with self.assertRaisesRegex(
+                edgedb.UnsupportedFeatureError,
+                "cannot use type operator '|' with non-object type",
+        ):
+            await self.con.query('select 1 is (int64 | float64);')
+
+    async def test_edgeql_typeop_03(self):
+        with self.assertRaisesRegex(
+                edgedb.UnsupportedFeatureError,
+                "cannot use type operator '|' with non-object type",
+        ):
+            await self.con.query('select 1 is (Object | float64);')
+
+    async def test_edgeql_typeop_04(self):
+        with self.assertRaisesRegex(
+                edgedb.UnsupportedFeatureError,
+                "cannot use type operator '|' with non-object type",
+        ):
+            await self.con.query(
+                'select [1] is (array<int64> | array<float64>);')
+
+    async def test_edgeql_typeop_05(self):
+        with self.assertRaisesRegex(
+                edgedb.UnsupportedFeatureError,
+                "cannot use type operator '|' with non-object type",
+        ):
+            await self.con.query(
+                'select (1,) is (tuple<int64> | tuple<float64>);')
+
+    async def test_edgeql_typeop_06(self):
+        with self.assertRaisesRegex(
+                edgedb.UnsupportedFeatureError,
+                "cannot use type operator '|' with non-object type",
+        ):
+            await self.con.query(
+                'select [1] is (typeof [2] | typeof [2.2]);')
+
+    async def test_edgeql_typeop_07(self):
+        with self.assertRaisesRegex(
+                edgedb.UnsupportedFeatureError,
+                "cannot use type operator '|' with non-object type",
+        ):
+            await self.con.query(
+                'select (1,) is (typeof (2,) | typeof (2.2,));')
+
+    async def test_edgeql_typeop_08(self):
+        await self.assert_query_result(
+            'select {x := 1} is (typeof Issue.references | Object);',
+            {False}
+        )
+        await self.assert_query_result(
+            'select {x := 1} is (typeof Issue.references | BaseObject);',
+            {True}
+        )
+
     async def test_edgeql_assert_single_01(self):
         await self.con.execute("""
             INSERT User {