Repository: django-import-export/django-import-export

Issue:
Can't import file without an `id` column in V4
**Describe the bug**

In V4 I get an error attempting to import data without an `id` column.

**To Reproduce**

When updating to V4 it seems I'm no longer able to import data without an `id` field present in the import data. My expectation (and I think this was the V3 behavior?) is that if `id` is present it is used to check for an existing record but if it's not present a new record  is added.

Example configuration:

```python
class ImportExportResourceBase(resources.ModelResource):
    id = fields.Field(attribute="id")
    child = fields.Field(attribute="child_id", column_name="child_id")
    child_first_name = fields.Field(attribute="child__first_name", readonly=True)
    child_last_name = fields.Field(attribute="child__last_name", readonly=True)

    class Meta:
        clean_model_instances = True
        exclude = ("duration",)


class WeightImportExportResource(ImportExportResourceBase):
    class Meta:
        model = models.Weight


@admin.register(models.Weight)
class WeightAdmin(ImportExportMixin, ExportActionMixin, admin.ModelAdmin):
    list_display = (
        "child",
        "weight",
        "date",
    )
    list_filter = ("child", "tags")
    search_fields = (
        "child__first_name",
        "child__last_name",
        "weight",
    )
    resource_class = WeightImportExportResource
    import_error_display = ('message', 'traceback', 'row')
```

And import file:

```csv
child_id,weight,date,notes
1,10.45,2020-02-14,
1,10.17,2020-02-07,Medical that leader line general marriage plan present. Good garden suggest no. Section than order little share property why. Prepare production imagine commercial seek teach over anything. Reason late consumer.
1,10.04,2020-01-31,Drive usually end than forget. Option value either building car minute the. After resource worker response if. Include second property finish. Music event pick give wind.
1,9.89,2020-01-24,
1,9.68,2020-01-17,
```

When I try to import this file I get errors (for all rows):

```
Errors

    The following fields are declared in 'import_id_fields' but are not present in the file headers: id

    Traceback (most recent call last):
    File "/workspaces/babybuddy/.venv/lib/python3.12/site-packages/import_export/resources.py", line 871, in import_data_inner
    self._check_import_id_fields(dataset.headers)
    File "/workspaces/babybuddy/.venv/lib/python3.12/site-packages/import_export/resources.py", line 1144, in _check_import_id_fields
    raise exceptions.FieldError(
    import_export.exceptions.FieldError: The following fields are declared in 'import_id_fields' but are not present in the file headers: id
    Line number: 1 - "Column 'id' not found in dataset. Available columns are: ['child_id', 'weight', 'date', 'notes']"
    1, 10.45, 2020-02-14,

    Traceback (most recent call last):
    File "/workspaces/babybuddy/.venv/lib/python3.12/site-packages/import_export/fields.py", line 82, in clean
    value = row[self.column_name]
    ~~~^^^^^^^^^^^^^^^^^^
    KeyError: 'id'

    During handling of the above exception, another exception occurred:

    Traceback (most recent call last):
    File "/workspaces/babybuddy/.venv/lib/python3.12/site-packages/import_export/resources.py", line 707, in import_row
    instance, new = self.get_or_init_instance(instance_loader, row)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    File "/workspaces/babybuddy/.venv/lib/python3.12/site-packages/import_export/resources.py", line 177, in get_or_init_instance
    instance = self.get_instance(instance_loader, row)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    File "/workspaces/babybuddy/.venv/lib/python3.12/site-packages/import_export/resources.py", line 170, in get_instance
    return instance_loader.get_instance(row)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    File "/workspaces/babybuddy/.venv/lib/python3.12/site-packages/import_export/instance_loaders.py", line 29, in get_instance
    params[field.attribute] = field.clean(row)
    ^^^^^^^^^^^^^^^^
    File "/workspaces/babybuddy/.venv/lib/python3.12/site-packages/import_export/fields.py", line 84, in clean
    raise KeyError(
    KeyError: "Column 'id' not found in dataset. Available columns are: ['child_id', 'weight', 'date', 'notes']"
```

**Versions (please complete the following information):**

 - Django Import Export: 4.0.0
 - Python: 3.12
 - Django: 5.0.4

**Expected behavior**

If there is no `id` column in the data to import, new entries are added to the database for each row.



Discussion:
Thanks for raising.  Is your use case that you want to always create new records, and there is never any need to check the db for existing rows?

If you need a workaround, this may work (haven't tested) if you declare it on your resource:

```python
   def before_import(self, dataset, **kwargs):
       super().before_import(dataset, **kwargs)
       dataset.append_col([], header='id')
```


> Is your use case that you want to always create new records, and there is never any need to check the db for existing rows?

My use case is that I want to create new records if there is no `id` field in the imported data or update existing records if there is an `id` field. This is how it worked in V3.

I should be able to use the `before_import` approach for the use case (by also checking for the `id` header in the dataset) but this use case feels like the most common/expected one to me. If the new behavior in V4 is necessary this change should probably be called out pretty prominently in the release/upgrade notes.

Resolving patch:
diff --git a/import_export/resources.py b/import_export/resources.py
--- a/import_export/resources.py
+++ b/import_export/resources.py
@@ -167,6 +167,13 @@ def get_instance(self, instance_loader, row):
         """
         Calls the :doc:`InstanceLoader <api_instance_loaders>`.
         """
+        import_id_fields = [self.fields[f] for f in self.get_import_id_fields()]
+        for field in import_id_fields:
+            if field.column_name not in row:
+                # if there is an 'import id field' which is not defined in the
+                # row, then it is not possible to return an existing instance,
+                # so no need to proceed any further
+                return
         return instance_loader.get_instance(row)
 
     def get_or_init_instance(self, instance_loader, row):
@@ -1119,6 +1126,10 @@ def _check_import_id_fields(self, headers):
         missing_fields = list()
         missing_headers = list()
 
+        if self.get_import_id_fields() == ["id"]:
+            # this is the default case, so ok if not present
+            return
+
         for field_name in self.get_import_id_fields():
             if field_name not in self.fields:
                 missing_fields.append(field_name)


Test patch:
diff --git a/tests/core/tests/test_resources/test_import_export.py b/tests/core/tests/test_resources/test_import_export.py
--- a/tests/core/tests/test_resources/test_import_export.py
+++ b/tests/core/tests/test_resources/test_import_export.py
@@ -3,7 +3,7 @@
 
 import tablib
 from core.models import Author, Book, Category
-from core.tests.resources import BookResource
+from core.tests.resources import AuthorResource, BookResource
 from django.test import TestCase
 
 from import_export import exceptions, fields, resources, widgets
@@ -364,3 +364,11 @@ def test_import_with_missing_field_in_row(self, mock_field_save, mock_logger):
             "- column name 'author_email' is not present in row"
         )
         self.assertEqual(2, mock_field_save.call_count)
+
+    def test_import_row_with_no_defined_id_field(self):
+        """Ensure a row with no id field can be imported (issue 1812)."""
+        self.assertEqual(0, Author.objects.count())
+        dataset = tablib.Dataset(*[("J. R. R. Tolkien",)], headers=["name"])
+        self.resource = AuthorResource()
+        self.resource.import_data(dataset)
+        self.assertEqual(1, Author.objects.count())
diff --git a/tests/core/tests/test_resources/test_modelresource/test_error_handling.py b/tests/core/tests/test_resources/test_modelresource/test_error_handling.py
--- a/tests/core/tests/test_resources/test_modelresource/test_error_handling.py
+++ b/tests/core/tests/test_resources/test_modelresource/test_error_handling.py
@@ -11,7 +11,7 @@
 from django.core.exceptions import ImproperlyConfigured, ValidationError
 from django.test import TestCase
 
-from import_export import exceptions, results
+from import_export import exceptions, resources, results
 
 
 class ErrorHandlingTest(TestCase):
@@ -48,8 +48,8 @@ def test_importing_with_line_number_logging(self):
 
     def test_import_data_raises_field_specific_validation_errors(self):
         resource = AuthorResource()
-        dataset = tablib.Dataset(headers=["id", "name", "birthday"])
-        dataset.append(["", "A.A.Milne", "1882test-01-18"])
+        dataset = tablib.Dataset(headers=["name", "birthday"])
+        dataset.append(["A.A.Milne", "1882test-01-18"])
 
         result = resource.import_data(dataset, raise_errors=False)
 
@@ -61,8 +61,6 @@ def test_import_data_raises_field_specific_validation_errors_with_skip_unchanged
         self,
     ):
         resource = AuthorResource()
-        resource._meta.skip_unchanged = True
-
         author = Author.objects.create(name="Some author")
 
         dataset = tablib.Dataset(headers=["id", "birthday"])
@@ -75,14 +73,19 @@ def test_import_data_raises_field_specific_validation_errors_with_skip_unchanged
         self.assertIn("birthday", result.invalid_rows[0].field_specific_errors)
 
     def test_import_data_empty_dataset_with_collect_failed_rows(self):
-        resource = AuthorResource()
+        class _AuthorResource(resources.ModelResource):
+            class Meta:
+                model = Author
+                import_id_fields = ["non_existent_field"]
+
+        resource = _AuthorResource()
         with self.assertRaises(exceptions.ImportError) as e:
             resource.import_data(
                 tablib.Dataset(), collect_failed_rows=True, raise_errors=True
             )
         self.assertEqual(
             "The following fields are declared in 'import_id_fields' "
-            "but are not present in the file headers: id",
+            "but are not present in the resource fields: non_existent_field",
             str(e.exception),
         )