Repository: mathesar-foundation/mathesar

Issue:
 Column API should support creating columns without names 
## Problem
The `name` parameter for the column API shouldn't be required.

## Proposed solution
We should auto-generate a name if it was not specified.

## Additional context
Similar to #449. Please follow a similar naming scheme of `Column n`.



Discussion:
@kgodey Can i work on this one?

@arvind-1 Yes, assigned. Thank you!
@arvind-1 How's this issue going? Do you need any help?
@kgodey working on it following the other PR which was similar to this. I will let you know if needed any help. Thank you. 
@arvind-1 Checking in on this since it's been a couple of months.
@arvind-1 I've unassigned you since we haven't heard from you in a few months. 
Hey @kgodey, I would like to take up this issue, can you please point me to some docs related to the project that would help me solve this issue. I have already taken a look at #449.
@Anish9901 I've assigned this issue to you. We do not have enough docs documenting code level architecture at the moment. Please feel free to ask your questions on our [matrix channels](https://wiki.mathesar.org/en/community/matrix).

It's night at @kgodey's part of the world, she might have additional pointers for you when she's back online.
ok, @pavish 
@Anish9901 Please see `db/columns/operations/create.py` for the db-layer part of the code. You'll need to add a bit of logic there. Please let us know here or on Matrix if you have some specific questions. The developer docs are a bit light as we're moving quickly to get this released.

@mathemancer do we need to have the input field popup for adding the column name while creating a new column ? Or the + should generate a new column which the user can rename later as they wish.
@Anish9901 There's no frontend changes necessary for this issue, it's purely a backend issue to enhance the capabilities of our API.
@kgodey Ok. Also ,How should I change the incoming request.data when an empty string is added in the column name field?
> @kgodey Ok. Also ,How should I change the incoming request.data when an empty string is added in the column name field?

@Anish9901 I'm not sure what you mean, could you elaborate?
@kgodey Currently I am removing the disabled tag inside the html and adding the column without any name, after that I am updating the `request.data['name']` to `Column n`. 
@Anish9901 I'm still not sure what you're referring to. You shouldn't need to make any changes in the HTML.

Could you open a draft pull request with what you have so far? I think it will be easier to discuss if we can see your code.

Resolving patch:
diff --git a/db/columns/operations/create.py b/db/columns/operations/create.py
--- a/db/columns/operations/create.py
+++ b/db/columns/operations/create.py
@@ -19,8 +19,14 @@
 from db.tables.operations.select import reflect_table_from_oid
 from db.types.operations.cast import get_supported_alter_column_types
 
+COLUMN_NAME_TEMPLATE = 'Column'
+
 
 def create_column(engine, table_oid, column_data):
+    table = reflect_table_from_oid(table_oid, engine)
+    column_name = column_data.get(NAME, '').strip()
+    if column_name == '':
+        column_data[NAME] = gen_col_name(table)
     column_type = column_data.get(TYPE, column_data.get("type"))
     column_type_options = column_data.get("type_options", {})
     column_nullable = column_data.get(NULLABLE, True)
@@ -67,6 +73,13 @@ def create_column(engine, table_oid, column_data):
     )
 
 
+def gen_col_name(table):
+    base_name = COLUMN_NAME_TEMPLATE
+    col_num = len(table.c)
+    name = f'{base_name} {col_num}'
+    return name
+
+
 def _gen_col_name(table, column_name):
     num = 1
     new_column_name = f"{column_name}_{num}"
diff --git a/mathesar/api/serializers/columns.py b/mathesar/api/serializers/columns.py
--- a/mathesar/api/serializers/columns.py
+++ b/mathesar/api/serializers/columns.py
@@ -95,7 +95,7 @@ class Meta(SimpleColumnSerializer.Meta):
         )
         model_fields = ('display_options',)
 
-    name = serializers.CharField(required=False)
+    name = serializers.CharField(required=False, allow_blank=True)
 
     # From scratch fields
     type = serializers.CharField(source='plain_type', required=False)
@@ -116,7 +116,7 @@ class Meta(SimpleColumnSerializer.Meta):
 
     def validate(self, data):
         if not self.partial:
-            from_scratch_required_fields = ['name', 'type']
+            from_scratch_required_fields = ['type']
             from_scratch_specific_fields = ['type', 'nullable', 'primary_key']
             from_dupe_required_fields = ['source_column']
             from_dupe_specific_fields = ['source_column', 'copy_source_data',


Test patch:
diff --git a/db/tests/columns/operations/test_create.py b/db/tests/columns/operations/test_create.py
--- a/db/tests/columns/operations/test_create.py
+++ b/db/tests/columns/operations/test_create.py
@@ -1,7 +1,7 @@
 import pytest
 from sqlalchemy import Integer, Column, Table, MetaData, Numeric, UniqueConstraint
 
-from db.columns.operations.create import create_column, duplicate_column
+from db.columns.operations.create import create_column, duplicate_column, gen_col_name
 from db.columns.operations.select import get_column_attnum_from_name, get_column_default
 from db.tables.operations.select import get_oid_from_table, reflect_table_from_oid
 from db.constraints.operations.select import get_column_constraints
@@ -357,3 +357,75 @@ def test_duplicate_column_default(engine_with_schema, copy_data, copy_constraint
         assert default == expt_default
     else:
         assert default is None
+
+
+def test_create_column_accepts_column_data_without_name_attribute(engine_email_type):
+    engine, schema = engine_email_type
+    table_name = "atableone"
+    initial_column_name = "Column 0"
+    expected_column_name = "Column 1"
+    table = Table(
+        table_name,
+        MetaData(bind=engine, schema=schema),
+        Column(initial_column_name, Integer),
+    )
+    table.create()
+    table_oid = get_oid_from_table(table_name, schema, engine)
+    column_data = {"type": "BOOLEAN"}
+    created_col = create_column(engine, table_oid, column_data)
+    altered_table = reflect_table_from_oid(table_oid, engine)
+    assert len(altered_table.columns) == 2
+    assert created_col.name == expected_column_name
+
+
+def test_create_column_accepts_column_data_with_name_as_empty_string(engine_email_type):
+    engine, schema = engine_email_type
+    table_name = "atableone"
+    initial_column_name = "Column 0"
+    expected_column_name = "Column 1"
+    table = Table(
+        table_name,
+        MetaData(bind=engine, schema=schema),
+        Column(initial_column_name, Integer),
+    )
+    table.create()
+    table_oid = get_oid_from_table(table_name, schema, engine)
+    column_data = {"name": "", "type": "BOOLEAN"}
+    created_col = create_column(engine, table_oid, column_data)
+    altered_table = reflect_table_from_oid(table_oid, engine)
+    assert len(altered_table.columns) == 2
+    assert created_col.name == expected_column_name
+
+
+def test_generate_column_name(engine_email_type):
+    engine, schema = engine_email_type
+    name_set = {
+        'Center',
+        'Status',
+        'Case Number',
+        'Patent Number',
+        'Application SN',
+        'Title',
+        'Patent Expiration Date',
+        ''
+    }
+    table_name = "atableone"
+    initial_column_name = "id"
+    table = Table(
+        table_name,
+        MetaData(bind=engine, schema=schema),
+        Column(initial_column_name, Integer),
+    )
+    table.create()
+    table_oid = get_oid_from_table(table_name, schema, engine)
+    for name in name_set:
+        column_data = {"name": name, "type": "BOOLEAN"}
+        create_column(engine, table_oid, column_data)
+    altered_table = reflect_table_from_oid(table_oid, engine)
+    n = len(name_set) + 1
+    # Expected column name should be 'Column n'
+    # where n is length of number of columns already in the table
+    expected_column_name = f"Column {n}"
+    generated_column_name = gen_col_name(altered_table)
+    assert len(altered_table.columns) == n
+    assert generated_column_name == expected_column_name
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
@@ -389,6 +389,49 @@ def test_column_create_some_parameters(column_test_table, client):
     assert response_data['field'] == "type"
 
 
+def test_column_create_no_name_parameter(column_test_table, client):
+    cache.clear()
+    type_ = "BOOLEAN"
+    num_columns = len(column_test_table.sa_columns)
+    generated_name = f"Column {num_columns}"
+    data = {
+        "type": type_
+    }
+    response = client.post(
+        f"/api/db/v0/tables/{column_test_table.id}/columns/", data=data
+    )
+    assert response.status_code == 201
+    new_columns_response = client.get(
+        f"/api/db/v0/tables/{column_test_table.id}/columns/"
+    )
+    assert new_columns_response.json()["count"] == num_columns + 1
+    actual_new_col = new_columns_response.json()["results"][-1]
+    assert actual_new_col["name"] == generated_name
+    assert actual_new_col["type"] == type_
+
+
+def test_column_create_name_parameter_empty(column_test_table, client):
+    cache.clear()
+    name = ""
+    type_ = "BOOLEAN"
+    num_columns = len(column_test_table.sa_columns)
+    generated_name = f"Column {num_columns}"
+    data = {
+        "name": name, "type": type_
+    }
+    response = client.post(
+        f"/api/db/v0/tables/{column_test_table.id}/columns/", data=data
+    )
+    assert response.status_code == 201
+    new_columns_response = client.get(
+        f"/api/db/v0/tables/{column_test_table.id}/columns/"
+    )
+    assert new_columns_response.json()["count"] == num_columns + 1
+    actual_new_col = new_columns_response.json()["results"][-1]
+    assert actual_new_col["name"] == generated_name
+    assert actual_new_col["type"] == type_
+
+
 def test_column_update_name(column_test_table, client):
     cache.clear()
     name = "updatedname"
@@ -754,6 +797,4 @@ def test_column_duplicate_no_parameters(column_test_table, client):
     response_data = response.json()
     assert response.status_code == 400
     assert response_data[0]["message"] == "This field is required."
-    assert response_data[0]["field"] == "name"
-    assert response_data[1]["message"] == "This field is required."
-    assert response_data[1]["field"] == "type"
+    assert response_data[0]["field"] == "type"