Repository: mathesar-foundation/mathesar

Issue:
IndexError when deleting a column
## Description
<!-- A clear and concise description of what the bug is. -->
An indexError occurs when deleting a column through the API. Most of the time the error occurs when deleting the first or second column of a table. Deleting the last columns in a table does not seem to produce this error. 

## Expected behavior
<!-- A clear and concise description of what you expected to happen. -->

- A column should be deleted

## To Reproduce
<!-- How can we recreate this bug? Please try to provide a Minimal, Complete, and Verifiable (http://stackoverflow.com/help/mcve) example if code-related. -->

1. Delete the first  or second column of a table via API. Example: api/v0/tables/1/columns/1/
2. Delete the first or second column of another table via API. Example: api/v0/tables/2/columns/0/

## Screenshots

![Screenshot (35)](https://user-images.githubusercontent.com/66047479/133317146-3e4aa024-afc2-4370-9a79-1007549edccd.png)
![Screenshot (36)](https://user-images.githubusercontent.com/66047479/133317195-0da1725a-9895-4ee3-8b18-90b83eda90c4.png)

## Environment
 - OS: (_eg._ macOS 10.14.6; Fedora 32)
 - Browser: (_eg._ Safari; Firefox)
 - Browser Version: (_eg._ 13; 73)
 - Other info:

## Additional context
<!-- Add any other context about the problem or screenshots here. -->



Discussion:
@mr-gabe49 Can you post the full traceback? In the error message page that you posted, next to the "Traceback" header, there's a link that says "Switch to copy and paste view". If you click that, it should give you the full traceback in an easy-to-copy format.
@kgodey 
```
 Environment:


Request Method: GET
Request URL: http://localhost:8000/api/v0/tables/107/columns/

Django Version: 3.1.12
Python Version: 3.9.7
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/sqlalchemy/sql/base.py", line 1158, in __getitem__
    return self._index[key]

The above exception (-1) was the direct cause of the following exception:
  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 469, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
    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/viewsets/columns.py", line 22, in list
    return paginator.get_paginated_response(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 502, in to_representation
    attribute = field.get_attribute(instance)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/fields.py", line 457, in get_attribute
    return get_attribute(instance, self.source_attrs)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/fields.py", line 97, in get_attribute
    instance = getattr(instance, attr)
  File "/code/db/columns.py", line 176, in default_value
    return get_column_default(table_oid, self.column_index, self.engine)
  File "/code/db/columns.py", line 440, in get_column_default
    column = table.columns[column_index]
  File "/usr/local/lib/python3.9/site-packages/sqlalchemy/sql/base.py", line 1161, in __getitem__
    util.raise_(IndexError(key), replace_context=err)
  File "/usr/local/lib/python3.9/site-packages/sqlalchemy/util/compat.py", line 207, in raise_
    raise exception

Exception Type: IndexError at /api/v0/tables/107/columns/
Exception Value: -1
```

Thanks!
@kgodey  Were you able to replicate the error? I wasn't sure if it was local to my environment or not.
@mr-gabe49 I'm not able to replicate it.  Is this still occurring?  To double-check (while I attempt to replicate):  This occurs when you delete columns from different tables in sequence?
@mathemancer Thanks for looking into this. I'm still seeing the error, even after pulling the latest master. Yes, you are correct. If I have two tables, and I delete the first column of table 1 this is what happens to the columns of table 2: The first and second **indexes** are both 0. If I then delete either column 0 or 1 of table 2, I get an IndexError. 
<h1>Table 2: Before deleting a column in Table 1</h1>
<img src="https://user-images.githubusercontent.com/66047479/134015646-6951abce-4ae6-4909-aa81-629ff3d68d0e.png" width="375" height="375">
<h1>Table 2: After deleting a column in Table 1</h1>
<img src="https://user-images.githubusercontent.com/66047479/134016265-4bef2a6f-45ca-4b82-83d0-9bdbcc23dcfa.png" width="375" height="375">

My knowledge of Django is very limited. I'm going to learn more about it to make sure this error is not just local to my machine. 


@mr-gabe49 I've managed to replicate the problem.  I actually had state in _my_ local setup that kept the bug from happening.  

Resolving patch:
diff --git a/db/columns/operations/select.py b/db/columns/operations/select.py
--- a/db/columns/operations/select.py
+++ b/db/columns/operations/select.py
@@ -22,6 +22,7 @@ def get_column_index_from_name(table_oid, column_name, engine, connection_to_use
     sel = (
         select(func.count())
         .where(and_(
+            pg_attribute.c.attrelid == table_oid,
             pg_attribute.c.attisdropped.is_(True),
             pg_attribute.c.attnum < result,
         ))


Test patch:
diff --git a/db/tests/columns/operations/test_select.py b/db/tests/columns/operations/test_select.py
--- a/db/tests/columns/operations/test_select.py
+++ b/db/tests/columns/operations/test_select.py
@@ -1,3 +1,5 @@
+from alembic.migration import MigrationContext
+from alembic.operations import Operations
 import pytest
 from sqlalchemy import String, Integer, Column, Table, MetaData, Sequence, DateTime, func
 
@@ -23,6 +25,63 @@ def test_get_column_index_from_name(engine_with_schema):
     assert get_column_index_from_name(table_oid, one_name, engine) == 1
 
 
+def test_get_column_index_from_name_after_delete(engine_with_schema):
+    engine, schema = engine_with_schema
+    table_name = "table_with_columns"
+    zero_name = "colzero"
+    one_name = "colone"
+    two_name = "coltwo"
+    table = Table(
+        table_name,
+        MetaData(bind=engine, schema=schema),
+        Column(zero_name, Integer),
+        Column(one_name, String),
+        Column(two_name, String),
+    )
+    table.create()
+    with engine.begin() as conn:
+        op = Operations(MigrationContext.configure(conn))
+        op.drop_column(table.name, one_name, schema=schema)
+
+    table_oid = get_oid_from_table(table_name, schema, engine)
+    assert get_column_index_from_name(table_oid, zero_name, engine) == 0
+    assert get_column_index_from_name(table_oid, two_name, engine) == 1
+
+
+def test_get_column_index_from_name_after_delete_two_tables(engine_with_schema):
+    engine, schema = engine_with_schema
+    table_name = "table_with_columns"
+    zero_name = "colzero"
+    one_name = "colone"
+    two_name = "coltwo"
+
+    for suffix in ["1", "2"]:
+        table = Table(
+            table_name + suffix,
+            MetaData(bind=engine, schema=schema),
+            Column(zero_name + suffix, Integer),
+            Column(one_name + suffix, String),
+            Column(two_name + suffix, String),
+        )
+        table.create()
+
+    with engine.begin() as conn:
+        op = Operations(MigrationContext.configure(conn))
+        op.drop_column(table_name + "1", one_name + "1", schema=schema)
+
+    table_oid1 = get_oid_from_table(table_name + "1", schema, engine)
+    table_oid2 = get_oid_from_table(table_name + "2", schema, engine)
+    assert all(
+        [
+            get_column_index_from_name(table_oid1, zero_name + "1", engine) == 0,
+            get_column_index_from_name(table_oid1, two_name + "1", engine) == 1,
+            get_column_index_from_name(table_oid2, zero_name + "2", engine) == 0,
+            get_column_index_from_name(table_oid2, one_name + "2", engine) == 1,
+            get_column_index_from_name(table_oid2, two_name + "2", engine) == 2,
+        ]
+    )
+
+
 @pytest.mark.parametrize("filler", [True, False])
 @pytest.mark.parametrize("col_type", column_test_dict.keys())
 def test_get_column_default(engine_with_schema, filler, col_type):