Skip to content

Commit b310f17

Browse files
committed
Issue #7 -- Use temp table for large number of parameters inside in clause
1 parent 605725e commit b310f17

File tree

5 files changed

+81
-1
lines changed

5 files changed

+81
-1
lines changed

mssql/functions.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from django.db.models.functions import Cast
77
from django.db.models.functions.math import ATan2, Log, Ln, Mod, Round
88
from django.db.models.expressions import Case, Exists, OrderBy, When
9-
from django.db.models.lookups import Lookup
9+
from django.db.models.lookups import Lookup, In
1010

1111
DJANGO3 = VERSION[0] >= 3
1212

@@ -91,11 +91,29 @@ def sqlserver_orderby(self, compiler, connection):
9191
return self.as_sql(compiler, connection, template=template)
9292

9393

94+
def split_parameter_list_as_sql(self, compiler, connection):
95+
# Insert In clause parameters 1000 at a time into a temp table.
96+
lhs, _ = self.process_lhs(compiler, connection)
97+
_, rhs_params = self.batch_process_rhs(compiler, connection)
98+
99+
with connection.cursor() as cursor:
100+
cursor.execute("IF OBJECT_ID('tempdb.dbo.#Temp_params', 'U') IS NOT NULL DROP TABLE #Temp_params; ")
101+
cursor.execute("CREATE TABLE #Temp_params (params int)")
102+
for offset in range(0, len(rhs_params), 1000):
103+
sqls_params = rhs_params[offset: offset + 1000]
104+
sqls_params = ", ".join("({})".format(item) for item in sqls_params)
105+
cursor.execute("INSERT INTO #Temp_params VALUES %s" % sqls_params)
106+
107+
in_clause = lhs + ' IN ' + '(SELECT params from #Temp_params)'
108+
109+
return in_clause, ()
110+
94111
ATan2.as_microsoft = sqlserver_atan2
95112
Log.as_microsoft = sqlserver_log
96113
Ln.as_microsoft = sqlserver_ln
97114
Mod.as_microsoft = sqlserver_mod
98115
Round.as_microsoft = sqlserver_round
116+
In.split_parameter_list_as_sql = split_parameter_list_as_sql
99117

100118
if DJANGO3:
101119
Lookup.as_microsoft = sqlserver_lookup

mssql/operations.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ class DatabaseOperations(BaseDatabaseOperations):
2020

2121
cast_char_field_without_max_length = 'nvarchar(max)'
2222

23+
def max_in_list_size(self):
24+
# The driver might add a few parameters
25+
# chose a reasonable number less than 2100 limit
26+
return 2048
27+
2328
def _convert_field_to_tz(self, field_name, tzname):
2429
if settings.USE_TZ and not tzname == 'UTC':
2530
offset = self._get_utcoffset(tzname)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Generated by Django 3.1.7 on 2021-03-08 18:28
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('testapp', '0009_test_drop_table_with_foreign_key_reference_part2'),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name='Topping',
15+
fields=[
16+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17+
('name', models.CharField(max_length=30)),
18+
],
19+
),
20+
migrations.CreateModel(
21+
name='Pizza',
22+
fields=[
23+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
24+
('name', models.CharField(max_length=50)),
25+
('toppings', models.ManyToManyField(to='testapp.Topping')),
26+
],
27+
),
28+
]

testapp/models.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,17 @@ class TestRemoveOneToOneFieldModel(models.Model):
7474
# thats already is removed.
7575
# b = models.OneToOneField('self', on_delete=models.SET_NULL, null=True)
7676
a = models.CharField(max_length=50)
77+
78+
79+
class Topping(models.Model):
80+
name = models.CharField(max_length=30)
81+
82+
class Pizza(models.Model):
83+
name = models.CharField(max_length=50)
84+
toppings = models.ManyToManyField(Topping)
85+
86+
def __str__(self):
87+
return "%s (%s)" % (
88+
self.name,
89+
", ".join(topping.name for topping in self.toppings.all()),
90+
)

testapp/tests/test_lookups.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT license.
3+
4+
from django.test import TestCase
5+
from ..models import Pizza, Topping
6+
7+
class TestLookups(TestCase):
8+
def test_large_number_of_params(self):
9+
iterations = 3000
10+
for i in range(iterations):
11+
Pizza.objects.create(name="Pizza" + str(i))
12+
Topping.objects.create(name="Topping" + str(i))
13+
prefetch_result = Pizza.objects.prefetch_related('toppings')
14+
15+
self.assertEqual(len(prefetch_result), iterations)

0 commit comments

Comments
 (0)