diff --git a/django/core/management/sql.py b/django/core/management/sql.py index 0d54f9b220e..27113dbbbeb 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -13,8 +13,12 @@ def sql_flush(style, connection, only_django=False, reset_sequences=True, allow_ tables = connection.introspection.django_table_names(only_existing=True, include_views=False) else: tables = connection.introspection.table_names(include_views=False) - seqs = connection.introspection.sequence_list() if reset_sequences else () - return connection.ops.sql_flush(style, tables, seqs, allow_cascade) + return connection.ops.sql_flush( + style, + tables, + reset_sequences=reset_sequences, + allow_cascade=allow_cascade, + ) def emit_pre_migrate_signal(verbosity, interactive, db, **kwargs): diff --git a/django/db/backends/base/operations.py b/django/db/backends/base/operations.py index 6e1187fd37e..70ac07ae095 100644 --- a/django/db/backends/base/operations.py +++ b/django/db/backends/base/operations.py @@ -382,16 +382,18 @@ class BaseDatabaseOperations: """ return '' - def sql_flush(self, style, tables, sequences, allow_cascade=False): + def sql_flush(self, style, tables, *, reset_sequences=False, allow_cascade=False): """ Return a list of SQL statements required to remove all data from the given database tables (without actually removing the tables - themselves) and the SQL statements required to reset the sequences - passed in `sequences`. + themselves). The `style` argument is a Style object as returned by either color_style() or no_style() in django.core.management.color. + If `reset_sequences` is True, the list includes SQL statements required + to reset the sequences. + The `allow_cascade` argument determines whether truncation may cascade to tables with foreign keys pointing the tables being truncated. PostgreSQL requires a cascade even if these tables are empty. diff --git a/django/db/backends/mysql/operations.py b/django/db/backends/mysql/operations.py index 9d69ba11523..d01e3bef6b4 100644 --- a/django/db/backends/mysql/operations.py +++ b/django/db/backends/mysql/operations.py @@ -193,29 +193,30 @@ class DatabaseOperations(BaseDatabaseOperations): ] return 'RETURNING %s' % ', '.join(columns), () - def sql_flush(self, style, tables, sequences, allow_cascade=False): + def sql_flush(self, style, tables, *, reset_sequences=False, allow_cascade=False): if not tables: return [] + sql = ['SET FOREIGN_KEY_CHECKS = 0;'] - tables = set(tables) - with_sequences = set(s['table'] for s in sequences) - # It's faster to TRUNCATE tables that require a sequence reset since - # ALTER TABLE AUTO_INCREMENT is slower than TRUNCATE. - sql.extend( - '%s %s;' % ( - style.SQL_KEYWORD('TRUNCATE'), - style.SQL_FIELD(self.quote_name(table_name)), - ) for table_name in tables.intersection(with_sequences) - ) - # Otherwise issue a simple DELETE since it's faster than TRUNCATE - # and preserves sequences. - sql.extend( - '%s %s %s;' % ( - style.SQL_KEYWORD('DELETE'), - style.SQL_KEYWORD('FROM'), - style.SQL_FIELD(self.quote_name(table_name)), - ) for table_name in tables.difference(with_sequences) - ) + if reset_sequences: + # It's faster to TRUNCATE tables that require a sequence reset + # since ALTER TABLE AUTO_INCREMENT is slower than TRUNCATE. + sql.extend( + '%s %s;' % ( + style.SQL_KEYWORD('TRUNCATE'), + style.SQL_FIELD(self.quote_name(table_name)), + ) for table_name in tables + ) + else: + # Otherwise issue a simple DELETE since it's faster than TRUNCATE + # and preserves sequences. + sql.extend( + '%s %s %s;' % ( + style.SQL_KEYWORD('DELETE'), + style.SQL_KEYWORD('FROM'), + style.SQL_FIELD(self.quote_name(table_name)), + ) for table_name in tables + ) sql.append('SET FOREIGN_KEY_CHECKS = 1;') return sql diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py index 0dc77aefc9e..6f4121425f8 100644 --- a/django/db/backends/oracle/operations.py +++ b/django/db/backends/oracle/operations.py @@ -404,7 +404,7 @@ END; # Django's test suite. return lru_cache(maxsize=512)(self.__foreign_key_constraints) - def sql_flush(self, style, tables, sequences, allow_cascade=False): + def sql_flush(self, style, tables, *, reset_sequences=False, allow_cascade=False): if not tables: return [] @@ -446,9 +446,15 @@ END; style.SQL_FIELD(self.quote_name(constraint)), ) for table, constraint in constraints ] - # Since we've just deleted all the rows, running our sequence ALTER - # code will reset the sequence to 0. - sql.extend(self.sequence_reset_by_name_sql(style, sequences)) + if reset_sequences: + sequences = [ + sequence + for sequence in self.connection.introspection.sequence_list() + if sequence['table'].upper() in truncated_tables + ] + # Since we've just deleted all the rows, running our sequence ALTER + # code will reset the sequence to 0. + sql.extend(self.sequence_reset_by_name_sql(style, sequences)) return sql def sequence_reset_by_name_sql(self, style, sequences): diff --git a/django/db/backends/postgresql/operations.py b/django/db/backends/postgresql/operations.py index dd422af1af7..70880d41796 100644 --- a/django/db/backends/postgresql/operations.py +++ b/django/db/backends/postgresql/operations.py @@ -117,28 +117,21 @@ class DatabaseOperations(BaseDatabaseOperations): def set_time_zone_sql(self): return "SET TIME ZONE %s" - def sql_flush(self, style, tables, sequences, allow_cascade=False): + def sql_flush(self, style, tables, *, reset_sequences=False, allow_cascade=False): if not tables: return [] # Perform a single SQL 'TRUNCATE x, y, z...;' statement. It allows us # to truncate tables referenced by a foreign key in any other table. - tables_sql = ', '.join( - style.SQL_FIELD(self.quote_name(table)) for table in tables - ) + sql_parts = [ + style.SQL_KEYWORD('TRUNCATE'), + ', '.join(style.SQL_FIELD(self.quote_name(table)) for table in tables), + ] + if reset_sequences: + sql_parts.append(style.SQL_KEYWORD('RESTART IDENTITY')) if allow_cascade: - sql = ['%s %s %s;' % ( - style.SQL_KEYWORD('TRUNCATE'), - tables_sql, - style.SQL_KEYWORD('CASCADE'), - )] - else: - sql = ['%s %s;' % ( - style.SQL_KEYWORD('TRUNCATE'), - tables_sql, - )] - sql.extend(self.sequence_reset_by_name_sql(style, sequences)) - return sql + sql_parts.append(style.SQL_KEYWORD('CASCADE')) + return ['%s;' % ' '.join(sql_parts)] def sequence_reset_by_name_sql(self, style, sequences): # 'ALTER SEQUENCE sequence_name RESTART WITH 1;'... style SQL statements diff --git a/django/db/backends/sqlite3/operations.py b/django/db/backends/sqlite3/operations.py index fcc2a06d7a7..3f55332c99c 100644 --- a/django/db/backends/sqlite3/operations.py +++ b/django/db/backends/sqlite3/operations.py @@ -196,7 +196,7 @@ class DatabaseOperations(BaseDatabaseOperations): # Django's test suite. return lru_cache(maxsize=512)(self.__references_graph) - def sql_flush(self, style, tables, sequences, allow_cascade=False): + def sql_flush(self, style, tables, *, reset_sequences=False, allow_cascade=False): if tables and allow_cascade: # Simulate TRUNCATE CASCADE by recursively collecting the tables # referencing the tables to be flushed. diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt index 55f539d5344..3ec3afcf3ac 100644 --- a/docs/releases/3.1.txt +++ b/docs/releases/3.1.txt @@ -519,6 +519,13 @@ backends. * ``DatabaseClient.runshell()`` now requires an additional ``parameters`` argument as a list of extra arguments to pass on to the command-line client. +* The ``sequences`` positional argument of ``DatabaseOperations.sql_flush()`` + is replaced by the boolean keyword-only argument ``reset_sequences``. If + ``True``, the sequences of the truncated tables will be reset. + +* The ``allow_cascade`` argument of ``DatabaseOperations.sql_flush()`` is now a + keyword-only argument. + Dropped support for MariaDB 10.1 -------------------------------- diff --git a/tests/backends/base/test_operations.py b/tests/backends/base/test_operations.py index 5dff48d44e1..0485fe84651 100644 --- a/tests/backends/base/test_operations.py +++ b/tests/backends/base/test_operations.py @@ -43,7 +43,7 @@ class SimpleDatabaseOperationTests(SimpleTestCase): def test_sql_flush(self): msg = 'subclasses of BaseDatabaseOperations must provide a sql_flush() method' with self.assertRaisesMessage(NotImplementedError, msg): - self.ops.sql_flush(None, None, None) + self.ops.sql_flush(None, None) def test_pk_default_value(self): self.assertEqual(self.ops.pk_default_value(), 'DEFAULT') @@ -154,7 +154,7 @@ class SqlFlushTests(TransactionTestCase): available_apps = ['backends'] def test_sql_flush_no_tables(self): - self.assertEqual(connection.ops.sql_flush(no_style(), [], []), []) + self.assertEqual(connection.ops.sql_flush(no_style(), []), []) def test_execute_sql_flush_statements(self): with transaction.atomic(): @@ -169,12 +169,7 @@ class SqlFlushTests(TransactionTestCase): sql_list = connection.ops.sql_flush( no_style(), [Author._meta.db_table, Book._meta.db_table], - [ - { - 'table': Author._meta.db_table, - 'column': Author._meta.pk.db_column, - }, - ], + reset_sequences=True, allow_cascade=True, ) connection.ops.execute_sql_flush(connection.alias, sql_list) @@ -185,3 +180,5 @@ class SqlFlushTests(TransactionTestCase): if connection.features.supports_sequence_reset: author = Author.objects.create(name='F. Scott Fitzgerald') self.assertEqual(author.pk, 1) + book = Book.objects.create(author=author) + self.assertEqual(book.pk, 1) diff --git a/tests/backends/mysql/test_operations.py b/tests/backends/mysql/test_operations.py index f1d9342a7b2..a98e8963b74 100644 --- a/tests/backends/mysql/test_operations.py +++ b/tests/backends/mysql/test_operations.py @@ -4,7 +4,7 @@ from django.core.management.color import no_style from django.db import connection from django.test import SimpleTestCase -from ..models import Person, Square, Tag +from ..models import Person, Tag @unittest.skipUnless(connection.vendor == 'mysql', 'MySQL tests.') @@ -13,50 +13,35 @@ class MySQLOperationsTests(SimpleTestCase): # allow_cascade doesn't change statements on MySQL. for allow_cascade in [False, True]: with self.subTest(allow_cascade=allow_cascade): - statements = connection.ops.sql_flush( - no_style(), - [Person._meta.db_table, Tag._meta.db_table], - [], - allow_cascade=allow_cascade, - ) - self.assertEqual(statements[0], 'SET FOREIGN_KEY_CHECKS = 0;') - # The tables are processed in an unordered set. self.assertEqual( - sorted(statements[1:-1]), + connection.ops.sql_flush( + no_style(), + [Person._meta.db_table, Tag._meta.db_table], + allow_cascade=allow_cascade, + ), [ + 'SET FOREIGN_KEY_CHECKS = 0;', 'DELETE FROM `backends_person`;', 'DELETE FROM `backends_tag`;', + 'SET FOREIGN_KEY_CHECKS = 1;', ], ) - self.assertEqual(statements[-1], 'SET FOREIGN_KEY_CHECKS = 1;') def test_sql_flush_sequences(self): # allow_cascade doesn't change statements on MySQL. for allow_cascade in [False, True]: with self.subTest(allow_cascade=allow_cascade): - statements = connection.ops.sql_flush( - no_style(), - [Person._meta.db_table, Square._meta.db_table, Tag._meta.db_table], - [ - { - 'table': Person._meta.db_table, - 'column': Person._meta.pk.db_column, - }, - { - 'table': Tag._meta.db_table, - 'column': Tag._meta.pk.db_column, - }, - ], - allow_cascade=allow_cascade, - ) - self.assertEqual(statements[0], 'SET FOREIGN_KEY_CHECKS = 0;') - # The tables are processed in an unordered set. self.assertEqual( - sorted(statements[1:-1]), + connection.ops.sql_flush( + no_style(), + [Person._meta.db_table, Tag._meta.db_table], + reset_sequences=True, + allow_cascade=allow_cascade, + ), [ - 'DELETE FROM `backends_square`;', + 'SET FOREIGN_KEY_CHECKS = 0;', 'TRUNCATE `backends_person`;', 'TRUNCATE `backends_tag`;', + 'SET FOREIGN_KEY_CHECKS = 1;', ], ) - self.assertEqual(statements[-1], 'SET FOREIGN_KEY_CHECKS = 1;') diff --git a/tests/backends/oracle/test_operations.py b/tests/backends/oracle/test_operations.py index 97760ecbfed..7722744ecc5 100644 --- a/tests/backends/oracle/test_operations.py +++ b/tests/backends/oracle/test_operations.py @@ -31,7 +31,6 @@ class OperationsTests(unittest.TestCase): statements = connection.ops.sql_flush( no_style(), [Person._meta.db_table, Tag._meta.db_table], - [], ) # The tables and constraints are processed in an unordered set. self.assertEqual( @@ -56,7 +55,6 @@ class OperationsTests(unittest.TestCase): statements = connection.ops.sql_flush( no_style(), [Person._meta.db_table, Tag._meta.db_table], - [], allow_cascade=True, ) # The tables and constraints are processed in an unordered set. @@ -83,16 +81,7 @@ class OperationsTests(unittest.TestCase): statements = connection.ops.sql_flush( no_style(), [Person._meta.db_table, Tag._meta.db_table], - [ - { - 'table': Person._meta.db_table, - 'column': Person._meta.pk.db_column, - }, - { - 'table': Tag._meta.db_table, - 'column': Tag._meta.pk.db_column, - }, - ], + reset_sequences=True, ) # The tables and constraints are processed in an unordered set. self.assertEqual( @@ -121,16 +110,7 @@ class OperationsTests(unittest.TestCase): statements = connection.ops.sql_flush( no_style(), [Person._meta.db_table, Tag._meta.db_table], - [ - { - 'table': Person._meta.db_table, - 'column': Person._meta.pk.db_column, - }, - { - 'table': Tag._meta.db_table, - 'column': Tag._meta.pk.db_column, - }, - ], + reset_sequences=True, allow_cascade=True, ) # The tables and constraints are processed in an unordered set. @@ -153,6 +133,7 @@ class OperationsTests(unittest.TestCase): '"BACKENDS__PERSON_ID_1DD5E829_F";', ) # Sequences. - self.assertEqual(len(statements[5:]), 2) + self.assertEqual(len(statements[5:]), 3) self.assertIn('BACKENDS_PERSON_SQ', statements[5]) - self.assertIn('BACKENDS_TAG_SQ', statements[6]) + self.assertIn('BACKENDS_VERYLONGMODELN7BE2_SQ', statements[6]) + self.assertIn('BACKENDS_TAG_SQ', statements[7]) diff --git a/tests/backends/postgresql/test_operations.py b/tests/backends/postgresql/test_operations.py index b073f688f45..821bb29cee2 100644 --- a/tests/backends/postgresql/test_operations.py +++ b/tests/backends/postgresql/test_operations.py @@ -14,7 +14,6 @@ class PostgreSQLOperationsTests(SimpleTestCase): connection.ops.sql_flush( no_style(), [Person._meta.db_table, Tag._meta.db_table], - [], ), ['TRUNCATE "backends_person", "backends_tag";'], ) @@ -24,61 +23,28 @@ class PostgreSQLOperationsTests(SimpleTestCase): connection.ops.sql_flush( no_style(), [Person._meta.db_table, Tag._meta.db_table], - [], allow_cascade=True, ), ['TRUNCATE "backends_person", "backends_tag" CASCADE;'], ) def test_sql_flush_sequences(self): - sequence_reset_sql = ( - "SELECT setval(pg_get_serial_sequence('%s','id'), 1, false);" - ) self.assertEqual( connection.ops.sql_flush( no_style(), [Person._meta.db_table, Tag._meta.db_table], - [ - { - 'table': Person._meta.db_table, - 'column': Person._meta.pk.db_column, - }, - { - 'table': Tag._meta.db_table, - 'column': Tag._meta.pk.db_column, - }, - ], + reset_sequences=True, ), - [ - 'TRUNCATE "backends_person", "backends_tag";', - sequence_reset_sql % '"backends_person"', - sequence_reset_sql % '"backends_tag"', - ], + ['TRUNCATE "backends_person", "backends_tag" RESTART IDENTITY;'], ) def test_sql_flush_sequences_allow_cascade(self): - sequence_reset_sql = ( - "SELECT setval(pg_get_serial_sequence('%s','id'), 1, false);" - ) self.assertEqual( connection.ops.sql_flush( no_style(), [Person._meta.db_table, Tag._meta.db_table], - [ - { - 'table': Person._meta.db_table, - 'column': Person._meta.pk.db_column, - }, - { - 'table': Tag._meta.db_table, - 'column': Tag._meta.pk.db_column, - }, - ], + reset_sequences=True, allow_cascade=True, ), - [ - 'TRUNCATE "backends_person", "backends_tag" CASCADE;', - sequence_reset_sql % '"backends_person"', - sequence_reset_sql % '"backends_tag"', - ], + ['TRUNCATE "backends_person", "backends_tag" RESTART IDENTITY CASCADE;'], ) diff --git a/tests/backends/sqlite/test_operations.py b/tests/backends/sqlite/test_operations.py index 34c4d823dad..0ee70061f8e 100644 --- a/tests/backends/sqlite/test_operations.py +++ b/tests/backends/sqlite/test_operations.py @@ -14,7 +14,6 @@ class SQLiteOperationsTests(TestCase): connection.ops.sql_flush( no_style(), [Person._meta.db_table, Tag._meta.db_table], - [], ), [ 'DELETE FROM "backends_person";', @@ -26,7 +25,6 @@ class SQLiteOperationsTests(TestCase): statements = connection.ops.sql_flush( no_style(), [Person._meta.db_table, Tag._meta.db_table], - [], allow_cascade=True, ) self.assertEqual( @@ -47,16 +45,7 @@ class SQLiteOperationsTests(TestCase): connection.ops.sql_flush( no_style(), [Person._meta.db_table, Tag._meta.db_table], - [ - { - 'table': Person._meta.db_table, - 'column': Person._meta.pk.db_column, - }, - { - 'table': Tag._meta.db_table, - 'column': Tag._meta.pk.db_column, - }, - ], + reset_sequences=True, ), [ 'DELETE FROM "backends_person";', @@ -69,16 +58,7 @@ class SQLiteOperationsTests(TestCase): statements = connection.ops.sql_flush( no_style(), [Person._meta.db_table, Tag._meta.db_table], - [ - { - 'table': Person._meta.db_table, - 'column': Person._meta.pk.db_column, - }, - { - 'table': Tag._meta.db_table, - 'column': Tag._meta.pk.db_column, - }, - ], + reset_sequences=True, allow_cascade=True, ) self.assertEqual( diff --git a/tests/backends/tests.py b/tests/backends/tests.py index f20d3db5f1d..2cbfa2f5a2f 100644 --- a/tests/backends/tests.py +++ b/tests/backends/tests.py @@ -161,13 +161,7 @@ class LongNameTest(TransactionTestCase): VLM._meta.db_table, VLM_m2m._meta.db_table, ] - sequences = [ - { - 'column': VLM._meta.pk.column, - 'table': VLM._meta.db_table - }, - ] - sql_list = connection.ops.sql_flush(no_style(), tables, sequences) + sql_list = connection.ops.sql_flush(no_style(), tables, reset_sequences=True) with connection.cursor() as cursor: for statement in sql_list: cursor.execute(statement)