Repository: mathesar-foundation/mathesar

Issue:
Provide detailed error info when failing to disallow NULL for a column
## Reproduce

1. Set up a column which contains a `NULL` value.

1. PATCH to `/api/db/v0/tables/1/columns/<id>/` with:

    ```json5
    {
      "nullable": false
    }
    ```

1. Expect a 4xx HTTP response code.

1. Observe a 5xx HTTP response code.

1. Expect the response body to contain a Mathesar error code specific to this situation.

1. Observe the response body to use the error code `4999` (`UnknownError`).


Discussion:
Could I be assigned to this issue.
Thanks! @kid-116 I have assigned the issue to you.

Resolving patch:
diff --git a/mathesar/api/db/viewsets/columns.py b/mathesar/api/db/viewsets/columns.py
--- a/mathesar/api/db/viewsets/columns.py
+++ b/mathesar/api/db/viewsets/columns.py
@@ -1,10 +1,10 @@
 import warnings
-from psycopg2.errors import DuplicateColumn
+from psycopg2.errors import DuplicateColumn, NotNullViolation
 from rest_framework import status, viewsets
 from rest_framework.decorators import action
 from rest_framework.exceptions import NotFound
 from rest_framework.response import Response
-from sqlalchemy.exc import ProgrammingError
+from sqlalchemy.exc import ProgrammingError, IntegrityError
 
 from mathesar.api.exceptions.database_exceptions import (
     exceptions as database_api_exceptions,
@@ -172,6 +172,16 @@ def partial_update(self, request, pk=None, table_pk=None):
                     message='This type casting is invalid.',
                     status_code=status.HTTP_400_BAD_REQUEST
                 )
+            except IntegrityError as e:
+                if type(e.orig) == NotNullViolation:
+                    raise database_api_exceptions.NotNullViolationAPIException(
+                        e,
+                        field="nullable",
+                        status_code=status.HTTP_400_BAD_REQUEST,
+                        table=table,
+                    )
+                else:
+                    raise base_api_exceptions.MathesarAPIException(e)
             except Exception as e:
                 raise base_api_exceptions.MathesarAPIException(e)
 
diff --git a/mathesar/api/exceptions/database_exceptions/exceptions.py b/mathesar/api/exceptions/database_exceptions/exceptions.py
--- a/mathesar/api/exceptions/database_exceptions/exceptions.py
+++ b/mathesar/api/exceptions/database_exceptions/exceptions.py
@@ -269,7 +269,7 @@ def __init__(
             table.schema._sa_engine,
             metadata=get_cached_metadata(),
         )
-        column = Column.objects.get(attnum=column_attnum)
+        column = Column.objects.get(attnum=column_attnum, table=table)
         details = {
             'record_detail': exception_diagnostics.message_detail,
             'column_id': column.id


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
@@ -484,6 +484,29 @@ def test_column_update_invalid_type(create_patents_table, client):
     assert response_json[0]['message'] == "This type casting is invalid."
 
 
+def test_column_update_invalid_nullable(create_patents_table, client):
+    table = create_patents_table('Column Invalid Nullable')
+    body = {"nullable": False}
+    response = client.get(
+        f"/api/db/v0/tables/{table.id}/columns/"
+    )
+    assert response.status_code == 200
+    columns = response.json()['results']
+    column_index = 4
+    column_id = columns[column_index]['id']
+    response = client.patch(
+        f"/api/db/v0/tables/{table.id}/columns/{column_id}/",
+        body
+    )
+    assert response.status_code == 400
+    response_json = response.json()
+    assert response_json[0]['code'] == ErrorCodes.NotNullViolation.value
+    assert response_json[0]['message'] == (
+        'column "Patent Number" of relation'
+        ' "Column Invalid Nullable" contains null values'
+    )
+
+
 def test_column_update_returns_table_dependent_fields(column_test_table, client):
     expt_default = 5
     data = {"default": {"value": expt_default}}