prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Fix API handling of multi-column UNIQUE and PK columns
  See https://github.com/centerofci/mathesar/issues/2231#issuecomment-1380309139 for some insight.
  Fix API handling of multi-column UNIQUE and PK columns
  See https://github.com/centerofci/mathesar/issues/2231#issuecomment-1380309139 for some insight.
  
  --- 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/constraints.py`
  Content:
  ```
  1 from psycopg.errors import DuplicateTable, UniqueViolation
  2 from rest_framework import serializers, status
  3 
  4 from db.constraints import utils as constraint_utils
  5 from db.identifiers import is_identifier_too_long
  6 from db.constraints.base import ForeignKeyConstraint, UniqueConstraint
  7 
  8 import mathesar.api.exceptions.database_exceptions.exceptions as database_api_exceptions
  9 from mathesar.api.exceptions.validation_exceptions.exceptions import (
  10     ConstraintColumnEmptyAPIException, UnsupportedConstraintAPIException,
  11     InvalidTableName
  12 )
  13 from mathesar.api.serializers.shared_serializers import (
  14     MathesarPolymorphicErrorMixin,
  15     ReadWritePolymorphicSerializerMappingMixin,
  16 )
  17 from mathesar.models.base import Column, Constraint, Table
  18 
  19 
  20 class Table_Filtered_Column_queryset(serializers.PrimaryKeyRelatedField):
  21     def get_queryset(self):
  22         table_id = self.context.get('table_id', None)
  23         queryset = super(Table_Filtered_Column_queryset, self).get_queryset()
  24         if table_id is None or not queryset:
  25             return None
  26         return queryset.filter(table__id=table_id)
  27 
  28 
  29 class BaseConstraintSerializer(serializers.ModelSerializer):
  30     name = serializers.CharField(required=False)
  31     type = serializers.CharField()
  32     columns = Table_Filtered_Column_queryset(queryset=Column.current_objects, many=True)
  33 
  34     class Meta:
  35         model = Constraint
  36         fields = ['id', 'name', 'type', 'columns']
  37 
  38     def construct_constraint_obj(self, table, data):
  39         columns_attnum = [column.attnum for column in data.get('columns')]
  40         if data.get('type') == constraint_utils.ConstraintType.UNIQUE.value:
  41             return UniqueConstraint(data.get('name', None), table.oid, columns_attnum)
  42         return None
  43 
  44     def create(self, validated_data):
  45         table = self.context['table']
  46         constraint_obj = self.construct_constraint_obj(table, validated_data)
  47         # Additional check is needed because we support read operations for primary key constraint,
  48         # but we don't support write operations
  49         if constraint_obj is None:
  50             constraint_type = validated_data.get('type', None)
  51             raise UnsupportedConstraintAPIException(constraint_type=constraint_type, field='type')
  52         try:
  53             constraint = table.add_constraint(constraint_obj)
  54         except DuplicateTable as e:
  55             raise database_api_exceptions.DuplicateTableAPIException(
  56                 e,
  57                 message='Relation with the same name already exists',
  58                 status_code=status.HTTP_400_BAD_REQUEST
  59             )
  60         except UniqueViolation as e:
  61             raise database_api_exceptions.UniqueViolationAPIException(
  62                 e,
  63                 status_code=status.HTTP_400_BAD_REQUEST
  64             )
  65         return constraint
  66 
  67     def validate_name(self, name):
  68         if is_identifier_too_long(name):
  69             raise database_api_exceptions.IdentifierTooLong(field='name')
  70         return name
  71 
  72 
  73 class ForeignKeyConstraintSerializer(BaseConstraintSerializer):
  74     class Meta:
  75         model = Constraint
  76         fields = BaseConstraintSerializer.Meta.fields + [
  77             'referent_columns',
  78             'referent_table',
  79             'onupdate',
  80             'ondelete',
  81             'deferrable',
  82             'match'
  83         ]
  84 
  85     referent_columns = serializers.PrimaryKeyRelatedField(queryset=Column.current_objects.all(), many=True)
  86     referent_table = serializers.SerializerMethodField()
  87     onupdate = serializers.ChoiceField(
  88         choices=['RESTRICT', 'CASCADE', 'SET NULL', 'NO ACTION', 'SET DEFAULT'],
  89         required=False,
  90         allow_null=True
  91     )
  92     ondelete = serializers.ChoiceField(
  93         choices=['RESTRICT', 'CASCADE', 'SET NULL', 'NO ACTION', 'SET DEFAULT'],
  94         required=False,
  95         allow_null=True
  96     )
  97     deferrable = serializers.BooleanField(allow_null=True, required=False)
  98     match = serializers.ChoiceField(choices=['SIMPLE', 'PARTIAL', 'FULL'], allow_null=True, required=False)
  99 
  100     def get_referent_table(self, obj):
  101         return obj.referent_columns[0].table.id
  102 
  103     def construct_constraint_obj(self, table, data):
  104         columns_attnum = [column.attnum for column in data.get('columns')]
  105         referent_columns = data.get('referent_columns')
  106         referent_columns_attnum = [column.attnum for column in referent_columns]
  107         constraint_options_fields = ['onupdate', 'ondelete', 'deferrable']
  108         constraint_options = {
  109             constraint_options_field: data[constraint_options_field]
  110             for constraint_options_field in constraint_options_fields if constraint_options_field in data
  111         }
  112         return ForeignKeyConstraint(
  113             data.get('name', None),
  114             table.oid,
  115             columns_attnum,
  116             referent_columns[0].table.oid,
  117             referent_columns_attnum,
  118             constraint_options
  119         )
  120 
  121 
  122 class ConstraintSerializer(
  123     ReadWritePolymorphicSerializerMappingMixin,
  124     MathesarPolymorphicErrorMixin,
  125     serializers.ModelSerializer
  126 ):
  127     class Meta:
  128         model = Constraint
  129         fields = '__all__'
  130 
  131     serializers_mapping = {
  132         'foreignkey': ForeignKeyConstraintSerializer,
  133         'primary': BaseConstraintSerializer,
  134         'unique': BaseConstraintSerializer,
  135     }
  136 
  137     def get_mapping_field(self, data):
  138         if isinstance(data, Constraint):
  139             constraint_type = data.type
  140         else:
  141             constraint_type = data.get('type', None)
  142         assert constraint_type is not None
  143         return constraint_type
  144 
  145     def create(self, validated_data):
  146         serializer = self.get_serializer_class(self.get_mapping_field(validated_data))
  147         return serializer.create(validated_data)
  148 
  149     def run_validation(self, data):
  150         if referent_table := data.get('referent_table', None):
  151             referent_table_name = Table.current_objects.get(id=referent_table).name
  152             if any(
  153                 invalid_char in referent_table_name
  154                 for invalid_char in ('(', ')')
  155             ):
  156                 raise InvalidTableName(
  157                     referent_table_name,
  158                     field='referent_table'
  159                 )
  160         constraint_type = data.get('type', None)
  161         if constraint_type not in self.serializers_mapping.keys():
  162             raise UnsupportedConstraintAPIException(constraint_type=constraint_type)
  163         columns = data.get('columns', None)
  164         if columns == []:
  165             raise ConstraintColumnEmptyAPIException(field='columns')
  166         return super(ConstraintSerializer, self).run_validation(data)
  167
  ```
  --- 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()
  
  ```
  
golden_diff: diff --git a/mathesar/api/serializers/constraints.py b/mathesar/api/serializers/constraints.py
  --- a/mathesar/api/serializers/constraints.py
  +++ b/mathesar/api/serializers/constraints.py
  @@ -17,10 +17,17 @@
   from mathesar.models.base import Column, Constraint, Table
   
   
  -class Table_Filtered_Column_queryset(serializers.PrimaryKeyRelatedField):
  +class TableFilteredPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
  +    """
  +    Limits the accepted related primary key values to a specific table.
  +    For example, if the PrimaryKeyRelatedField is instantiated with a
  +    Column queryset, only columns in the "associated table" are
  +    accepted. The "associated table" is defined by the context dict's
  +    `table_id` value.
  +    """
       def get_queryset(self):
           table_id = self.context.get('table_id', None)
  -        queryset = super(Table_Filtered_Column_queryset, self).get_queryset()
  +        queryset = super(TableFilteredPrimaryKeyRelatedField, self).get_queryset()
           if table_id is None or not queryset:
               return None
           return queryset.filter(table__id=table_id)
  @@ -29,7 +36,7 @@
   class BaseConstraintSerializer(serializers.ModelSerializer):
       name = serializers.CharField(required=False)
       type = serializers.CharField()
  -    columns = Table_Filtered_Column_queryset(queryset=Column.current_objects, many=True)
  +    columns = TableFilteredPrimaryKeyRelatedField(queryset=Column.current_objects, many=True)
   
       class Meta:
           model = Constraint
  
verification_info: {"golden_diff": "diff --git a/mathesar/api/serializers/constraints.py b/mathesar/api/serializers/constraints.py\n--- a/mathesar/api/serializers/constraints.py\n+++ b/mathesar/api/serializers/constraints.py\n@@ -17,10 +17,17 @@\n from mathesar.models.base import Column, Constraint, Table\n \n \n-class Table_Filtered_Column_queryset(serializers.PrimaryKeyRelatedField):\n+class TableFilteredPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):\n+    \"\"\"\n+    Limits the accepted related primary key values to a specific table.\n+    For example, if the PrimaryKeyRelatedField is instantiated with a\n+    Column queryset, only columns in the \"associated table\" are\n+    accepted. The \"associated table\" is defined by the context dict's\n+    `table_id` value.\n+    \"\"\"\n     def get_queryset(self):\n         table_id = self.context.get('table_id', None)\n-        queryset = super(Table_Filtered_Column_queryset, self).get_queryset()\n+        queryset = super(TableFilteredPrimaryKeyRelatedField, self).get_queryset()\n         if table_id is None or not queryset:\n             return None\n         return queryset.filter(table__id=table_id)\n@@ -29,7 +36,7 @@\n class BaseConstraintSerializer(serializers.ModelSerializer):\n     name = serializers.CharField(required=False)\n     type = serializers.CharField()\n-    columns = Table_Filtered_Column_queryset(queryset=Column.current_objects, many=True)\n+    columns = TableFilteredPrimaryKeyRelatedField(queryset=Column.current_objects, many=True)\n \n     class Meta:\n         model = Constraint\n", "issue": "Fix API handling of multi-column UNIQUE and PK columns\nSee https://github.com/centerofci/mathesar/issues/2231#issuecomment-1380309139 for some insight.\nFix API handling of multi-column UNIQUE and PK columns\nSee https://github.com/centerofci/mathesar/issues/2231#issuecomment-1380309139 for some insight.\n", "before_files": [{"content": "from psycopg.errors import DuplicateTable, UniqueViolation\nfrom rest_framework import serializers, status\n\nfrom db.constraints import utils as constraint_utils\nfrom db.identifiers import is_identifier_too_long\nfrom db.constraints.base import ForeignKeyConstraint, UniqueConstraint\n\nimport mathesar.api.exceptions.database_exceptions.exceptions as database_api_exceptions\nfrom mathesar.api.exceptions.validation_exceptions.exceptions import (\n    ConstraintColumnEmptyAPIException, UnsupportedConstraintAPIException,\n    InvalidTableName\n)\nfrom mathesar.api.serializers.shared_serializers import (\n    MathesarPolymorphicErrorMixin,\n    ReadWritePolymorphicSerializerMappingMixin,\n)\nfrom mathesar.models.base import Column, Constraint, Table\n\n\nclass Table_Filtered_Column_queryset(serializers.PrimaryKeyRelatedField):\n    def get_queryset(self):\n        table_id = self.context.get('table_id', None)\n        queryset = super(Table_Filtered_Column_queryset, self).get_queryset()\n        if table_id is None or not queryset:\n            return None\n        return queryset.filter(table__id=table_id)\n\n\nclass BaseConstraintSerializer(serializers.ModelSerializer):\n    name = serializers.CharField(required=False)\n    type = serializers.CharField()\n    columns = Table_Filtered_Column_queryset(queryset=Column.current_objects, many=True)\n\n    class Meta:\n        model = Constraint\n        fields = ['id', 'name', 'type', 'columns']\n\n    def construct_constraint_obj(self, table, data):\n        columns_attnum = [column.attnum for column in data.get('columns')]\n        if data.get('type') == constraint_utils.ConstraintType.UNIQUE.value:\n            return UniqueConstraint(data.get('name', None), table.oid, columns_attnum)\n        return None\n\n    def create(self, validated_data):\n        table = self.context['table']\n        constraint_obj = self.construct_constraint_obj(table, validated_data)\n        # Additional check is needed because we support read operations for primary key constraint,\n        # but we don't support write operations\n        if constraint_obj is None:\n            constraint_type = validated_data.get('type', None)\n            raise UnsupportedConstraintAPIException(constraint_type=constraint_type, field='type')\n        try:\n            constraint = table.add_constraint(constraint_obj)\n        except DuplicateTable as e:\n            raise database_api_exceptions.DuplicateTableAPIException(\n                e,\n                message='Relation with the same name already exists',\n                status_code=status.HTTP_400_BAD_REQUEST\n            )\n        except UniqueViolation as e:\n            raise database_api_exceptions.UniqueViolationAPIException(\n                e,\n                status_code=status.HTTP_400_BAD_REQUEST\n            )\n        return constraint\n\n    def validate_name(self, name):\n        if is_identifier_too_long(name):\n            raise database_api_exceptions.IdentifierTooLong(field='name')\n        return name\n\n\nclass ForeignKeyConstraintSerializer(BaseConstraintSerializer):\n    class Meta:\n        model = Constraint\n        fields = BaseConstraintSerializer.Meta.fields + [\n            'referent_columns',\n            'referent_table',\n            'onupdate',\n            'ondelete',\n            'deferrable',\n            'match'\n        ]\n\n    referent_columns = serializers.PrimaryKeyRelatedField(queryset=Column.current_objects.all(), many=True)\n    referent_table = serializers.SerializerMethodField()\n    onupdate = serializers.ChoiceField(\n        choices=['RESTRICT', 'CASCADE', 'SET NULL', 'NO ACTION', 'SET DEFAULT'],\n        required=False,\n        allow_null=True\n    )\n    ondelete = serializers.ChoiceField(\n        choices=['RESTRICT', 'CASCADE', 'SET NULL', 'NO ACTION', 'SET DEFAULT'],\n        required=False,\n        allow_null=True\n    )\n    deferrable = serializers.BooleanField(allow_null=True, required=False)\n    match = serializers.ChoiceField(choices=['SIMPLE', 'PARTIAL', 'FULL'], allow_null=True, required=False)\n\n    def get_referent_table(self, obj):\n        return obj.referent_columns[0].table.id\n\n    def construct_constraint_obj(self, table, data):\n        columns_attnum = [column.attnum for column in data.get('columns')]\n        referent_columns = data.get('referent_columns')\n        referent_columns_attnum = [column.attnum for column in referent_columns]\n        constraint_options_fields = ['onupdate', 'ondelete', 'deferrable']\n        constraint_options = {\n            constraint_options_field: data[constraint_options_field]\n            for constraint_options_field in constraint_options_fields if constraint_options_field in data\n        }\n        return ForeignKeyConstraint(\n            data.get('name', None),\n            table.oid,\n            columns_attnum,\n            referent_columns[0].table.oid,\n            referent_columns_attnum,\n            constraint_options\n        )\n\n\nclass ConstraintSerializer(\n    ReadWritePolymorphicSerializerMappingMixin,\n    MathesarPolymorphicErrorMixin,\n    serializers.ModelSerializer\n):\n    class Meta:\n        model = Constraint\n        fields = '__all__'\n\n    serializers_mapping = {\n        'foreignkey': ForeignKeyConstraintSerializer,\n        'primary': BaseConstraintSerializer,\n        'unique': BaseConstraintSerializer,\n    }\n\n    def get_mapping_field(self, data):\n        if isinstance(data, Constraint):\n            constraint_type = data.type\n        else:\n            constraint_type = data.get('type', None)\n        assert constraint_type is not None\n        return constraint_type\n\n    def create(self, validated_data):\n        serializer = self.get_serializer_class(self.get_mapping_field(validated_data))\n        return serializer.create(validated_data)\n\n    def run_validation(self, data):\n        if referent_table := data.get('referent_table', None):\n            referent_table_name = Table.current_objects.get(id=referent_table).name\n            if any(\n                invalid_char in referent_table_name\n                for invalid_char in ('(', ')')\n            ):\n                raise InvalidTableName(\n                    referent_table_name,\n                    field='referent_table'\n                )\n        constraint_type = data.get('type', None)\n        if constraint_type not in self.serializers_mapping.keys():\n            raise UnsupportedConstraintAPIException(constraint_type=constraint_type)\n        columns = data.get('columns', None)\n        if columns == []:\n            raise ConstraintColumnEmptyAPIException(field='columns')\n        return super(ConstraintSerializer, self).run_validation(data)\n", "path": "mathesar/api/serializers/constraints.py"}], "after_files": [{"content": "from psycopg.errors import DuplicateTable, UniqueViolation\nfrom rest_framework import serializers, status\n\nfrom db.constraints import utils as constraint_utils\nfrom db.identifiers import is_identifier_too_long\nfrom db.constraints.base import ForeignKeyConstraint, UniqueConstraint\n\nimport mathesar.api.exceptions.database_exceptions.exceptions as database_api_exceptions\nfrom mathesar.api.exceptions.validation_exceptions.exceptions import (\n    ConstraintColumnEmptyAPIException, UnsupportedConstraintAPIException,\n    InvalidTableName\n)\nfrom mathesar.api.serializers.shared_serializers import (\n    MathesarPolymorphicErrorMixin,\n    ReadWritePolymorphicSerializerMappingMixin,\n)\nfrom mathesar.models.base import Column, Constraint, Table\n\n\nclass TableFilteredPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):\n    \"\"\"\n    Limits the accepted related primary key values to a specific table.\n    For example, if the PrimaryKeyRelatedField is instantiated with a\n    Column queryset, only columns in the \"associated table\" are\n    accepted. The \"associated table\" is defined by the context dict's\n    `table_id` value.\n    \"\"\"\n    def get_queryset(self):\n        table_id = self.context.get('table_id', None)\n        queryset = super(TableFilteredPrimaryKeyRelatedField, self).get_queryset()\n        if table_id is None or not queryset:\n            return None\n        return queryset.filter(table__id=table_id)\n\n\nclass BaseConstraintSerializer(serializers.ModelSerializer):\n    name = serializers.CharField(required=False)\n    type = serializers.CharField()\n    columns = TableFilteredPrimaryKeyRelatedField(queryset=Column.current_objects, many=True)\n\n    class Meta:\n        model = Constraint\n        fields = ['id', 'name', 'type', 'columns']\n\n    def construct_constraint_obj(self, table, data):\n        columns_attnum = [column.attnum for column in data.get('columns')]\n        if data.get('type') == constraint_utils.ConstraintType.UNIQUE.value:\n            return UniqueConstraint(data.get('name', None), table.oid, columns_attnum)\n        return None\n\n    def create(self, validated_data):\n        table = self.context['table']\n        constraint_obj = self.construct_constraint_obj(table, validated_data)\n        # Additional check is needed because we support read operations for primary key constraint,\n        # but we don't support write operations\n        if constraint_obj is None:\n            constraint_type = validated_data.get('type', None)\n            raise UnsupportedConstraintAPIException(constraint_type=constraint_type, field='type')\n        try:\n            constraint = table.add_constraint(constraint_obj)\n        except DuplicateTable as e:\n            raise database_api_exceptions.DuplicateTableAPIException(\n                e,\n                message='Relation with the same name already exists',\n                status_code=status.HTTP_400_BAD_REQUEST\n            )\n        except UniqueViolation as e:\n            raise database_api_exceptions.UniqueViolationAPIException(\n                e,\n                status_code=status.HTTP_400_BAD_REQUEST\n            )\n        return constraint\n\n    def validate_name(self, name):\n        if is_identifier_too_long(name):\n            raise database_api_exceptions.IdentifierTooLong(field='name')\n        return name\n\n\nclass ForeignKeyConstraintSerializer(BaseConstraintSerializer):\n    class Meta:\n        model = Constraint\n        fields = BaseConstraintSerializer.Meta.fields + [\n            'referent_columns',\n            'referent_table',\n            'onupdate',\n            'ondelete',\n            'deferrable',\n            'match'\n        ]\n\n    referent_columns = serializers.PrimaryKeyRelatedField(queryset=Column.current_objects.all(), many=True)\n    referent_table = serializers.SerializerMethodField()\n    onupdate = serializers.ChoiceField(\n        choices=['RESTRICT', 'CASCADE', 'SET NULL', 'NO ACTION', 'SET DEFAULT'],\n        required=False,\n        allow_null=True\n    )\n    ondelete = serializers.ChoiceField(\n        choices=['RESTRICT', 'CASCADE', 'SET NULL', 'NO ACTION', 'SET DEFAULT'],\n        required=False,\n        allow_null=True\n    )\n    deferrable = serializers.BooleanField(allow_null=True, required=False)\n    match = serializers.ChoiceField(choices=['SIMPLE', 'PARTIAL', 'FULL'], allow_null=True, required=False)\n\n    def get_referent_table(self, obj):\n        return obj.referent_columns[0].table.id\n\n    def construct_constraint_obj(self, table, data):\n        columns_attnum = [column.attnum for column in data.get('columns')]\n        referent_columns = data.get('referent_columns')\n        referent_columns_attnum = [column.attnum for column in referent_columns]\n        constraint_options_fields = ['onupdate', 'ondelete', 'deferrable']\n        constraint_options = {\n            constraint_options_field: data[constraint_options_field]\n            for constraint_options_field in constraint_options_fields if constraint_options_field in data\n        }\n        return ForeignKeyConstraint(\n            data.get('name', None),\n            table.oid,\n            columns_attnum,\n            referent_columns[0].table.oid,\n            referent_columns_attnum,\n            constraint_options\n        )\n\n\nclass ConstraintSerializer(\n    ReadWritePolymorphicSerializerMappingMixin,\n    MathesarPolymorphicErrorMixin,\n    serializers.ModelSerializer\n):\n    class Meta:\n        model = Constraint\n        fields = '__all__'\n\n    serializers_mapping = {\n        'foreignkey': ForeignKeyConstraintSerializer,\n        'primary': BaseConstraintSerializer,\n        'unique': BaseConstraintSerializer,\n    }\n\n    def get_mapping_field(self, data):\n        if isinstance(data, Constraint):\n            constraint_type = data.type\n        else:\n            constraint_type = data.get('type', None)\n        assert constraint_type is not None\n        return constraint_type\n\n    def create(self, validated_data):\n        serializer = self.get_serializer_class(self.get_mapping_field(validated_data))\n        return serializer.create(validated_data)\n\n    def run_validation(self, data):\n        if referent_table := data.get('referent_table', None):\n            referent_table_name = Table.current_objects.get(id=referent_table).name\n            if any(\n                invalid_char in referent_table_name\n                for invalid_char in ('(', ')')\n            ):\n                raise InvalidTableName(\n                    referent_table_name,\n                    field='referent_table'\n                )\n        constraint_type = data.get('type', None)\n        if constraint_type not in self.serializers_mapping.keys():\n            raise UnsupportedConstraintAPIException(constraint_type=constraint_type)\n        columns = data.get('columns', None)\n        if columns == []:\n            raise ConstraintColumnEmptyAPIException(field='columns')\n        return super(ConstraintSerializer, self).run_validation(data)\n", "path": "mathesar/api/serializers/constraints.py"}]}