Repository: tortoise/tortoise-orm

Issue:
v0.18.1: bulk_create doesn't work correctly with more than 1 update_fields
**Describe the bug**
`bulk_create` works correctly when only a single field is specified in param `update_fields` but not when specifying more than one field.

**To Reproduce**
Model:
```
class Books(models.Model):
    id = fields.BigIntField(pk=True)
    author = fields.CharField()
    name = fields.CharField()
    description = fields.CharField()
    code = fields.CharField()

    class Meta:
        table = "books"
        unique_together=("author", "name")       
``` 
Create data:
```
# Book 1
await Books.create(**{ 'author': 'Joe', 'name': 'Book of the Dead', 'description': 'none', 'code': 'missing' })
# Book 2
await Books.create(**{ 'author': 'Dean', 'name': 'Book of the Dead', 'description': 'none', 'code': 'missing' })
```
Upsert works correctly: Update only description of Book 1
```
b1 = Books(**{ 'author': 'Joe', 'name': 'Book of the Dead', 'description': 'BOD', 'code': 'book_of_the_dead' })
await Books.bulk_create(
    [b1],
    update_fields=["description"],
    on_conflict=["author","name"],
)

# Generated SQL
INSERT INTO "books" (
    "author",
    "name",
    "description",
    "code"
    )
VALUES (
        $1
    ) ON CONFLICT (
        "author",
        "name",
    ) DO
UPDATE
SET "description" = EXCLUDED."description"
RETURNING "id"
```
Upsert DOES NOT work correctly: Update description and code of Book 2:
```
b2 = Books(**{ 'author': 'Dean', 'name': 'Book of the Dead', 'description': 'BOD', 'code': 'book_of_the_dead' })
await Books.bulk_create(
    [b2],
    update_fields=["description", "code"],
    on_conflict=["author","name"],
)

# Generated SQL:
INSERT INTO "books" (
    "author",
    "name",
    "description",
    "code"
    )
VALUES (
        $1
    ) ON CONFLICT (
        "author",
        "name",
    ) DO
UPDATE
SET "description" = 'code'
RETURNING "id"
```
Notice the second last line of the generated SQL with the issue.
Additionally if you pass more than 2 values (e.g. 4) you get an error:
```
do_update() takes from 2 to 3 positional arguments but 4 were given
```


**Expected behavior**
Should be able to pass unlimited number of values in `update_fields` param




Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 91a9ba9..3f78e3f 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -8,6 +8,12 @@ Changelog
 
 0.18
 ====
+0.18.2
+------
+Fixed
+^^^^^
+- Fix `bulk_create` doesn't work correctly with more than 1 update_fields. (#1046)
+
 0.18.1
 ------
 Added
diff --git a/pyproject.toml b/pyproject.toml
index a3bb7a7..5513a30 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "tortoise-orm"
-version = "0.18.1"
+version = "0.18.2"
 description = "Easy async ORM for python, built with relations in mind"
 authors = ["Andrey Bondar <andrey@bondar.ru>", "Nickolas Grigoriadis <nagrigoriadis@gmail.com>", "long2ice <long2ice@gmail.com>"]
 license = "Apache-2.0"
diff --git a/tortoise/__init__.py b/tortoise/__init__.py
index 6923851..3b4e369 100644
--- a/tortoise/__init__.py
+++ b/tortoise/__init__.py
@@ -709,4 +709,4 @@ def run_async(coro: Coroutine) -> None:
         loop.run_until_complete(Tortoise.close_connections())
 
 
-__version__ = "0.18.1"
+__version__ = "0.18.2"
diff --git a/tortoise/queryset.py b/tortoise/queryset.py
index 0195c96..0f02d26 100644
--- a/tortoise/queryset.py
+++ b/tortoise/queryset.py
@@ -1765,17 +1765,18 @@ class BulkCreateQuery(AwaitableQuery):
                     columns_all, has_generated=False, ignore_conflicts=self.ignore_conflicts
                 )
             if self.update_fields:
-                alias = f"new_{self.model.__name__}"
-                self.insert_query_all = (
-                    self.insert_query_all.as_(alias)  # type:ignore
-                    .on_conflict(*self.on_conflict)
-                    .do_update(*self.update_fields)
+                alias = f"new_{self.model._meta.db_table}"
+                self.insert_query_all = self.insert_query_all.as_(alias).on_conflict(  # type:ignore
+                    *self.on_conflict
                 )
-                self.insert_query = (
-                    self.insert_query.as_(alias)  # type:ignore
-                    .on_conflict(*self.on_conflict)
-                    .do_update(*self.update_fields)
+                self.insert_query = self.insert_query.as_(alias).on_conflict(  # type:ignore
+                    *self.on_conflict
                 )
+                for update_field in self.update_fields:
+                    self.insert_query_all = self.insert_query_all.do_update(  # type:ignore
+                        update_field
+                    )
+                    self.insert_query = self.insert_query.do_update(update_field)  # type:ignore
 
     async def _execute(self) -> List[MODEL]:
         for instance_chunk in chunk(self.objects, self.batch_size):


Test patch:
diff --git a/tests/test_basic.py b/tests/test_basic.py
index bcfc772..c6491ba 100644
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -18,4 +18,5 @@ class TestBasic(test.TestCase):
                 {"id": tournament.id, "name": "Updated name"},
                 {"id": tournament.id + 1, "name": "Test 2"},
             ],
+            sorted_key="id",
         )
diff --git a/tests/test_bulk.py b/tests/test_bulk.py
index 78fba04..03c00de 100644
--- a/tests/test_bulk.py
+++ b/tests/test_bulk.py
@@ -11,7 +11,9 @@ class TestBulk(test.TruncationTestCase):
         await UniqueName.bulk_create([UniqueName() for _ in range(1000)])
         all_ = await UniqueName.all().values("id", "name")
         inc = all_[0]["id"]
-        self.assertListSortEqual(all_, [{"id": val + inc, "name": None} for val in range(1000)])
+        self.assertListSortEqual(
+            all_, [{"id": val + inc, "name": None} for val in range(1000)], sorted_key="id"
+        )
 
     async def test_bulk_create_update_fields(self):
         await UniqueName.bulk_create([UniqueName(name="name")])
@@ -23,16 +25,32 @@ class TestBulk(test.TruncationTestCase):
         all_ = await UniqueName.all().values("name", "optional")
         self.assertListSortEqual(all_, [{"name": "name", "optional": "optional"}])
 
+    async def test_bulk_create_more_that_one_update_fields(self):
+        await UniqueName.bulk_create([UniqueName(name="name")])
+        await UniqueName.bulk_create(
+            [UniqueName(name="name", optional="optional", other_optional="other_optional")],
+            update_fields=["optional", "other_optional"],
+            on_conflict=["name"],
+        )
+        all_ = await UniqueName.all().values("name", "optional", "other_optional")
+        self.assertListSortEqual(
+            all_, [{"name": "name", "optional": "optional", "other_optional": "other_optional"}]
+        )
+
     async def test_bulk_create_with_batch_size(self):
         await UniqueName.bulk_create([UniqueName() for _ in range(1000)], batch_size=100)
         all_ = await UniqueName.all().values("id", "name")
         inc = all_[0]["id"]
-        self.assertListSortEqual(all_, [{"id": val + inc, "name": None} for val in range(1000)])
+        self.assertListSortEqual(
+            all_, [{"id": val + inc, "name": None} for val in range(1000)], sorted_key="id"
+        )
 
     async def test_bulk_create_with_specified(self):
         await UniqueName.bulk_create([UniqueName(id=id_) for id_ in range(1000, 2000)])
         all_ = await UniqueName.all().values("id", "name")
-        self.assertListSortEqual(all_, [{"id": id_, "name": None} for id_ in range(1000, 2000)])
+        self.assertListSortEqual(
+            all_, [{"id": id_, "name": None} for id_ in range(1000, 2000)], sorted_key="id"
+        )
 
     async def test_bulk_create_mix_specified(self):
         await UniqueName.bulk_create(
@@ -44,11 +62,11 @@ class TestBulk(test.TruncationTestCase):
         self.assertEqual(len(all_), 2000)
 
         self.assertListSortEqual(
-            all_[:1000], [{"id": id_, "name": None} for id_ in range(10000, 11000)]
+            all_[:1000], [{"id": id_, "name": None} for id_ in range(10000, 11000)], sorted_key="id"
         )
         inc = all_[1000]["id"]
         self.assertListSortEqual(
-            all_[1000:], [{"id": val + inc, "name": None} for val in range(1000)]
+            all_[1000:], [{"id": val + inc, "name": None} for val in range(1000)], sorted_key="id"
         )
 
     async def test_bulk_create_uuidpk(self):
diff --git a/tests/test_group_by.py b/tests/test_group_by.py
index 264e2b4..73db405 100644
--- a/tests/test_group_by.py
+++ b/tests/test_group_by.py
@@ -35,7 +35,9 @@ class TestGroupBy(test.TestCase):
             .values("author__name", "count")
         )
         self.assertListSortEqual(
-            ret, [{"author__name": "author1", "count": 10}, {"author__name": "author2", "count": 5}]
+            ret,
+            [{"author__name": "author1", "count": 10}, {"author__name": "author2", "count": 5}],
+            sorted_key="author__name",
         )
 
     async def test_count_filter_group_by(self):
@@ -69,6 +71,7 @@ class TestGroupBy(test.TestCase):
         self.assertListSortEqual(
             ret,
             [{"author__name": "author1", "sum": 45.0}, {"author__name": "author2", "sum": 10.0}],
+            sorted_key="author__name",
         )
 
     async def test_sum_filter_group_by(self):
@@ -101,7 +104,9 @@ class TestGroupBy(test.TestCase):
             .values("author__name", "avg")
         )
         self.assertListSortEqual(
-            ret, [{"author__name": "author1", "avg": 4.5}, {"author__name": "author2", "avg": 2}]
+            ret,
+            [{"author__name": "author1", "avg": 4.5}, {"author__name": "author2", "avg": 2}],
+            sorted_key="author__name",
         )
 
     async def test_avg_filter_group_by(self):
@@ -223,5 +228,7 @@ class TestGroupBy(test.TestCase):
             .values("upper_name", "count")
         )
         self.assertListSortEqual(
-            ret, [{"upper_name": "AUTHOR1", "count": 10}, {"upper_name": "AUTHOR2", "count": 5}]
+            ret,
+            [{"upper_name": "AUTHOR1", "count": 10}, {"upper_name": "AUTHOR2", "count": 5}],
+            sorted_key="upper_name",
         )
diff --git a/tests/test_values.py b/tests/test_values.py
index f7dde2c..7b7360a 100644
--- a/tests/test_values.py
+++ b/tests/test_values.py
@@ -153,6 +153,7 @@ class TestValues(test.TestCase):
                 {"name": "Championship", "name_slength": 12},
                 {"name": "Super Bowl", "name_slength": 10},
             ],
+            sorted_key="name",
         )
 
     async def test_values_list_annotations_trim(self):
@@ -170,5 +171,7 @@ class TestValues(test.TestCase):
 
         tournaments = await Tournament.annotate(name_trim=Trim("name")).values("name", "name_trim")
         self.assertListSortEqual(
-            tournaments, [{"name": "  x", "name_trim": "x"}, {"name": " y ", "name_trim": "y"}]
+            tournaments,
+            [{"name": "  x", "name_trim": "x"}, {"name": " y ", "name_trim": "y"}],
+            sorted_key="name",
         )
diff --git a/tests/testmodels.py b/tests/testmodels.py
index 6de8c6b..a4a194d 100644
--- a/tests/testmodels.py
+++ b/tests/testmodels.py
@@ -314,6 +314,7 @@ class UniqueName(Model):
     id = fields.IntField(pk=True)
     name = fields.CharField(max_length=20, null=True, unique=True)
     optional = fields.CharField(max_length=20, null=True)
+    other_optional = fields.CharField(max_length=20, null=True)
 
 
 class UniqueTogetherFields(Model):
diff --git a/tortoise/contrib/test/__init__.py b/tortoise/contrib/test/__init__.py
index f62cfaf..4554941 100644
--- a/tortoise/contrib/test/__init__.py
+++ b/tortoise/contrib/test/__init__.py
@@ -195,13 +195,21 @@ class SimpleTestCase(unittest.IsolatedAsyncioTestCase):
         Tortoise._connections = {}
         Tortoise._inited = False
 
-    def assertListSortEqual(self, list1: List[Any], list2: List[Any], msg: Any = ...) -> None:
+    def assertListSortEqual(
+        self, list1: List[Any], list2: List[Any], msg: Any = ..., sorted_key: Optional[str] = None
+    ) -> None:
         if isinstance(list1[0], Model):
             super().assertListEqual(
-                sorted(list1, key=lambda x: x.pk), sorted(list2, key=lambda x: x.pk)
+                sorted(list1, key=lambda x: x.pk), sorted(list2, key=lambda x: x.pk), msg=msg
             )
-        elif isinstance(list1[0], tuple):
-            super().assertListEqual(sorted(list1), sorted(list2))
+        elif isinstance(list1[0], dict) and sorted_key:
+            super().assertListEqual(
+                sorted(list1, key=lambda x: x[sorted_key]),
+                sorted(list2, key=lambda x: x[sorted_key]),
+                msg=msg,
+            )
+        else:
+            super().assertListEqual(sorted(list1), sorted(list2), msg=msg)
 
 
 class IsolatedTestCase(SimpleTestCase):