Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d0e2148
Implement can_return_rows_from_bulk_insert feature, returning ids or …
jean-frenette-optel Feb 18, 2022
28411f7
Since mssql-django supports Django 2.2, we also need the pre-Django 3…
marcperrinoptel Feb 18, 2022
7581cd1
My alternative changes on SQLInsertCompiler.as_sql.
marcperrinoptel Feb 18, 2022
fddb1fa
Don't try to use the OUTPUT clause when inserting without fields
marcperrinoptel Feb 18, 2022
eff9939
Actually we don't really have to offer the feature for Django 2.2, so…
marcperrinoptel Feb 18, 2022
a790db9
Tentative fix: when there are returning fields, but no fields (which …
marcperrinoptel Feb 18, 2022
cdc7560
Using MERGE INTO to support Bulk Insertion of multiple rows into a ta…
jean-frenette-optel Feb 23, 2022
c437f2c
Merge branch 'bulk-insert-ids-mpe' into dev
jean-frenette-optel Feb 23, 2022
82ee55b
Add a link to a reference web page.
jean-frenette-optel Feb 23, 2022
464b4e3
Attempt to make Django 2.2 tests pass
jean-frenette-optel Feb 23, 2022
41899b4
Get back to a lighter diff of as_sql function vs. original
marcperrinoptel Feb 23, 2022
8f39ec3
Merge pull request #1 from optelgroup/tweak-bulk-insert
jean-frenette-optel Feb 23, 2022
0706960
Use a query to generate sequence of numbers instead of using the mast…
jean-frenette-optel Feb 23, 2022
a7a4d2c
Update mssql/operations.py
jean-frenette-optel Feb 24, 2022
78f496a
Merge branch 'microsoft:dev' into dev
jean-frenette-optel Mar 3, 2022
2a8bb82
Merge branch 'microsoft:dev' into dev
jean-frenette-optel Mar 7, 2022
06af269
Simplification & refactoring
marcperrinoptel Mar 7, 2022
d8fd8b5
Merge pull request #4 from optelgroup/tweak-bulk-again
marcperrinoptel Mar 8, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 41 additions & 6 deletions mssql/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,16 @@ def get_returned_fields(self):
return self.returning_fields
return self.return_id

def can_return_columns_from_insert(self):
if django.VERSION >= (3, 0, 0):
return self.connection.features.can_return_columns_from_insert
return self.connection.features.can_return_id_from_insert

def can_return_rows_from_bulk_insert(self):
if django.VERSION >= (3, 0, 0):
return self.connection.features.can_return_rows_from_bulk_insert
return self.connection.features.can_return_ids_from_bulk_insert

def fix_auto(self, sql, opts, fields, qn):
if opts.auto_field is not None:
# db_column is None if not explicitly specified by model field
Expand All @@ -447,9 +457,9 @@ def as_sql(self):
qn = self.connection.ops.quote_name
opts = self.query.get_meta()
result = ['INSERT INTO %s' % qn(opts.db_table)]
fields = self.query.fields or [opts.pk]

if self.query.fields:
fields = self.query.fields
result.append('(%s)' % ', '.join(qn(f.column) for f in fields))
values_format = 'VALUES (%s)'
value_rows = [
Expand All @@ -470,11 +480,36 @@ def as_sql(self):

placeholder_rows, param_rows = self.assemble_as_sql(fields, value_rows)

if self.get_returned_fields() and self.connection.features.can_return_id_from_insert:
result.insert(0, 'SET NOCOUNT ON')
result.append((values_format + ';') % ', '.join(placeholder_rows[0]))
params = [param_rows[0]]
result.append('SELECT CAST(SCOPE_IDENTITY() AS bigint)')
if self.get_returned_fields() and self.can_return_columns_from_insert():
if self.can_return_rows_from_bulk_insert():
if not(self.query.fields):
# There isn't really a single statement to bulk multiple DEFAULT VALUES insertions,
Copy link
Contributor

@marcperrinoptel marcperrinoptel Feb 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little bit of context to explain why we resort to a weird workaround to achieve bulk insertion in the case where there are no self.query.fields (but there are "returning fields"):

  • INSERT...DEFAULT VALUES won't help us: yes it is compatible with OUPUT INSERTED, but it is limited to inserting a single row. It's fine to continue to "fake bulk" like this when no returning fields are desired, but it won't help us here (as there is only one cursor.fetchall() to get all the returning values - unless we start overriding more stuff e.g. execute_sql, which isn't appealing at all)
  • specifying a column list for the INSERT despite the absence of self.query.fields and using value DEFAULT for every column (x every row) isn't an option either, because that column list would basically be: all fields minus auto fields; but having only auto fields is certainly possible (cf. the infamous bulk_create.tests.BulkCreateTests.test_empty_model), which leaves that list empty, and that does not work

# so we have to use a workaround:
# https://dba.stackexchange.com/questions/254771/insert-multiple-rows-into-a-table-with-only-an-identity-column
result = ['MERGE INTO %s' % qn(opts.db_table)]
result.append("""
USING (SELECT TOP %s * FROM master..spt_values) T
ON 1 = 0
WHEN NOT MATCHED THEN
INSERT DEFAULT VALUES""" % len(self.query.objs))
r_sql, self.returning_params = self.connection.ops.return_insert_columns(self.get_returned_fields())
if r_sql:
result.append(r_sql)
sql = " ".join(result) + ";"
return [(sql, None)]
# Regular bulk insert
params = []
r_sql, self.returning_params = self.connection.ops.return_insert_columns(self.get_returned_fields())
if r_sql:
result.append(r_sql)
params += [self.returning_params]
params += param_rows
result.append(self.connection.ops.bulk_insert_sql(fields, placeholder_rows))
else:
result.insert(0, 'SET NOCOUNT ON')
result.append((values_format + ';') % ', '.join(placeholder_rows[0]))
params = [param_rows[0]]
result.append('SELECT CAST(SCOPE_IDENTITY() AS bigint)')
sql = [(" ".join(result), tuple(chain.from_iterable(params)))]
else:
if can_bulk:
Expand Down
1 change: 1 addition & 0 deletions mssql/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
can_introspect_small_integer_field = True
can_return_columns_from_insert = True
can_return_id_from_insert = True
can_return_rows_from_bulk_insert = True
can_rollback_ddl = True
can_use_chunked_reads = False
for_update_after_from = True
Expand Down
18 changes: 18 additions & 0 deletions mssql/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,24 @@ def datetime_trunc_sql(self, lookup_type, field_name, tzname):
sql = "CONVERT(datetime2, CONVERT(varchar, %s, 20))" % field_name
return sql

def fetch_returned_insert_rows(self, cursor):
"""
Given a cursor object that has just performed an INSERT...
statement into a table, return the list of returned data.
"""
return cursor.fetchall()

def return_insert_columns(self, fields):
if not fields:
return '', ()
columns = [
'%s.%s' % (
'INSERTED',
self.quote_name(field.column),
) for field in fields
]
return 'OUTPUT %s' % ', '.join(columns), ()

def for_update_sql(self, nowait=False, skip_locked=False, of=()):
if skip_locked:
return 'WITH (ROWLOCK, UPDLOCK, READPAST)'
Expand Down