Repository: edgedb/edgedb

Issue:
Disallow volatile operations in schema computables
For example
```
type Foo {
    property rand := random();
}
```
or more excitingly `link best_friends := (SELECT .friends FILTER random() > .5)`
ought to both be disallowed.



Discussion:

Resolving patch:
diff --git a/edb/edgeql/compiler/inference/volatility.py b/edb/edgeql/compiler/inference/volatility.py
--- a/edb/edgeql/compiler/inference/volatility.py
+++ b/edb/edgeql/compiler/inference/volatility.py
@@ -118,13 +118,26 @@ def __infer_set(
     env: context.Environment,
 ) -> qltypes.Volatility:
     if ir.is_binding:
-        return STABLE
+        vol = STABLE
     elif ir.rptr is not None:
-        return infer_volatility(ir.rptr.source, env)
+        vol = infer_volatility(ir.rptr.source, env)
     elif ir.expr is not None:
-        return infer_volatility(ir.expr, env)
+        vol = infer_volatility(ir.expr, env)
     else:
-        return STABLE
+        vol = STABLE
+
+    # Cache our best-known as to this point volatility, to prevent
+    # infinite recursion.
+    env.inferred_volatility[ir] = vol
+
+    if ir.shape:
+        vol = _max_volatility([
+            _common_volatility(
+                (el.expr for el, _ in ir.shape if el.expr), env
+            ),
+            vol,
+        ])
+    return vol
 
 
 @_infer_volatility.register
diff --git a/edb/schema/expraliases.py b/edb/schema/expraliases.py
--- a/edb/schema/expraliases.py
+++ b/edb/schema/expraliases.py
@@ -20,6 +20,8 @@
 from __future__ import annotations
 from typing import *
 
+from edb import errors
+
 from edb.edgeql import ast as qlast
 from edb.edgeql import compiler as qlcompiler
 from edb.edgeql import qltypes
@@ -140,6 +142,14 @@ def _compile_alias_expr(
             ),
         )
 
+        if ir.volatility == qltypes.Volatility.Volatile:
+            srcctx = self.get_attribute_source_context('expr')
+            raise errors.SchemaDefinitionError(
+                f'volatile functions are not permitted in schema-defined '
+                f'computables',
+                context=srcctx
+            )
+
         context.cache_value((expr, classname), ir)
 
         return ir  # type: ignore
diff --git a/edb/schema/pointers.py b/edb/schema/pointers.py
--- a/edb/schema/pointers.py
+++ b/edb/schema/pointers.py
@@ -351,6 +351,16 @@ def _merge_types(
         return schema, current_target
 
 
+def _is_view_source(
+        source: Optional[so.Object], schema: s_schema.Schema) -> bool:
+    if isinstance(source, Pointer):
+        return _is_view_source(source.get_source(schema), schema)
+    elif isinstance(source, s_types.Type):
+        return source.is_view(schema)
+    else:
+        return False
+
+
 Pointer_T = TypeVar("Pointer_T", bound="Pointer")
 
 
@@ -1105,6 +1115,17 @@ def _parse_computable(
         if spec_required is None:
             self.set_attribute_value('required', required, computed=True)
 
+        if (
+            not _is_view_source(source, schema)
+            and expression.irast.volatility == qltypes.Volatility.Volatile
+        ):
+            srcctx = self.get_attribute_source_context('target')
+            raise errors.SchemaDefinitionError(
+                f'volatile functions are not permitted in schema-defined '
+                f'computables',
+                context=srcctx
+            )
+
         self.set_attribute_value('computable', True)
 
         return schema, target_shell, base


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
@@ -10853,6 +10853,67 @@ async def test_edgeql_ddl_dupe_link_storage_01(self):
             WITH W := (Bar UNION Baz), SELECT (W, W.foo.id);
         """)
 
+    async def test_edgeql_ddl_no_volatile_computable_01(self):
+        async with self.assertRaisesRegexTx(
+            edgedb.QueryError,
+            "volatile functions are not permitted in schema-defined "
+            "computables",
+        ):
+            await self.con.execute("""
+                CREATE TYPE test::Foo {
+                    CREATE PROPERTY foo := random();
+                }
+            """)
+
+        async with self.assertRaisesRegexTx(
+            edgedb.QueryError,
+            "volatile functions are not permitted in schema-defined "
+            "computables",
+        ):
+            await self.con.execute("""
+                CREATE TYPE test::Foo {
+                    CREATE PROPERTY foo := (SELECT stdgraphql::Query {
+                        asdf := random()
+                    }).asdf
+                }
+            """)
+
+        async with self.assertRaisesRegexTx(
+            edgedb.QueryError,
+            "volatile functions are not permitted in schema-defined "
+            "computables",
+        ):
+            await self.con.execute("""
+                CREATE TYPE test::Noob {
+                    CREATE MULTI LINK friends -> test::Noob;
+                    CREATE LINK best_friends := (
+                        SELECT .friends FILTER random() > 0.5
+                    );
+                }
+            """)
+
+        async with self.assertRaisesRegexTx(
+            edgedb.QueryError,
+            "volatile functions are not permitted in schema-defined "
+            "computables",
+        ):
+            await self.con.execute("""
+                CREATE TYPE test::Noob {
+                    CREATE LINK noob -> test::Noob {
+                        CREATE PROPERTY foo := random();
+                    }
+                }
+            """)
+
+        async with self.assertRaisesRegexTx(
+            edgedb.QueryError,
+            "volatile functions are not permitted in schema-defined "
+            "computables",
+        ):
+            await self.con.execute("""
+                CREATE ALIAS Asdf := Object { foo := random() };
+            """)
+
 
 class TestConsecutiveMigrations(tb.DDLTestCase):
     TRANSACTION_ISOLATION = False