diff --git a/django/db/backends/sqlite3/features.py b/django/db/backends/sqlite3/features.py index ef42f70595..1e995a87c0 100644 --- a/django/db/backends/sqlite3/features.py +++ b/django/db/backends/sqlite3/features.py @@ -1,4 +1,5 @@ import operator +import sqlite3 from django.db import transaction from django.db.backends.base.features import BaseDatabaseFeatures @@ -13,7 +14,6 @@ class DatabaseFeatures(BaseDatabaseFeatures): test_db_allows_multiple_connections = False supports_unspecified_pk = True supports_timezones = False - max_query_params = 999 supports_transactions = True atomic_transactions = False can_rollback_ddl = True @@ -140,6 +140,16 @@ class DatabaseFeatures(BaseDatabaseFeatures): "SmallAutoField": "AutoField", } + @property + def max_query_params(self): + """ + SQLite has a variable limit per query. The limit can be changed using + the SQLITE_MAX_VARIABLE_NUMBER compile-time option (which defaults to + 999 in versions < 3.32.0 or 32766 in newer versions) or lowered per + connection at run-time with setlimit(SQLITE_LIMIT_VARIABLE_NUMBER, N). + """ + return self.connection.connection.getlimit(sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER) + @cached_property def supports_json_field(self): with self.connection.cursor() as cursor: diff --git a/django/db/backends/sqlite3/operations.py b/django/db/backends/sqlite3/operations.py index 0ab853f766..340bd28ef8 100644 --- a/django/db/backends/sqlite3/operations.py +++ b/django/db/backends/sqlite3/operations.py @@ -30,8 +30,8 @@ class DatabaseOperations(BaseDatabaseOperations): def bulk_batch_size(self, fields, objs): """ - SQLite has a compile-time default (SQLITE_LIMIT_VARIABLE_NUMBER) of - 999 variables per query. + SQLite has a variable limit defined by SQLITE_LIMIT_VARIABLE_NUMBER + (reflected in max_query_params). If there's only a single field to insert, the limit is 500 (SQLITE_MAX_COMPOUND_SELECT). diff --git a/tests/backends/sqlite/test_features.py b/tests/backends/sqlite/test_features.py index 5bc891f5ee..95e060fbcc 100644 --- a/tests/backends/sqlite/test_features.py +++ b/tests/backends/sqlite/test_features.py @@ -1,3 +1,4 @@ +import sqlite3 from unittest import mock, skipUnless from django.db import OperationalError, connection @@ -17,3 +18,14 @@ class FeaturesTests(TestCase): ): with self.assertRaisesMessage(OperationalError, msg): connection.features.supports_json_field + + def test_max_query_params_respects_variable_limit(self): + limit_name = sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER + current_limit = connection.features.max_query_params + new_limit = min(42, current_limit) + try: + connection.connection.setlimit(limit_name, new_limit) + self.assertEqual(connection.features.max_query_params, new_limit) + finally: + connection.connection.setlimit(limit_name, current_limit) + self.assertEqual(connection.features.max_query_params, current_limit) diff --git a/tests/backends/sqlite/test_operations.py b/tests/backends/sqlite/test_operations.py index 10cbffdf80..ab6efaa771 100644 --- a/tests/backends/sqlite/test_operations.py +++ b/tests/backends/sqlite/test_operations.py @@ -1,3 +1,4 @@ +import sqlite3 import unittest from django.core.management.color import no_style @@ -108,3 +109,32 @@ class SQLiteOperationsTests(TestCase): ), connection.features.max_query_params // 3, ) + + def test_bulk_batch_size_respects_variable_limit(self): + first_name_field = Person._meta.get_field("first_name") + last_name_field = Person._meta.get_field("last_name") + limit_name = sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER + current_limit = connection.features.max_query_params + self.assertEqual( + connection.ops.bulk_batch_size( + [first_name_field, last_name_field], [Person()] + ), + current_limit // 2, + ) + new_limit = min(42, current_limit) + try: + connection.connection.setlimit(limit_name, new_limit) + self.assertEqual( + connection.ops.bulk_batch_size( + [first_name_field, last_name_field], [Person()] + ), + new_limit // 2, + ) + finally: + connection.connection.setlimit(limit_name, current_limit) + self.assertEqual( + connection.ops.bulk_batch_size( + [first_name_field, last_name_field], [Person()] + ), + current_limit // 2, + )