diff --git a/django/core/management/commands/flush.py b/django/core/management/commands/flush.py index c56fc1e1b0..7054187e45 100644 --- a/django/core/management/commands/flush.py +++ b/django/core/management/commands/flush.py @@ -32,8 +32,9 @@ class Command(NoArgsCommand): connection = connections[db] verbosity = int(options.get('verbosity')) interactive = options.get('interactive') - # 'reset_sequences' is a stealth option + # 'reset_sequences' and 'allow_cascade' are stealth options reset_sequences = options.get('reset_sequences', True) + allow_cascade = options.get('allow_cascade', False) self.style = no_style() @@ -45,7 +46,9 @@ class Command(NoArgsCommand): except ImportError: pass - sql_list = sql_flush(self.style, connection, only_django=True, reset_sequences=reset_sequences) + sql_list = sql_flush(self.style, connection, only_django=True, + reset_sequences=reset_sequences, + allow_cascade=allow_cascade) if interactive: confirm = input("""You have requested a flush of the database. diff --git a/django/core/management/sql.py b/django/core/management/sql.py index 42ccafa2c5..b58d89f60a 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -102,7 +102,7 @@ def sql_delete(app, style, connection): return output[::-1] # Reverse it, to deal with table dependencies. -def sql_flush(style, connection, only_django=False, reset_sequences=True): +def sql_flush(style, connection, only_django=False, reset_sequences=True, allow_cascade=False): """ Returns a list of the SQL statements used to flush the database. @@ -114,7 +114,7 @@ def sql_flush(style, connection, only_django=False, reset_sequences=True): else: tables = connection.introspection.table_names() seqs = connection.introspection.sequence_list() if reset_sequences else () - statements = connection.ops.sql_flush(style, tables, seqs) + statements = connection.ops.sql_flush(style, tables, seqs, allow_cascade) return statements diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 160c63d9d9..fa3cc5ac02 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -390,7 +390,7 @@ class BaseDatabaseWrapper(object): def disable_constraint_checking(self): """ Backends can implement as needed to temporarily disable foreign key - constraint checking. Should return True if the constraints were + constraint checking. Should return True if the constraints were disabled and will need to be reenabled. """ return False @@ -947,7 +947,7 @@ class BaseDatabaseOperations(object): """ return '' - def sql_flush(self, style, tables, sequences): + def sql_flush(self, style, tables, sequences, allow_cascade=False): """ Returns a list of SQL statements required to remove all data from the given database tables (without actually removing the tables @@ -958,6 +958,10 @@ class BaseDatabaseOperations(object): The `style` argument is a Style object as returned by either color_style() or no_style() in django.core.management.color. + + 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. """ raise NotImplementedError() diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index dbddad7312..ea687715e4 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -298,14 +298,17 @@ class DatabaseOperations(BaseDatabaseOperations): def random_function_sql(self): return 'RAND()' - def sql_flush(self, style, tables, sequences): + def sql_flush(self, style, tables, sequences, allow_cascade=False): # NB: The generated SQL below is specific to MySQL # 'TRUNCATE x;', 'TRUNCATE y;', 'TRUNCATE z;'... style SQL statements # to clear all tables of all data if tables: sql = ['SET FOREIGN_KEY_CHECKS = 0;'] for table in tables: - sql.append('%s %s;' % (style.SQL_KEYWORD('TRUNCATE'), style.SQL_FIELD(self.quote_name(table)))) + sql.append('%s %s;' % ( + style.SQL_KEYWORD('TRUNCATE'), + style.SQL_FIELD(self.quote_name(table)), + )) sql.append('SET FOREIGN_KEY_CHECKS = 1;') sql.extend(self.sequence_reset_by_name_sql(style, sequences)) return sql diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 0afc5f23e8..3f39a15aa7 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -339,17 +339,17 @@ WHEN (new.%(col_name)s IS NULL) def savepoint_rollback_sql(self, sid): return convert_unicode("ROLLBACK TO SAVEPOINT " + self.quote_name(sid)) - def sql_flush(self, style, tables, sequences): + def sql_flush(self, style, tables, sequences, allow_cascade=False): # Return a list of 'TRUNCATE x;', 'TRUNCATE y;', # 'TRUNCATE z;'... style SQL statements if tables: # Oracle does support TRUNCATE, but it seems to get us into # FK referential trouble, whereas DELETE FROM table works. - sql = ['%s %s %s;' % \ - (style.SQL_KEYWORD('DELETE'), - style.SQL_KEYWORD('FROM'), - style.SQL_FIELD(self.quote_name(table))) - for table in tables] + sql = ['%s %s %s;' % ( + style.SQL_KEYWORD('DELETE'), + style.SQL_KEYWORD('FROM'), + style.SQL_FIELD(self.quote_name(table)) + ) for table in 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)) diff --git a/django/db/backends/postgresql_psycopg2/operations.py b/django/db/backends/postgresql_psycopg2/operations.py index f06eec5a1d..f96757da8b 100644 --- a/django/db/backends/postgresql_psycopg2/operations.py +++ b/django/db/backends/postgresql_psycopg2/operations.py @@ -101,15 +101,24 @@ class DatabaseOperations(BaseDatabaseOperations): def set_time_zone_sql(self): return "SET TIME ZONE %s" - def sql_flush(self, style, tables, sequences): + def sql_flush(self, style, tables, sequences, allow_cascade=False): if tables: # Perform a single SQL 'TRUNCATE x, y, z...;' statement. It allows # us to truncate tables referenced by a foreign key in any other # table. - sql = ['%s %s;' % \ - (style.SQL_KEYWORD('TRUNCATE'), - style.SQL_FIELD(', '.join([self.quote_name(table) for table in tables])) - )] + tables_sql = ', '.join( + style.SQL_FIELD(self.quote_name(table)) for table in tables) + 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 else: diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index ead325a33b..a2d925ac3f 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -209,15 +209,15 @@ class DatabaseOperations(BaseDatabaseOperations): def no_limit_value(self): return -1 - def sql_flush(self, style, tables, sequences): + def sql_flush(self, style, tables, sequences, allow_cascade=False): # NB: The generated SQL below is specific to SQLite # Note: The DELETE FROM... SQL generated below works for SQLite databases # because constraints don't exist - sql = ['%s %s %s;' % \ - (style.SQL_KEYWORD('DELETE'), - style.SQL_KEYWORD('FROM'), - style.SQL_FIELD(self.quote_name(table)) - ) for table in tables] + sql = ['%s %s %s;' % ( + style.SQL_KEYWORD('DELETE'), + style.SQL_KEYWORD('FROM'), + style.SQL_FIELD(self.quote_name(table)) + ) for table in tables] # Note: No requirement for reset of auto-incremented indices (cf. other # sql_flush() implementations). Just return SQL at this point return sql