Repository: sdv-dev/SDV

Issue:
Add an API for removing a table from the Metadata
### Problem Description
When writing a custom constraint, I may be supplying a transform function that modifies the original data (so as to meet the constraint logic). This transform function is allowed to do anything to the data -- such as adding or removing tables. Therefore after performing the transformation, the resulting data does not necessarily match the original metadata.

With the new CAG framework, I am required to specify how the metadata will be changing. In order to do that, I have to provide exactly what the updated metadata will look like. Currently, this requires me to convert my metadata to a dictionary, make updates to the dictionary, and then convert it back to the metadata.

This is what it looks like when I have to delete a table from the metadata:
```python
metadata_dict = metadata.to_dict()
del metadata_dict['tables']['MY_TABLE_NAME']
metadata = Metadata.load_from_dict(metadata_dict)
```

It will be helpful if I am able to programmatically do this via the Metadata API instead.

### Expected behavior
Create a new function in the Metadata API called `remove_table`. It will have the following parameters:
- (required) `table_name`: A string with the name of the table to remove

```python
metadata.remove_table(table_name='MY_TABLE_NAME')
```

As a result of calling this function, the following should change in the metadata:

- The table name and all of its corresponding metadata should be removed
- The table should should also be removed if it appears anywhere else in the metadata. This includes:
   - In the `'relationships'` part of the metadata: If it appears as the parent table name or child table name within any relationship, remove the relationship


Discussion:

Patch:
diff --git a/sdv/metadata/metadata.py b/sdv/metadata/metadata.py
index 4f6a31646..23c20ae1b 100644
--- a/sdv/metadata/metadata.py
+++ b/sdv/metadata/metadata.py
@@ -324,6 +324,33 @@ def add_column(self, column_name, table_name=None, **kwargs):
         table_name = self._handle_table_name(table_name)
         super().add_column(table_name, column_name, **kwargs)
 
+    def remove_table(self, table_name):
+        """Remove a table from the metadata.
+
+        This method removes a table from the metadata as well as all relationships that table
+        contains.
+
+        Args:
+            table_name (str):
+                The name of the table to remove.
+        """
+        self._validate_table_exists(table_name)
+
+        # Remove relationships
+        updated_relationships = []
+        for relationship in self.relationships:
+            relationship_tables = (
+                relationship['parent_table_name'],
+                relationship['child_table_name'],
+            )
+            if table_name not in relationship_tables:
+                updated_relationships.append(relationship)
+
+        self.relationships = updated_relationships
+
+        del self.tables[table_name]
+        self._multi_table_updated = True
+
     def add_column_relationship(
         self,
         relationship_type,


Test patch:
diff --git a/tests/integration/metadata/test_metadata.py b/tests/integration/metadata/test_metadata.py
index 648296e55..204785dd9 100644
--- a/tests/integration/metadata/test_metadata.py
+++ b/tests/integration/metadata/test_metadata.py
@@ -998,3 +998,114 @@ def test_validate_metadata_with_reused_foreign_keys():
     )
     with pytest.raises(InvalidMetadataError, match=error_msg):
         metadata.validate()
+
+
+def test_remove_table():
+    """Test that a table and all relationships it has are removed."""
+    # Setup
+    metadata_dict = {
+        'tables': {
+            'users': {
+                'columns': {
+                    'gender': {'sdtype': 'categorical'},
+                    'age': {'sdtype': 'numerical'},
+                    'name': {'sdtype': 'text'},
+                    'user_id': {'sdtype': 'id'},
+                },
+                'primary_key': 'user_id',
+            },
+            'transactions': {
+                'columns': {
+                    'user_id': {'sdtype': 'id'},
+                    'transaction_id': {'sdtype': 'id'},
+                    'product_id': {'sdtype': 'id'},
+                    'amount': {'sdtype': 'numerical'},
+                },
+                'primary_key': 'transaction_id',
+            },
+            'products': {
+                'columns': {
+                    'product_id': {'sdtype': 'id'},
+                    'cost': {'sdtype': 'numerical'},
+                    'weight': {'sdtype': 'numerical'},
+                    'manufacturer': {'sdtype': 'id'},
+                },
+                'primary_key': 'product_id',
+            },
+            'manufacturers': {
+                'columns': {
+                    'country': {'sdtype': 'categorical'},
+                    'address': {'sdtype': 'text'},
+                    'id': {'sdtype': 'id'},
+                },
+                'primary_key': 'id',
+            },
+        },
+        'relationships': [
+            {
+                'parent_table_name': 'users',
+                'parent_primary_key': 'user_id',
+                'child_table_name': 'transactions',
+                'child_foreign_key': 'user_id',
+            },
+            {
+                'parent_table_name': 'products',
+                'parent_primary_key': 'product_id',
+                'child_table_name': 'transactions',
+                'child_foreign_key': 'product_id',
+            },
+            {
+                'parent_table_name': 'manufacturers',
+                'parent_primary_key': 'id',
+                'child_table_name': 'products',
+                'child_foreign_key': 'manufacturer',
+            },
+        ],
+    }
+    metadata = Metadata.load_from_dict(metadata_dict)
+
+    # Run
+    metadata.remove_table('products')
+
+    # Assert
+    expected_metadata_dict = {
+        'tables': {
+            'users': {
+                'columns': {
+                    'gender': {'sdtype': 'categorical'},
+                    'age': {'sdtype': 'numerical'},
+                    'name': {'sdtype': 'text'},
+                    'user_id': {'sdtype': 'id'},
+                },
+                'primary_key': 'user_id',
+            },
+            'transactions': {
+                'columns': {
+                    'user_id': {'sdtype': 'id'},
+                    'transaction_id': {'sdtype': 'id'},
+                    'product_id': {'sdtype': 'id'},
+                    'amount': {'sdtype': 'numerical'},
+                },
+                'primary_key': 'transaction_id',
+            },
+            'manufacturers': {
+                'columns': {
+                    'country': {'sdtype': 'categorical'},
+                    'address': {'sdtype': 'text'},
+                    'id': {'sdtype': 'id'},
+                },
+                'primary_key': 'id',
+            },
+        },
+        'relationships': [
+            {
+                'parent_table_name': 'users',
+                'parent_primary_key': 'user_id',
+                'child_table_name': 'transactions',
+                'child_foreign_key': 'user_id',
+            },
+        ],
+        'METADATA_SPEC_VERSION': 'V1',
+    }
+    assert expected_metadata_dict == metadata.to_dict()
+    assert metadata._multi_table_updated
diff --git a/tests/unit/metadata/test_metadata.py b/tests/unit/metadata/test_metadata.py
index bcee746e1..e44242394 100644
--- a/tests/unit/metadata/test_metadata.py
+++ b/tests/unit/metadata/test_metadata.py
@@ -913,3 +913,27 @@ def test_visualize_with_sequence_key_and_index(self, visualize_graph_mock):
             'nasdaq100_2019': expected_label,
         }
         visualize_graph_mock.assert_called_once_with(expected_nodes, [], None)
+
+    def test_remove_table_bad_table_name(self, metadata_instance):
+        """Test an error is raised if the table name does not exist in the metadata."""
+        # Run and Assert
+        expected_message = re.escape("Unknown table name ('fake_table').")
+        with pytest.raises(InvalidMetadataError, match=expected_message):
+            metadata_instance.remove_table('fake_table')
+
+    def test_remove_table_removes_table_and_relationships(self, metadata_instance):
+        """Test that the method removes the table and any relationships it was in."""
+        # Run
+        metadata_instance.remove_table('sessions')
+
+        # Assert
+        assert 'session' not in metadata_instance.tables
+        assert len(metadata_instance.tables) == 3
+        assert metadata_instance.relationships == [
+            {
+                'parent_table_name': 'users',
+                'parent_primary_key': 'id',
+                'child_table_name': 'payments',
+                'child_foreign_key': 'user_id',
+            },
+        ]