We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Incorrect error is thrown when id field is not provided for a column object during a batch update
## Description
The following `Patch` request to `/tables/{id}` 

```

{
    "columns": [
        {
            "name": "id",
            "type": "INTEGER"
        },
        {
            "name": "Center",
            "type": "TEXT"
        }
    ]
}
```

 fails with an error `{code: 4999, message: "local variable 'name' referenced before assignment", field: null, detail: null}`.

This is happening due to https://github.com/centerofci/mathesar/blob/a8bad8268c8634bc766fa2e4fde271f4ec1a165e/db/columns/operations/alter.py#L215

always expecting a name which is fetched from the `column id` that is missing from the patch request.

## Expected behavior

`id` field of the column object should be required when batch updating using `/tables/{id}`.

## To Reproduce
Using API 
- Create a table using `/api/db/v0/tables/` API 
- Send a patch request to `/api/db/v0/tables/{id}`

Using frontend client
- Create a table by importing a csv file
- Error will be thrown when clicking on `Finish import`



--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `mathesar/api/serializers/tables.py`
Content:
```
1 from django.urls import reverse
2 from psycopg2.errors import DuplicateTable
3 from rest_framework import serializers, status
4 from sqlalchemy.exc import ProgrammingError
5 
6 from db.types.base import get_db_type_enum_from_id
7 
8 from mathesar.api.exceptions.validation_exceptions.exceptions import (
9     ColumnSizeMismatchAPIException, DistinctColumnRequiredAPIException,
10     MultipleDataFileAPIException, UnknownDatabaseTypeIdentifier
11 )
12 from mathesar.api.exceptions.database_exceptions.exceptions import DuplicateTableAPIException
13 from mathesar.api.exceptions.database_exceptions.base_exceptions import ProgrammingAPIException
14 from mathesar.api.exceptions.mixins import MathesarErrorMessageMixin
15 from mathesar.api.serializers.columns import SimpleColumnSerializer
16 from mathesar.models import Table, DataFile
17 from mathesar.utils.tables import gen_table_name, create_table_from_datafile, create_empty_table
18 
19 
20 class TableSerializer(MathesarErrorMessageMixin, serializers.ModelSerializer):
21     columns = SimpleColumnSerializer(many=True, required=False)
22     records_url = serializers.SerializerMethodField()
23     constraints_url = serializers.SerializerMethodField()
24     columns_url = serializers.SerializerMethodField()
25     type_suggestions_url = serializers.SerializerMethodField()
26     previews_url = serializers.SerializerMethodField()
27     name = serializers.CharField(required=False, allow_blank=True, default='')
28     data_files = serializers.PrimaryKeyRelatedField(
29         required=False, many=True, queryset=DataFile.objects.all()
30     )
31 
32     class Meta:
33         model = Table
34         fields = ['id', 'name', 'schema', 'created_at', 'updated_at', 'import_verified',
35                   'columns', 'records_url', 'constraints_url', 'columns_url',
36                   'type_suggestions_url', 'previews_url', 'data_files',
37                   'has_dependencies']
38 
39     def get_records_url(self, obj):
40         if isinstance(obj, Table):
41             # Only get records if we are serializing an existing table
42             request = self.context['request']
43             return request.build_absolute_uri(reverse('table-record-list', kwargs={'table_pk': obj.pk}))
44         else:
45             return None
46 
47     def get_constraints_url(self, obj):
48         if isinstance(obj, Table):
49             # Only get constraints if we are serializing an existing table
50             request = self.context['request']
51             return request.build_absolute_uri(reverse('table-constraint-list', kwargs={'table_pk': obj.pk}))
52         else:
53             return None
54 
55     def get_columns_url(self, obj):
56         if isinstance(obj, Table):
57             # Only get columns if we are serializing an existing table
58             request = self.context['request']
59             return request.build_absolute_uri(reverse('table-column-list', kwargs={'table_pk': obj.pk}))
60         else:
61             return None
62 
63     def get_type_suggestions_url(self, obj):
64         if isinstance(obj, Table):
65             # Only get type suggestions if we are serializing an existing table
66             request = self.context['request']
67             return request.build_absolute_uri(reverse('table-type-suggestions', kwargs={'pk': obj.pk}))
68         else:
69             return None
70 
71     def get_previews_url(self, obj):
72         if isinstance(obj, Table):
73             # Only get previews if we are serializing an existing table
74             request = self.context['request']
75             return request.build_absolute_uri(reverse('table-previews', kwargs={'pk': obj.pk}))
76         else:
77             return None
78 
79     def validate_data_files(self, data_files):
80         if data_files and len(data_files) > 1:
81             raise MultipleDataFileAPIException()
82         return data_files
83 
84     def create(self, validated_data):
85         schema = validated_data['schema']
86         data_files = validated_data.get('data_files')
87         name = validated_data.get('name') or gen_table_name(schema, data_files)
88 
89         try:
90             if data_files:
91                 table = create_table_from_datafile(data_files, name, schema)
92             else:
93                 table = create_empty_table(name, schema)
94         except ProgrammingError as e:
95             if type(e.orig) == DuplicateTable:
96                 raise DuplicateTableAPIException(
97                     e,
98                     message=f"Relation {validated_data['name']} already exists in schema {schema.id}",
99                     field="name",
100                     status_code=status.HTTP_400_BAD_REQUEST
101                 )
102             else:
103                 raise ProgrammingAPIException(e)
104         return table
105 
106 
107 class TablePreviewSerializer(MathesarErrorMessageMixin, serializers.Serializer):
108     name = serializers.CharField(required=False)
109     columns = SimpleColumnSerializer(many=True)
110 
111     def validate_columns(self, columns):
112         table = self.context['table']
113         column_names = [col["name"] for col in columns]
114         if not len(column_names) == len(set(column_names)):
115             raise DistinctColumnRequiredAPIException()
116         if not len(columns) == len(table.sa_columns):
117             raise ColumnSizeMismatchAPIException()
118         for column in columns:
119             db_type_id = column['type']
120             db_type = get_db_type_enum_from_id(db_type_id)
121             if db_type is None:
122                 raise UnknownDatabaseTypeIdentifier(db_type_id=db_type_id)
123         return columns
124
```

Path: `mathesar/api/db/viewsets/tables.py`
Content:
```
1 from django_filters import rest_framework as filters
2 from psycopg2.errors import InvalidTextRepresentation, CheckViolation
3 from rest_framework import status, viewsets
4 from rest_framework.decorators import action
5 from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, CreateModelMixin
6 from rest_framework.response import Response
7 from sqlalchemy.exc import DataError, IntegrityError
8 
9 from mathesar.api.exceptions.database_exceptions import (
10     exceptions as database_api_exceptions,
11     base_exceptions as database_base_api_exceptions,
12 )
13 from mathesar.api.exceptions.generic_exceptions import base_exceptions as base_api_exceptions
14 from db.types.exceptions import UnsupportedTypeException
15 from mathesar.api.dj_filters import TableFilter
16 from mathesar.api.pagination import DefaultLimitOffsetPagination
17 from mathesar.api.serializers.tables import TableSerializer, TablePreviewSerializer
18 from mathesar.models import Table
19 from mathesar.utils.tables import (
20     get_table_column_types
21 )
22 
23 
24 class TableViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, viewsets.GenericViewSet):
25     serializer_class = TableSerializer
26     pagination_class = DefaultLimitOffsetPagination
27     filter_backends = (filters.DjangoFilterBackend,)
28     filterset_class = TableFilter
29 
30     def get_queryset(self):
31         return Table.objects.all().order_by('-created_at')
32 
33     def partial_update(self, request, pk=None):
34         serializer = TableSerializer(
35             data=request.data, context={'request': request}, partial=True
36         )
37         serializer.is_valid(raise_exception=True)
38         table = self.get_object()
39 
40         # Save the fields that are stored in the model.
41         present_model_fields = []
42         for model_field in table.MODEL_FIELDS:
43             if model_field in serializer.validated_data:
44                 setattr(table, model_field, serializer.validated_data[model_field])
45                 present_model_fields.append(model_field)
46         table.save(update_fields=present_model_fields)
47         for key in present_model_fields:
48             del serializer.validated_data[key]
49 
50         # Save the fields that are stored in the underlying DB.
51         try:
52             table.update_sa_table(serializer.validated_data)
53         except ValueError as e:
54             raise base_api_exceptions.ValueAPIException(e, status_code=status.HTTP_400_BAD_REQUEST)
55 
56         # Reload the table to avoid cached properties
57         table = self.get_object()
58         serializer = TableSerializer(table, context={'request': request})
59         return Response(serializer.data)
60 
61     def destroy(self, request, pk=None):
62         table = self.get_object()
63         table.delete_sa_table()
64         table.delete()
65         return Response(status=status.HTTP_204_NO_CONTENT)
66 
67     @action(methods=['get'], detail=True)
68     def type_suggestions(self, request, pk=None):
69         table = self.get_object()
70         col_types = get_table_column_types(table)
71         return Response(col_types)
72 
73     @action(methods=['post'], detail=True)
74     def previews(self, request, pk=None):
75         table = self.get_object()
76         serializer = TablePreviewSerializer(data=request.data, context={"request": request, 'table': table})
77         serializer.is_valid(raise_exception=True)
78         columns_field_key = "columns"
79         columns = serializer.data[columns_field_key]
80         table_data = TableSerializer(table, context={"request": request}).data
81         try:
82             preview_records = table.get_preview(columns)
83         except (DataError, IntegrityError) as e:
84             if type(e.orig) == InvalidTextRepresentation or type(e.orig) == CheckViolation:
85                 raise database_api_exceptions.InvalidTypeCastAPIException(
86                     e,
87                     status_code=status.HTTP_400_BAD_REQUEST,
88                     field='columns'
89                 )
90             else:
91                 raise database_base_api_exceptions.IntegrityAPIException(
92                     e,
93                     status_code=status.HTTP_400_BAD_REQUEST,
94                     field='columns'
95                 )
96         except UnsupportedTypeException as e:
97             raise database_api_exceptions.UnsupportedTypeAPIException(
98                 e,
99                 field='columns',
100                 status_code=status.HTTP_400_BAD_REQUEST
101             )
102         table_data.update(
103             {
104                 # There's no way to reflect actual column data without
105                 # creating a view, so we just use the submission, assuming
106                 # no errors means we changed to the desired names and types
107                 "columns": columns,
108                 "records": preview_records
109             }
110         )
111 
112         return Response(table_data)
113
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/mathesar/api/db/viewsets/tables.py b/mathesar/api/db/viewsets/tables.py
--- a/mathesar/api/db/viewsets/tables.py
+++ b/mathesar/api/db/viewsets/tables.py
@@ -10,7 +10,6 @@
     exceptions as database_api_exceptions,
     base_exceptions as database_base_api_exceptions,
 )
-from mathesar.api.exceptions.generic_exceptions import base_exceptions as base_api_exceptions
 from db.types.exceptions import UnsupportedTypeException
 from mathesar.api.dj_filters import TableFilter
 from mathesar.api.pagination import DefaultLimitOffsetPagination
@@ -31,27 +30,12 @@
         return Table.objects.all().order_by('-created_at')
 
     def partial_update(self, request, pk=None):
+        table = self.get_object()
         serializer = TableSerializer(
-            data=request.data, context={'request': request}, partial=True
+            table, data=request.data, context={'request': request}, partial=True
         )
         serializer.is_valid(raise_exception=True)
-        table = self.get_object()
-
-        # Save the fields that are stored in the model.
-        present_model_fields = []
-        for model_field in table.MODEL_FIELDS:
-            if model_field in serializer.validated_data:
-                setattr(table, model_field, serializer.validated_data[model_field])
-                present_model_fields.append(model_field)
-        table.save(update_fields=present_model_fields)
-        for key in present_model_fields:
-            del serializer.validated_data[key]
-
-        # Save the fields that are stored in the underlying DB.
-        try:
-            table.update_sa_table(serializer.validated_data)
-        except ValueError as e:
-            raise base_api_exceptions.ValueAPIException(e, status_code=status.HTTP_400_BAD_REQUEST)
+        serializer.save()
 
         # Reload the table to avoid cached properties
         table = self.get_object()
diff --git a/mathesar/api/serializers/tables.py b/mathesar/api/serializers/tables.py
--- a/mathesar/api/serializers/tables.py
+++ b/mathesar/api/serializers/tables.py
@@ -1,6 +1,7 @@
 from django.urls import reverse
 from psycopg2.errors import DuplicateTable
 from rest_framework import serializers, status
+from rest_framework.exceptions import ValidationError
 from sqlalchemy.exc import ProgrammingError
 
 from db.types.base import get_db_type_enum_from_id
@@ -11,6 +12,8 @@
 )
 from mathesar.api.exceptions.database_exceptions.exceptions import DuplicateTableAPIException
 from mathesar.api.exceptions.database_exceptions.base_exceptions import ProgrammingAPIException
+from mathesar.api.exceptions.validation_exceptions import base_exceptions as base_validation_exceptions
+from mathesar.api.exceptions.generic_exceptions import base_exceptions as base_api_exceptions
 from mathesar.api.exceptions.mixins import MathesarErrorMessageMixin
 from mathesar.api.serializers.columns import SimpleColumnSerializer
 from mathesar.models import Table, DataFile
@@ -103,6 +106,35 @@
                 raise ProgrammingAPIException(e)
         return table
 
+    def update(self, instance, validated_data):
+        if self.partial:
+            # Save the fields that are stored in the model.
+            present_model_fields = []
+            for model_field in instance.MODEL_FIELDS:
+                if model_field in validated_data:
+                    setattr(instance, model_field, validated_data[model_field])
+                    present_model_fields.append(model_field)
+            instance.save(update_fields=present_model_fields)
+            for key in present_model_fields:
+                del validated_data[key]
+            # Save the fields that are stored in the underlying DB.
+            try:
+                instance.update_sa_table(validated_data)
+            except ValueError as e:
+                raise base_api_exceptions.ValueAPIException(e, status_code=status.HTTP_400_BAD_REQUEST)
+        return instance
+
+    def validate(self, data):
+        if self.partial:
+            columns = data.get('columns', None)
+            if columns is not None:
+                for col in columns:
+                    id = col.get('id', None)
+                    if id is None:
+                        message = "'id' field is required while batch updating columns."
+                        raise base_validation_exceptions.MathesarValidationException(ValidationError, message=message)
+        return data
+
 
 class TablePreviewSerializer(MathesarErrorMessageMixin, serializers.Serializer):
     name = serializers.CharField(required=False)

{"golden_diff": "diff --git a/mathesar/api/db/viewsets/tables.py b/mathesar/api/db/viewsets/tables.py\n--- a/mathesar/api/db/viewsets/tables.py\n+++ b/mathesar/api/db/viewsets/tables.py\n@@ -10,7 +10,6 @@\n     exceptions as database_api_exceptions,\n     base_exceptions as database_base_api_exceptions,\n )\n-from mathesar.api.exceptions.generic_exceptions import base_exceptions as base_api_exceptions\n from db.types.exceptions import UnsupportedTypeException\n from mathesar.api.dj_filters import TableFilter\n from mathesar.api.pagination import DefaultLimitOffsetPagination\n@@ -31,27 +30,12 @@\n         return Table.objects.all().order_by('-created_at')\n \n     def partial_update(self, request, pk=None):\n+        table = self.get_object()\n         serializer = TableSerializer(\n-            data=request.data, context={'request': request}, partial=True\n+            table, data=request.data, context={'request': request}, partial=True\n         )\n         serializer.is_valid(raise_exception=True)\n-        table = self.get_object()\n-\n-        # Save the fields that are stored in the model.\n-        present_model_fields = []\n-        for model_field in table.MODEL_FIELDS:\n-            if model_field in serializer.validated_data:\n-                setattr(table, model_field, serializer.validated_data[model_field])\n-                present_model_fields.append(model_field)\n-        table.save(update_fields=present_model_fields)\n-        for key in present_model_fields:\n-            del serializer.validated_data[key]\n-\n-        # Save the fields that are stored in the underlying DB.\n-        try:\n-            table.update_sa_table(serializer.validated_data)\n-        except ValueError as e:\n-            raise base_api_exceptions.ValueAPIException(e, status_code=status.HTTP_400_BAD_REQUEST)\n+        serializer.save()\n \n         # Reload the table to avoid cached properties\n         table = self.get_object()\ndiff --git a/mathesar/api/serializers/tables.py b/mathesar/api/serializers/tables.py\n--- a/mathesar/api/serializers/tables.py\n+++ b/mathesar/api/serializers/tables.py\n@@ -1,6 +1,7 @@\n from django.urls import reverse\n from psycopg2.errors import DuplicateTable\n from rest_framework import serializers, status\n+from rest_framework.exceptions import ValidationError\n from sqlalchemy.exc import ProgrammingError\n \n from db.types.base import get_db_type_enum_from_id\n@@ -11,6 +12,8 @@\n )\n from mathesar.api.exceptions.database_exceptions.exceptions import DuplicateTableAPIException\n from mathesar.api.exceptions.database_exceptions.base_exceptions import ProgrammingAPIException\n+from mathesar.api.exceptions.validation_exceptions import base_exceptions as base_validation_exceptions\n+from mathesar.api.exceptions.generic_exceptions import base_exceptions as base_api_exceptions\n from mathesar.api.exceptions.mixins import MathesarErrorMessageMixin\n from mathesar.api.serializers.columns import SimpleColumnSerializer\n from mathesar.models import Table, DataFile\n@@ -103,6 +106,35 @@\n                 raise ProgrammingAPIException(e)\n         return table\n \n+    def update(self, instance, validated_data):\n+        if self.partial:\n+            # Save the fields that are stored in the model.\n+            present_model_fields = []\n+            for model_field in instance.MODEL_FIELDS:\n+                if model_field in validated_data:\n+                    setattr(instance, model_field, validated_data[model_field])\n+                    present_model_fields.append(model_field)\n+            instance.save(update_fields=present_model_fields)\n+            for key in present_model_fields:\n+                del validated_data[key]\n+            # Save the fields that are stored in the underlying DB.\n+            try:\n+                instance.update_sa_table(validated_data)\n+            except ValueError as e:\n+                raise base_api_exceptions.ValueAPIException(e, status_code=status.HTTP_400_BAD_REQUEST)\n+        return instance\n+\n+    def validate(self, data):\n+        if self.partial:\n+            columns = data.get('columns', None)\n+            if columns is not None:\n+                for col in columns:\n+                    id = col.get('id', None)\n+                    if id is None:\n+                        message = \"'id' field is required while batch updating columns.\"\n+                        raise base_validation_exceptions.MathesarValidationException(ValidationError, message=message)\n+        return data\n+\n \n class TablePreviewSerializer(MathesarErrorMessageMixin, serializers.Serializer):\n     name = serializers.CharField(required=False)\n", "issue": "Incorrect error is thrown when id field is not provided for a column object during a batch update\n## Description\r\nThe following `Patch` request to `/tables/{id}` \r\n\r\n```\r\n\r\n{\r\n    \"columns\": [\r\n        {\r\n            \"name\": \"id\",\r\n            \"type\": \"INTEGER\"\r\n        },\r\n        {\r\n            \"name\": \"Center\",\r\n            \"type\": \"TEXT\"\r\n        }\r\n    ]\r\n}\r\n```\r\n\r\n fails with an error `{code: 4999, message: \"local variable 'name' referenced before assignment\", field: null, detail: null}`.\r\n\r\nThis is happening due to https://github.com/centerofci/mathesar/blob/a8bad8268c8634bc766fa2e4fde271f4ec1a165e/db/columns/operations/alter.py#L215\r\n\r\nalways expecting a name which is fetched from the `column id` that is missing from the patch request.\r\n\r\n## Expected behavior\r\n\r\n`id` field of the column object should be required when batch updating using `/tables/{id}`.\r\n\r\n## To Reproduce\r\nUsing API \r\n- Create a table using `/api/db/v0/tables/` API \r\n- Send a patch request to `/api/db/v0/tables/{id}`\r\n\r\nUsing frontend client\r\n- Create a table by importing a csv file\r\n- Error will be thrown when clicking on `Finish import`\r\n\r\n\n", "before_files": [{"content": "from django.urls import reverse\nfrom psycopg2.errors import DuplicateTable\nfrom rest_framework import serializers, status\nfrom sqlalchemy.exc import ProgrammingError\n\nfrom db.types.base import get_db_type_enum_from_id\n\nfrom mathesar.api.exceptions.validation_exceptions.exceptions import (\n    ColumnSizeMismatchAPIException, DistinctColumnRequiredAPIException,\n    MultipleDataFileAPIException, UnknownDatabaseTypeIdentifier\n)\nfrom mathesar.api.exceptions.database_exceptions.exceptions import DuplicateTableAPIException\nfrom mathesar.api.exceptions.database_exceptions.base_exceptions import ProgrammingAPIException\nfrom mathesar.api.exceptions.mixins import MathesarErrorMessageMixin\nfrom mathesar.api.serializers.columns import SimpleColumnSerializer\nfrom mathesar.models import Table, DataFile\nfrom mathesar.utils.tables import gen_table_name, create_table_from_datafile, create_empty_table\n\n\nclass TableSerializer(MathesarErrorMessageMixin, serializers.ModelSerializer):\n    columns = SimpleColumnSerializer(many=True, required=False)\n    records_url = serializers.SerializerMethodField()\n    constraints_url = serializers.SerializerMethodField()\n    columns_url = serializers.SerializerMethodField()\n    type_suggestions_url = serializers.SerializerMethodField()\n    previews_url = serializers.SerializerMethodField()\n    name = serializers.CharField(required=False, allow_blank=True, default='')\n    data_files = serializers.PrimaryKeyRelatedField(\n        required=False, many=True, queryset=DataFile.objects.all()\n    )\n\n    class Meta:\n        model = Table\n        fields = ['id', 'name', 'schema', 'created_at', 'updated_at', 'import_verified',\n                  'columns', 'records_url', 'constraints_url', 'columns_url',\n                  'type_suggestions_url', 'previews_url', 'data_files',\n                  'has_dependencies']\n\n    def get_records_url(self, obj):\n        if isinstance(obj, Table):\n            # Only get records if we are serializing an existing table\n            request = self.context['request']\n            return request.build_absolute_uri(reverse('table-record-list', kwargs={'table_pk': obj.pk}))\n        else:\n            return None\n\n    def get_constraints_url(self, obj):\n        if isinstance(obj, Table):\n            # Only get constraints if we are serializing an existing table\n            request = self.context['request']\n            return request.build_absolute_uri(reverse('table-constraint-list', kwargs={'table_pk': obj.pk}))\n        else:\n            return None\n\n    def get_columns_url(self, obj):\n        if isinstance(obj, Table):\n            # Only get columns if we are serializing an existing table\n            request = self.context['request']\n            return request.build_absolute_uri(reverse('table-column-list', kwargs={'table_pk': obj.pk}))\n        else:\n            return None\n\n    def get_type_suggestions_url(self, obj):\n        if isinstance(obj, Table):\n            # Only get type suggestions if we are serializing an existing table\n            request = self.context['request']\n            return request.build_absolute_uri(reverse('table-type-suggestions', kwargs={'pk': obj.pk}))\n        else:\n            return None\n\n    def get_previews_url(self, obj):\n        if isinstance(obj, Table):\n            # Only get previews if we are serializing an existing table\n            request = self.context['request']\n            return request.build_absolute_uri(reverse('table-previews', kwargs={'pk': obj.pk}))\n        else:\n            return None\n\n    def validate_data_files(self, data_files):\n        if data_files and len(data_files) > 1:\n            raise MultipleDataFileAPIException()\n        return data_files\n\n    def create(self, validated_data):\n        schema = validated_data['schema']\n        data_files = validated_data.get('data_files')\n        name = validated_data.get('name') or gen_table_name(schema, data_files)\n\n        try:\n            if data_files:\n                table = create_table_from_datafile(data_files, name, schema)\n            else:\n                table = create_empty_table(name, schema)\n        except ProgrammingError as e:\n            if type(e.orig) == DuplicateTable:\n                raise DuplicateTableAPIException(\n                    e,\n                    message=f\"Relation {validated_data['name']} already exists in schema {schema.id}\",\n                    field=\"name\",\n                    status_code=status.HTTP_400_BAD_REQUEST\n                )\n            else:\n                raise ProgrammingAPIException(e)\n        return table\n\n\nclass TablePreviewSerializer(MathesarErrorMessageMixin, serializers.Serializer):\n    name = serializers.CharField(required=False)\n    columns = SimpleColumnSerializer(many=True)\n\n    def validate_columns(self, columns):\n        table = self.context['table']\n        column_names = [col[\"name\"] for col in columns]\n        if not len(column_names) == len(set(column_names)):\n            raise DistinctColumnRequiredAPIException()\n        if not len(columns) == len(table.sa_columns):\n            raise ColumnSizeMismatchAPIException()\n        for column in columns:\n            db_type_id = column['type']\n            db_type = get_db_type_enum_from_id(db_type_id)\n            if db_type is None:\n                raise UnknownDatabaseTypeIdentifier(db_type_id=db_type_id)\n        return columns\n", "path": "mathesar/api/serializers/tables.py"}, {"content": "from django_filters import rest_framework as filters\nfrom psycopg2.errors import InvalidTextRepresentation, CheckViolation\nfrom rest_framework import status, viewsets\nfrom rest_framework.decorators import action\nfrom rest_framework.mixins import ListModelMixin, RetrieveModelMixin, CreateModelMixin\nfrom rest_framework.response import Response\nfrom sqlalchemy.exc import DataError, IntegrityError\n\nfrom mathesar.api.exceptions.database_exceptions import (\n    exceptions as database_api_exceptions,\n    base_exceptions as database_base_api_exceptions,\n)\nfrom mathesar.api.exceptions.generic_exceptions import base_exceptions as base_api_exceptions\nfrom db.types.exceptions import UnsupportedTypeException\nfrom mathesar.api.dj_filters import TableFilter\nfrom mathesar.api.pagination import DefaultLimitOffsetPagination\nfrom mathesar.api.serializers.tables import TableSerializer, TablePreviewSerializer\nfrom mathesar.models import Table\nfrom mathesar.utils.tables import (\n    get_table_column_types\n)\n\n\nclass TableViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, viewsets.GenericViewSet):\n    serializer_class = TableSerializer\n    pagination_class = DefaultLimitOffsetPagination\n    filter_backends = (filters.DjangoFilterBackend,)\n    filterset_class = TableFilter\n\n    def get_queryset(self):\n        return Table.objects.all().order_by('-created_at')\n\n    def partial_update(self, request, pk=None):\n        serializer = TableSerializer(\n            data=request.data, context={'request': request}, partial=True\n        )\n        serializer.is_valid(raise_exception=True)\n        table = self.get_object()\n\n        # Save the fields that are stored in the model.\n        present_model_fields = []\n        for model_field in table.MODEL_FIELDS:\n            if model_field in serializer.validated_data:\n                setattr(table, model_field, serializer.validated_data[model_field])\n                present_model_fields.append(model_field)\n        table.save(update_fields=present_model_fields)\n        for key in present_model_fields:\n            del serializer.validated_data[key]\n\n        # Save the fields that are stored in the underlying DB.\n        try:\n            table.update_sa_table(serializer.validated_data)\n        except ValueError as e:\n            raise base_api_exceptions.ValueAPIException(e, status_code=status.HTTP_400_BAD_REQUEST)\n\n        # Reload the table to avoid cached properties\n        table = self.get_object()\n        serializer = TableSerializer(table, context={'request': request})\n        return Response(serializer.data)\n\n    def destroy(self, request, pk=None):\n        table = self.get_object()\n        table.delete_sa_table()\n        table.delete()\n        return Response(status=status.HTTP_204_NO_CONTENT)\n\n    @action(methods=['get'], detail=True)\n    def type_suggestions(self, request, pk=None):\n        table = self.get_object()\n        col_types = get_table_column_types(table)\n        return Response(col_types)\n\n    @action(methods=['post'], detail=True)\n    def previews(self, request, pk=None):\n        table = self.get_object()\n        serializer = TablePreviewSerializer(data=request.data, context={\"request\": request, 'table': table})\n        serializer.is_valid(raise_exception=True)\n        columns_field_key = \"columns\"\n        columns = serializer.data[columns_field_key]\n        table_data = TableSerializer(table, context={\"request\": request}).data\n        try:\n            preview_records = table.get_preview(columns)\n        except (DataError, IntegrityError) as e:\n            if type(e.orig) == InvalidTextRepresentation or type(e.orig) == CheckViolation:\n                raise database_api_exceptions.InvalidTypeCastAPIException(\n                    e,\n                    status_code=status.HTTP_400_BAD_REQUEST,\n                    field='columns'\n                )\n            else:\n                raise database_base_api_exceptions.IntegrityAPIException(\n                    e,\n                    status_code=status.HTTP_400_BAD_REQUEST,\n                    field='columns'\n                )\n        except UnsupportedTypeException as e:\n            raise database_api_exceptions.UnsupportedTypeAPIException(\n                e,\n                field='columns',\n                status_code=status.HTTP_400_BAD_REQUEST\n            )\n        table_data.update(\n            {\n                # There's no way to reflect actual column data without\n                # creating a view, so we just use the submission, assuming\n                # no errors means we changed to the desired names and types\n                \"columns\": columns,\n                \"records\": preview_records\n            }\n        )\n\n        return Response(table_data)\n", "path": "mathesar/api/db/viewsets/tables.py"}], "after_files": [{"content": "from django.urls import reverse\nfrom psycopg2.errors import DuplicateTable\nfrom rest_framework import serializers, status\nfrom rest_framework.exceptions import ValidationError\nfrom sqlalchemy.exc import ProgrammingError\n\nfrom db.types.base import get_db_type_enum_from_id\n\nfrom mathesar.api.exceptions.validation_exceptions.exceptions import (\n    ColumnSizeMismatchAPIException, DistinctColumnRequiredAPIException,\n    MultipleDataFileAPIException, UnknownDatabaseTypeIdentifier\n)\nfrom mathesar.api.exceptions.database_exceptions.exceptions import DuplicateTableAPIException\nfrom mathesar.api.exceptions.database_exceptions.base_exceptions import ProgrammingAPIException\nfrom mathesar.api.exceptions.validation_exceptions import base_exceptions as base_validation_exceptions\nfrom mathesar.api.exceptions.generic_exceptions import base_exceptions as base_api_exceptions\nfrom mathesar.api.exceptions.mixins import MathesarErrorMessageMixin\nfrom mathesar.api.serializers.columns import SimpleColumnSerializer\nfrom mathesar.models import Table, DataFile\nfrom mathesar.utils.tables import gen_table_name, create_table_from_datafile, create_empty_table\n\n\nclass TableSerializer(MathesarErrorMessageMixin, serializers.ModelSerializer):\n    columns = SimpleColumnSerializer(many=True, required=False)\n    records_url = serializers.SerializerMethodField()\n    constraints_url = serializers.SerializerMethodField()\n    columns_url = serializers.SerializerMethodField()\n    type_suggestions_url = serializers.SerializerMethodField()\n    previews_url = serializers.SerializerMethodField()\n    name = serializers.CharField(required=False, allow_blank=True, default='')\n    data_files = serializers.PrimaryKeyRelatedField(\n        required=False, many=True, queryset=DataFile.objects.all()\n    )\n\n    class Meta:\n        model = Table\n        fields = ['id', 'name', 'schema', 'created_at', 'updated_at', 'import_verified',\n                  'columns', 'records_url', 'constraints_url', 'columns_url',\n                  'type_suggestions_url', 'previews_url', 'data_files',\n                  'has_dependencies']\n\n    def get_records_url(self, obj):\n        if isinstance(obj, Table):\n            # Only get records if we are serializing an existing table\n            request = self.context['request']\n            return request.build_absolute_uri(reverse('table-record-list', kwargs={'table_pk': obj.pk}))\n        else:\n            return None\n\n    def get_constraints_url(self, obj):\n        if isinstance(obj, Table):\n            # Only get constraints if we are serializing an existing table\n            request = self.context['request']\n            return request.build_absolute_uri(reverse('table-constraint-list', kwargs={'table_pk': obj.pk}))\n        else:\n            return None\n\n    def get_columns_url(self, obj):\n        if isinstance(obj, Table):\n            # Only get columns if we are serializing an existing table\n            request = self.context['request']\n            return request.build_absolute_uri(reverse('table-column-list', kwargs={'table_pk': obj.pk}))\n        else:\n            return None\n\n    def get_type_suggestions_url(self, obj):\n        if isinstance(obj, Table):\n            # Only get type suggestions if we are serializing an existing table\n            request = self.context['request']\n            return request.build_absolute_uri(reverse('table-type-suggestions', kwargs={'pk': obj.pk}))\n        else:\n            return None\n\n    def get_previews_url(self, obj):\n        if isinstance(obj, Table):\n            # Only get previews if we are serializing an existing table\n            request = self.context['request']\n            return request.build_absolute_uri(reverse('table-previews', kwargs={'pk': obj.pk}))\n        else:\n            return None\n\n    def validate_data_files(self, data_files):\n        if data_files and len(data_files) > 1:\n            raise MultipleDataFileAPIException()\n        return data_files\n\n    def create(self, validated_data):\n        schema = validated_data['schema']\n        data_files = validated_data.get('data_files')\n        name = validated_data.get('name') or gen_table_name(schema, data_files)\n\n        try:\n            if data_files:\n                table = create_table_from_datafile(data_files, name, schema)\n            else:\n                table = create_empty_table(name, schema)\n        except ProgrammingError as e:\n            if type(e.orig) == DuplicateTable:\n                raise DuplicateTableAPIException(\n                    e,\n                    message=f\"Relation {validated_data['name']} already exists in schema {schema.id}\",\n                    field=\"name\",\n                    status_code=status.HTTP_400_BAD_REQUEST\n                )\n            else:\n                raise ProgrammingAPIException(e)\n        return table\n\n    def update(self, instance, validated_data):\n        if self.partial:\n            # Save the fields that are stored in the model.\n            present_model_fields = []\n            for model_field in instance.MODEL_FIELDS:\n                if model_field in validated_data:\n                    setattr(instance, model_field, validated_data[model_field])\n                    present_model_fields.append(model_field)\n            instance.save(update_fields=present_model_fields)\n            for key in present_model_fields:\n                del validated_data[key]\n            # Save the fields that are stored in the underlying DB.\n            try:\n                instance.update_sa_table(validated_data)\n            except ValueError as e:\n                raise base_api_exceptions.ValueAPIException(e, status_code=status.HTTP_400_BAD_REQUEST)\n        return instance\n\n    def validate(self, data):\n        if self.partial:\n            columns = data.get('columns', None)\n            if columns is not None:\n                for col in columns:\n                    id = col.get('id', None)\n                    if id is None:\n                        message = \"'id' field is required while batch updating columns.\"\n                        raise base_validation_exceptions.MathesarValidationException(ValidationError, message=message)\n        return data\n\n\nclass TablePreviewSerializer(MathesarErrorMessageMixin, serializers.Serializer):\n    name = serializers.CharField(required=False)\n    columns = SimpleColumnSerializer(many=True)\n\n    def validate_columns(self, columns):\n        table = self.context['table']\n        column_names = [col[\"name\"] for col in columns]\n        if not len(column_names) == len(set(column_names)):\n            raise DistinctColumnRequiredAPIException()\n        if not len(columns) == len(table.sa_columns):\n            raise ColumnSizeMismatchAPIException()\n        for column in columns:\n            db_type_id = column['type']\n            db_type = get_db_type_enum_from_id(db_type_id)\n            if db_type is None:\n                raise UnknownDatabaseTypeIdentifier(db_type_id=db_type_id)\n        return columns\n", "path": "mathesar/api/serializers/tables.py"}, {"content": "from django_filters import rest_framework as filters\nfrom psycopg2.errors import InvalidTextRepresentation, CheckViolation\nfrom rest_framework import status, viewsets\nfrom rest_framework.decorators import action\nfrom rest_framework.mixins import ListModelMixin, RetrieveModelMixin, CreateModelMixin\nfrom rest_framework.response import Response\nfrom sqlalchemy.exc import DataError, IntegrityError\n\nfrom mathesar.api.exceptions.database_exceptions import (\n    exceptions as database_api_exceptions,\n    base_exceptions as database_base_api_exceptions,\n)\nfrom db.types.exceptions import UnsupportedTypeException\nfrom mathesar.api.dj_filters import TableFilter\nfrom mathesar.api.pagination import DefaultLimitOffsetPagination\nfrom mathesar.api.serializers.tables import TableSerializer, TablePreviewSerializer\nfrom mathesar.models import Table\nfrom mathesar.utils.tables import (\n    get_table_column_types\n)\n\n\nclass TableViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, viewsets.GenericViewSet):\n    serializer_class = TableSerializer\n    pagination_class = DefaultLimitOffsetPagination\n    filter_backends = (filters.DjangoFilterBackend,)\n    filterset_class = TableFilter\n\n    def get_queryset(self):\n        return Table.objects.all().order_by('-created_at')\n\n    def partial_update(self, request, pk=None):\n        table = self.get_object()\n        serializer = TableSerializer(\n            table, data=request.data, context={'request': request}, partial=True\n        )\n        serializer.is_valid(raise_exception=True)\n        serializer.save()\n\n        # Reload the table to avoid cached properties\n        table = self.get_object()\n        serializer = TableSerializer(table, context={'request': request})\n        return Response(serializer.data)\n\n    def destroy(self, request, pk=None):\n        table = self.get_object()\n        table.delete_sa_table()\n        table.delete()\n        return Response(status=status.HTTP_204_NO_CONTENT)\n\n    @action(methods=['get'], detail=True)\n    def type_suggestions(self, request, pk=None):\n        table = self.get_object()\n        col_types = get_table_column_types(table)\n        return Response(col_types)\n\n    @action(methods=['post'], detail=True)\n    def previews(self, request, pk=None):\n        table = self.get_object()\n        serializer = TablePreviewSerializer(data=request.data, context={\"request\": request, 'table': table})\n        serializer.is_valid(raise_exception=True)\n        columns_field_key = \"columns\"\n        columns = serializer.data[columns_field_key]\n        table_data = TableSerializer(table, context={\"request\": request}).data\n        try:\n            preview_records = table.get_preview(columns)\n        except (DataError, IntegrityError) as e:\n            if type(e.orig) == InvalidTextRepresentation or type(e.orig) == CheckViolation:\n                raise database_api_exceptions.InvalidTypeCastAPIException(\n                    e,\n                    status_code=status.HTTP_400_BAD_REQUEST,\n                    field='columns'\n                )\n            else:\n                raise database_base_api_exceptions.IntegrityAPIException(\n                    e,\n                    status_code=status.HTTP_400_BAD_REQUEST,\n                    field='columns'\n                )\n        except UnsupportedTypeException as e:\n            raise database_api_exceptions.UnsupportedTypeAPIException(\n                e,\n                field='columns',\n                status_code=status.HTTP_400_BAD_REQUEST\n            )\n        table_data.update(\n            {\n                # There's no way to reflect actual column data without\n                # creating a view, so we just use the submission, assuming\n                # no errors means we changed to the desired names and types\n                \"columns\": columns,\n                \"records\": preview_records\n            }\n        )\n\n        return Response(table_data)\n", "path": "mathesar/api/db/viewsets/tables.py"}]}