Repository: edgedb/edgedb

Issue:
Setting a type as abstract preserves existing data
```
edgedb> create type Foo;
OK: CREATE TYPE
edgedb> insert Foo;
{default::Foo {id: 6d4cab68-8528-11ec-b417-ab253b541761}}
edgedb> alter type Foo { set abstract; };
OK: ALTER TYPE
edgedb> select Foo;
{default::Foo {id: 6d4cab68-8528-11ec-b417-ab253b541761}}
```

Since it isn't possible to insert into an abstract type, I would expect that the abstract type shouldn't be able to contain data either?

It seems that on the pgsql schema side we don't have any special handling of abstract types at all, and they get tables and everything.


Discussion:
I think it should be an error to convert a non-empty type.
Oh, yeah, I think that is clearly right.

Resolving patch:
diff --git a/edb/pgsql/delta.py b/edb/pgsql/delta.py
--- a/edb/pgsql/delta.py
+++ b/edb/pgsql/delta.py
@@ -2952,6 +2952,7 @@ def apply(
         schema: s_schema.Schema,
         context: sd.CommandContext,
     ) -> s_schema.Schema:
+        orig_schema = schema
         schema = super().apply(schema, context=context)
         objtype = self.scls
 
@@ -2960,6 +2961,8 @@ def apply(
         self.table_name = common.get_backend_name(
             schema, objtype, catenate=False)
 
+        self._maybe_do_abstract_test(orig_schema, schema, context)
+
         if has_table(objtype, schema):
             self.attach_alter_table(context)
 
@@ -2969,6 +2972,39 @@ def apply(
 
         return schema
 
+    def _maybe_do_abstract_test(
+        self,
+        orig_schema: s_schema.Schema,
+        schema: s_schema.Schema,
+        context: sd.CommandContext,
+    ) -> s_schema.Schema:
+        orig_abstract = self.scls.get_abstract(orig_schema)
+        new_abstract = self.scls.get_abstract(schema)
+        if orig_abstract or not new_abstract:
+            return
+
+        table = q(*common.get_backend_name(
+            schema,
+            self.scls,
+            catenate=False,
+        ))
+
+        vn = self.scls.get_verbosename(schema)
+        check_qry = textwrap.dedent(f'''\
+            SELECT
+                edgedb.raise(
+                    NULL::text,
+                    'cardinality_violation',
+                    msg => {common.quote_literal(
+                            f"may not make non-empty {vn} abstract")},
+                    "constraint" => 'set abstract'
+                )
+            FROM {table}
+            INTO _dummy_text;
+        ''')
+
+        self.pgops.add(dbops.Query(check_qry))
+
 
 class DeleteObjectType(ObjectTypeMetaCommand,
                        adapts=s_objtypes.DeleteObjectType):
diff --git a/edb/server/compiler/errormech.py b/edb/server/compiler/errormech.py
--- a/edb/server/compiler/errormech.py
+++ b/edb/server/compiler/errormech.py
@@ -351,6 +351,9 @@ def _static_interpret_cardinality_violation(_code, err_details):
     elif err_details.constraint_name == 'std::assert_distinct':
         return errors.ConstraintViolationError(err_details.message)
 
+    elif err_details.constraint_name == 'set abstract':
+        return errors.ConstraintViolationError(err_details.message)
+
     return errors.InternalServerError(err_details.message)
 
 


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
@@ -12479,6 +12479,19 @@ async def test_edgeql_ddl_drop_multi_child_02(self):
             drop type Person;
         """)
 
+    async def test_edgeql_ddl_set_abstract_bogus_01(self):
+        await self.con.execute(r"""
+            create type Foo;
+            insert Foo;
+        """)
+
+        with self.assertRaisesRegex(
+                edgedb.ConstraintViolationError,
+                r"may not make non-empty object type 'default::Foo' abstract"):
+            await self.con.execute(r"""
+                alter type Foo set abstract;
+            """)
+
 
 class TestConsecutiveMigrations(tb.DDLTestCase):
     TRANSACTION_ISOLATION = False