Repository: mathesar-foundation/mathesar

Issue:
AssertionError or KeyError occurs when deleting a column and Mathesar is unusable once that error occurs
## Description
* Delete a column, notice that the response is a 500 with either AssertionError or KeyError
* Once that error occurs, Mathesar is unusable

```
Environment:


Request Method: DELETE
Request URL: http://localhost:8000/api/db/v0/tables/1/columns/16/

Django Version: 3.1.14
Python Version: 3.9.14
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'rest_framework',
 'django_filters',
 'django_property_filter',
 'mathesar']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware']



Traceback (most recent call last):
  File "/code/mathesar/models/base.py", line 675, in __getattribute__
    return super().__getattribute__(name)

During handling of the above exception ('Column' object has no attribute 'primary_key'), another exception occurred:
  File "/usr/local/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/usr/local/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/viewsets.py", line 125, in view
    return self.dispatch(request, *args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 509, in dispatch
    response = self.handle_exception(exc)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 466, in handle_exception
    response = exception_handler(exc, context)
  File "/code/mathesar/exception_handlers.py", line 55, in mathesar_exception_handler
    raise exc
  File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 506, in dispatch
    response = handler(request, *args, **kwargs)
  File "/code/mathesar/api/db/viewsets/columns.py", line 208, in destroy
    table.drop_column(column_instance.attnum)
  File "/code/mathesar/models/base.py", line 423, in drop_column
    reset_reflection()
  File "/code/mathesar/state/base.py", line 27, in reset_reflection
    _trigger_django_model_reflection()
  File "/code/mathesar/state/base.py", line 31, in _trigger_django_model_reflection
    reflect_db_objects(metadata=get_cached_metadata())
  File "/code/mathesar/state/django.py", line 44, in reflect_db_objects
    reflect_columns_from_tables(tables, metadata=metadata)
  File "/code/mathesar/state/django.py", line 123, in reflect_columns_from_tables
    models._compute_preview_template(table)
  File "/code/mathesar/models/base.py", line 859, in _compute_preview_template
    if column.primary_key:
  File "/code/mathesar/models/base.py", line 681, in __getattribute__
    return getattr(self._sa_column, name)
  File "/code/mathesar/models/base.py", line 675, in __getattribute__
    return super().__getattribute__(name)
  File "/code/mathesar/models/base.py", line 696, in _sa_column
    return self.table.sa_columns[self.name]
  File "/code/mathesar/models/base.py", line 675, in __getattribute__
    return super().__getattribute__(name)
  File "/code/mathesar/state/cached_property.py", line 62, in __get__
    new_value = self.original_get_fn(instance)
  File "/code/mathesar/models/base.py", line 718, in name
    assert type(name) is str

Exception Type: AssertionError at /api/db/v0/tables/1/columns/16/
Exception Value: 
```

Subsequent requests to table/column/record endpoints result in errors:
```
Environment:


Request Method: GET
Request URL: http://localhost:8000/mathesar_tables/1/tables/1/

Django Version: 3.1.14
Python Version: 3.9.14
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'rest_framework',
 'django_filters',
 'django_property_filter',
 'mathesar']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware']



Traceback (most recent call last):
  File "/usr/local/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/usr/local/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/contrib/auth/decorators.py", line 21, in _wrapped_view
    return view_func(request, *args, **kwargs)
  File "/code/mathesar/views.py", line 131, in schema_home
    'common_data': get_common_data(request, database, schema)
  File "/code/mathesar/views.py", line 74, in get_common_data
    'tables': get_table_list(request, schema),
  File "/code/mathesar/views.py", line 43, in get_table_list
    return table_serializer.data
  File "/usr/local/lib/python3.9/site-packages/rest_framework/serializers.py", line 745, in data
    ret = super().data
  File "/usr/local/lib/python3.9/site-packages/rest_framework/serializers.py", line 246, in data
    self._data = self.to_representation(self.instance)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/serializers.py", line 663, in to_representation
    return [
  File "/usr/local/lib/python3.9/site-packages/rest_framework/serializers.py", line 664, in <listcomp>
    self.child.to_representation(item) for item in iterable
  File "/usr/local/lib/python3.9/site-packages/rest_framework/serializers.py", line 515, in to_representation
    ret[field.field_name] = field.to_representation(attribute)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/serializers.py", line 663, in to_representation
    return [
  File "/usr/local/lib/python3.9/site-packages/rest_framework/serializers.py", line 664, in <listcomp>
    self.child.to_representation(item) for item in iterable
  File "/code/mathesar/api/serializers/columns.py", line 80, in to_representation
    db_type = instance.db_type
  File "/code/mathesar/models/base.py", line 675, in __getattribute__
    return super().__getattribute__(name)
  File "/code/mathesar/models/base.py", line 735, in db_type
    return self._sa_column.db_type
  File "/code/mathesar/models/base.py", line 675, in __getattribute__
    return super().__getattribute__(name)
  File "/code/mathesar/models/base.py", line 696, in _sa_column
    return self.table.sa_columns[self.name]
  File "/code/mathesar/models/base.py", line 675, in __getattribute__
    return super().__getattribute__(name)
  File "/code/mathesar/state/cached_property.py", line 62, in __get__
    new_value = self.original_get_fn(instance)
  File "/code/mathesar/models/base.py", line 718, in name
    assert type(name) is str

Exception Type: AssertionError at /mathesar_tables/1/tables/1/
Exception Value: 
```


Discussion:

Resolving patch:
diff --git a/mathesar/state/django.py b/mathesar/state/django.py
--- a/mathesar/state/django.py
+++ b/mathesar/state/django.py
@@ -1,6 +1,8 @@
 import logging
 from collections import defaultdict
+from functools import reduce
 
+import operator
 from django.conf import settings
 from django.core.cache import cache as dj_cache
 from django.db.models import Prefetch, Q
@@ -154,11 +156,15 @@ def _delete_stale_columns(attnum_tuples, tables):
     attnums_mapped_by_table_oid = defaultdict(list)
     for attnum, table_oid in attnum_tuples:
         attnums_mapped_by_table_oid[table_oid].append(attnum)
-    stale_columns_queryset = models.Column.current_objects
+    stale_columns_conditions = []
     for table_oid, attnums in attnums_mapped_by_table_oid.items():
         table = next(table for table in tables if table.oid == table_oid)
-        stale_columns_queryset = stale_columns_queryset.filter(Q(table=table) & ~Q(attnum__in=attnums))
-    stale_columns_queryset.delete()
+        stale_columns_conditions.append(Q(table=table) & ~Q(attnum__in=attnums))
+    stale_columns_query = reduce(
+        operator.or_,
+        stale_columns_conditions
+    )
+    models.Column.objects.filter(stale_columns_query).delete()
 
 
 # TODO pass in a cached engine instead of creating a new one


Test patch:
diff --git a/mathesar/tests/api/test_column_api.py b/mathesar/tests/api/test_column_api.py
--- a/mathesar/tests/api/test_column_api.py
+++ b/mathesar/tests/api/test_column_api.py
@@ -558,7 +558,8 @@ def test_column_update_when_missing(column_test_table, client):
     assert response_data['code'] == ErrorCodes.NotFound.value
 
 
-def test_column_destroy(column_test_table, client):
+def test_column_destroy(column_test_table, create_patents_table, client):
+    create_patents_table('Dummy Table')
     num_columns = len(column_test_table.sa_columns)
     col_one_name = column_test_table.sa_columns[1].name
     column = column_test_table.get_columns_by_name(['mycolumn1'])[0]