Repository: ckan/ckan

Issue:
Option for removing columns (safer default)
## CKAN version
master

## Describe the bug
#7625 changes the behaviour of `datastore_create` to remove fields(columns) not passed, this is useful but would be safer and more backwards compatible if we require an extra flag to indicate that's what the user intends

### Steps to reproduce
Current stable versions of `datastore_create` allow you to omit existing fields when adding/updating other fields

### Expected behavior
Delete fields only when a user actually asks to

### Additional details


Discussion:

Resolving patch:
diff --git a/ckanext/datastore/backend/postgres.py b/ckanext/datastore/backend/postgres.py
--- a/ckanext/datastore/backend/postgres.py
+++ b/ckanext/datastore/backend/postgres.py
@@ -1096,10 +1096,11 @@ def alter_table(context: Context, data_dict: dict[str, Any]):
                 identifier(f['id']),
                 info_sql))
 
-    for id_ in current_ids - field_ids - set(f['id'] for f in new_fields):
-        alter_sql.append('ALTER TABLE {0} DROP COLUMN {1};'.format(
-            identifier(data_dict['resource_id']),
-            identifier(id_)))
+    if data_dict['delete_fields']:
+        for id_ in current_ids - field_ids - set(f['id'] for f in new_fields):
+            alter_sql.append('ALTER TABLE {0} DROP COLUMN {1};'.format(
+                identifier(data_dict['resource_id']),
+                identifier(id_)))
 
     if alter_sql:
         context['connection'].execute(
diff --git a/ckanext/datastore/logic/action.py b/ckanext/datastore/logic/action.py
--- a/ckanext/datastore/logic/action.py
+++ b/ckanext/datastore/logic/action.py
@@ -60,6 +60,8 @@ def datastore_create(context: Context, data_dict: dict[str, Any]):
     :type aliases: list or comma separated string
     :param fields: fields/columns and their extra metadata. (optional)
     :type fields: list of dictionaries
+    :param delete_fields: set to True to remove existing fields not passed
+    :type delete_fields: bool (optional, default: False)
     :param records: the data, eg: [{"dob": "2005", "some_stuff": ["a", "b"]}]
                     (optional)
     :type records: list of dictionaries
diff --git a/ckanext/datastore/logic/schema.py b/ckanext/datastore/logic/schema.py
--- a/ckanext/datastore/logic/schema.py
+++ b/ckanext/datastore/logic/schema.py
@@ -120,6 +120,7 @@ def datastore_create_schema() -> Schema:
             'type': [ignore_missing],
             'info': [ignore_missing],
         },
+        'delete_fields': [default(False), boolean_validator],
         'primary_key': [ignore_missing, list_of_strings_or_string],
         'indexes': [ignore_missing, list_of_strings_or_string],
         'triggers': {


Test patch:
diff --git a/ckanext/datastore/tests/test_create.py b/ckanext/datastore/tests/test_create.py
--- a/ckanext/datastore/tests/test_create.py
+++ b/ckanext/datastore/tests/test_create.py
@@ -307,7 +307,7 @@ def test_calculate_record_count(self):
         last_analyze = when_was_last_analyze(resource["id"])
         assert last_analyze is not None
 
-    def test_remove_columns(self):
+    def test_delete_fields(self):
         resource = factories.Resource()
         data = {
             "resource_id": resource["id"],
@@ -330,6 +330,20 @@ def test_remove_columns(self):
         }
         helpers.call_action("datastore_create", **data)
         info = helpers.call_action("datastore_info", id=resource["id"])
+        assert [f['id'] for f in info['fields']] == [
+            'col_a', 'col_b', 'col_c', 'col_d'
+        ]
+        data = {
+            "resource_id": resource["id"],
+            "fields": [
+                {"id": "col_a", "type": "text"},
+                {"id": "col_c", "type": "text"},
+            ],
+            "force": True,
+            "delete_fields": True,
+        }
+        helpers.call_action("datastore_create", **data)
+        info = helpers.call_action("datastore_info", id=resource["id"])
         assert [f['id'] for f in info['fields']] == ['col_a', 'col_c']