Repository: edgedb/edgedb

Issue:
DROP MODULE doesn't delete the module contents
```
x3> create module test; create type test::foo;
OK: CREATE
OK: CREATE
x3> drop module test;
OK: DROP
x3> describe schema;
{
  'CREATE MODULE default IF NOT EXISTS;
CREATE TYPE test::foo;',
}
x3> create module test; create type test::foo;
OK: CREATE
ERROR: SchemaError: ObjectType <QualName test::foo> is already present in the schema <FlatSchema gen:1 at 0x7f738eabdcd0>
```

The objects are inaccessible while the module is gone, but will appear in `describe schema` and in introspection tables and if the module is recreated, all the objects in it are already there.


Discussion:

Resolving patch:
diff --git a/edb/schema/modules.py b/edb/schema/modules.py
--- a/edb/schema/modules.py
+++ b/edb/schema/modules.py
@@ -19,6 +19,8 @@
 
 from __future__ import annotations
 
+from typing import *
+
 from edb import errors
 
 from edb.edgeql import ast as qlast
@@ -73,3 +75,26 @@ class AlterModule(ModuleCommand, sd.AlterObject[Module]):
 
 class DeleteModule(ModuleCommand, sd.DeleteObject[Module]):
     astnode = qlast.DropModule
+
+    def _validate_legal_command(
+        self,
+        schema: s_schema.Schema,
+        context: sd.CommandContext,
+    ) -> None:
+        super()._validate_legal_command(schema, context)
+
+        # For now, we disallow deleting non-empty modules.
+
+        # Modules aren't actually stored with any direct linkage
+        # to the objects in them, so explicitly search for objects
+        # in the module (excluding the module itself).
+        has_objects = bool(any(schema.get_objects(
+            included_modules=[self.classname],
+            excluded_items=[self.classname],
+        )))
+
+        if has_objects:
+            vn = self.scls.get_verbosename(schema)
+            raise errors.SchemaError(
+                f'cannot drop {vn} because it is not empty'
+            )


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
@@ -6298,6 +6298,42 @@ async def test_edgeql_ddl_modules_03(self):
                 DROP MODULE test_other;
             """)
 
+    async def test_edgeql_ddl_modules_04(self):
+        await self.con.execute(r"""
+            CREATE MODULE test_other;
+
+            CREATE ABSTRACT TYPE test_other::Named {
+                CREATE REQUIRED PROPERTY name -> str;
+            };
+
+            CREATE ABSTRACT TYPE test_other::UniquelyNamed
+                EXTENDING test_other::Named
+            {
+                ALTER PROPERTY name {
+                    CREATE DELEGATED CONSTRAINT exclusive;
+                }
+            };
+
+            CREATE ABSTRACT ANNOTATION whatever;
+
+            CREATE TYPE test_other::Foo;
+            CREATE TYPE test_other::Bar {
+                CREATE LINK foo -> test_other::Foo;
+                CREATE ANNOTATION whatever := "huh";
+            };
+            ALTER TYPE test_other::Foo {
+                CREATE LINK bar -> test_other::Bar;
+            };
+        """)
+
+        async with self.assertRaisesRegexTx(
+            edgedb.SchemaError,
+            "cannot drop module 'test_other' because it is not empty",
+        ):
+            await self.con.execute(r"""
+                DROP MODULE test_other;
+            """)
+
     async def test_edgeql_ddl_extension_package_01(self):
         await self.con.execute(r"""
             CREATE EXTENSION PACKAGE foo_01 VERSION '1.0' {