Loading django/db/backends/mysql/introspection.py +17 −2 Original line number Diff line number Diff line Loading @@ -104,8 +104,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): def get_constraints(self, cursor, table_name): """ Retrieves any constraints (unique, pk, fk, check) across one or more columns. Returns {'cnname': {'columns': set(columns), 'primary_key': bool, 'unique': bool, 'foreign_key': None|(tbl, col)}} Retrieves any constraints or keys (unique, pk, fk, check, index) across one or more columns. """ constraints = {} # Get the actual constraint names and columns Loading @@ -124,6 +123,8 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): 'columns': set(), 'primary_key': False, 'unique': False, 'index': False, 'check': False, 'foreign_key': (ref_table, ref_column) if ref_column else None, } constraints[constraint]['columns'].add(column) Loading @@ -142,5 +143,19 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): constraints[constraint]['unique'] = True elif kind.lower() == "unique": constraints[constraint]['unique'] = True # Now add in the indexes cursor.execute("SHOW INDEX FROM %s" % self.connection.ops.quote_name(table_name)) for table, non_unique, index, colseq, column in [x[:5] for x in cursor.fetchall()]: if index not in constraints: constraints[index] = { 'columns': set(), 'primary_key': False, 'unique': False, 'index': True, 'check': False, 'foreign_key': None, } constraints[index]['index'] = True constraints[index]['columns'].add(column) # Return return constraints django/db/backends/mysql/schema.py +1 −1 Original line number Diff line number Diff line Loading @@ -15,7 +15,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): sql_create_fk = "ALTER TABLE %(table)s ADD CONSTRAINT %(name)s FOREIGN KEY (%(column)s) REFERENCES %(to_table)s (%(to_column)s)" sql_delete_fk = "ALTER TABLE %(table)s DROP FOREIGN KEY %(name)s" sql_delete_index = "DROP INDEX %(name)s ON %(table_name)s" sql_delete_index = "DROP INDEX %(name)s ON %(table)s" sql_delete_pk = "ALTER TABLE %(table)s DROP PRIMARY KEY" Loading django/db/backends/postgresql_psycopg2/introspection.py +81 −26 Original line number Diff line number Diff line Loading @@ -91,32 +91,87 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): def get_constraints(self, cursor, table_name): """ Retrieves any constraints (unique, pk, fk, check) across one or more columns. Returns {'cnname': {'columns': set(columns), 'primary_key': bool, 'unique': bool}} Retrieves any constraints or keys (unique, pk, fk, check, index) across one or more columns. """ constraints = {} # Loop over the constraint tables, collecting things as constraints ifsc_tables = ["constraint_column_usage", "key_column_usage"] for ifsc_table in ifsc_tables: # Loop over the key table, collecting things as constraints # This will get PKs, FKs, and uniques, but not CHECK cursor.execute(""" SELECT kc.constraint_name, kc.column_name, c.constraint_type FROM information_schema.%s AS kc SELECT kc.constraint_name, kc.column_name, c.constraint_type, array(SELECT table_name::text || '.' || column_name::text FROM information_schema.constraint_column_usage WHERE constraint_name = kc.constraint_name) FROM information_schema.key_column_usage AS kc JOIN information_schema.table_constraints AS c ON kc.table_schema = c.table_schema AND kc.table_name = c.table_name AND kc.constraint_name = c.constraint_name WHERE kc.table_schema = %%s AND kc.table_name = %%s """ % ifsc_table, ["public", table_name]) for constraint, column, kind in cursor.fetchall(): kc.table_schema = %s AND kc.table_name = %s """, ["public", table_name]) for constraint, column, kind, used_cols in cursor.fetchall(): # If we're the first column, make the record if constraint not in constraints: constraints[constraint] = { "columns": set(), "primary_key": kind.lower() == "primary key", "unique": kind.lower() in ["primary key", "unique"], "foreign_key": set([tuple(x.split(".", 1)) for x in used_cols]) if kind.lower() == "foreign key" else None, "check": False, "index": False, } # Record the details constraints[constraint]['columns'].add(column) # Now get CHECK constraint columns cursor.execute(""" SELECT kc.constraint_name, kc.column_name FROM information_schema.constraint_column_usage AS kc JOIN information_schema.table_constraints AS c ON kc.table_schema = c.table_schema AND kc.table_name = c.table_name AND kc.constraint_name = c.constraint_name WHERE c.constraint_type = 'CHECK' AND kc.table_schema = %s AND kc.table_name = %s """, ["public", table_name]) for constraint, column, kind in cursor.fetchall(): # If we're the first column, make the record if constraint not in constraints: constraints[constraint] = { "columns": set(), "primary_key": False, "unique": False, "foreign_key": False, "check": True, "index": False, } # Record the details constraints[constraint]['columns'].add(column) # Now get indexes cursor.execute(""" SELECT c2.relname, attr.attname, idx.indkey, idx.indisunique, idx.indisprimary FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index idx, pg_catalog.pg_attribute attr WHERE c.oid = idx.indrelid AND idx.indexrelid = c2.oid AND attr.attrelid = c.oid AND attr.attnum = idx.indkey[0] AND c.relname = %s """, [table_name]) for index, column, coli, unique, primary in cursor.fetchall(): # If we're the first column, make the record if index not in constraints: constraints[index] = { "columns": set(), "primary_key": False, "unique": False, "foreign_key": False, "check": False, "index": True, } # Record the details constraints[index]['columns'].add(column) return constraints django/db/backends/schema.py +41 −11 Original line number Diff line number Diff line Loading @@ -54,7 +54,7 @@ class BaseDatabaseSchemaEditor(object): sql_create_fk = "ALTER TABLE %(table)s ADD CONSTRAINT %(name)s FOREIGN KEY (%(column)s) REFERENCES %(to_table)s (%(to_column)s) DEFERRABLE INITIALLY DEFERRED" sql_delete_fk = "ALTER TABLE %(table)s DROP CONSTRAINT %(name)s" sql_create_index = "CREATE %(unique)s INDEX %(name)s ON %(table)s (%(columns)s)%(extra)s;" sql_create_index = "CREATE INDEX %(name)s ON %(table)s (%(columns)s)%(extra)s;" sql_delete_index = "DROP INDEX %(name)s" sql_create_pk = "ALTER TABLE %(table)s ADD CONSTRAINT %(constraint)s PRIMARY KEY (%(columns)s)" Loading Loading @@ -181,7 +181,6 @@ class BaseDatabaseSchemaEditor(object): if field.db_index: self.deferred_sql.append( self.sql_create_index % { "unique": "", "name": self._create_index_name(model, [field.column], suffix=""), "table": self.quote_name(model._meta.db_table), "columns": self.quote_name(field.column), Loading Loading @@ -350,12 +349,13 @@ class BaseDatabaseSchemaEditor(object): } self.execute(sql) def alter_field(self, model, old_field, new_field): def alter_field(self, model, old_field, new_field, strict=False): """ Allows a field's type, uniqueness, nullability, default, column, constraints etc. to be modified. Requires a copy of the old field as well so we can only perform changes that are required. If strict is true, raises errors if the old column does not match old_field precisely. """ # Ensure this field is even column-based old_type = old_field.db_type(connection=self.connection) Loading @@ -372,18 +372,36 @@ class BaseDatabaseSchemaEditor(object): if old_field.unique and not new_field.unique: # Find the unique constraint for this field constraint_names = self._constraint_names(model, [old_field.column], unique=True) if len(constraint_names) != 1: if strict and len(constraint_names) != 1: raise ValueError("Found wrong number (%s) of constraints for %s.%s" % ( len(constraint_names), model._meta.db_table, old_field.column, )) for constraint_name in constraint_names: self.execute( self.sql_delete_unique % { "table": self.quote_name(model._meta.db_table), "name": constraint_names[0], "name": constraint_name, }, ) # Removed an index? if old_field.db_index and not new_field.db_index and not old_field.unique and not new_field.unique: # Find the index for this field index_names = self._constraint_names(model, [old_field.column], index=True) if strict and len(index_names) != 1: raise ValueError("Found wrong number (%s) of indexes for %s.%s" % ( len(index_names), model._meta.db_table, old_field.column, )) for index_name in index_names: self.execute( self.sql_delete_index % { "table": self.quote_name(model._meta.db_table), "name": index_name, } ) # Have they renamed the column? if old_field.column != new_field.column: self.execute(self.sql_rename_column % { Loading Loading @@ -463,6 +481,16 @@ class BaseDatabaseSchemaEditor(object): "columns": self.quote_name(new_field.column), } ) # Added an index? if not old_field.db_index and new_field.db_index and not old_field.unique and not new_field.unique: self.execute( self.sql_create_index % { "table": self.quote_name(model._meta.db_table), "name": self._create_index_name(model, [new_field.column], suffix="_uniq"), "columns": self.quote_name(new_field.column), "extra": "", } ) def _type_for_alter(self, field): """ Loading Loading @@ -490,7 +518,7 @@ class BaseDatabaseSchemaEditor(object): index_name = '%s%s' % (table_name[:(self.connection.features.max_index_name_length - len(part))], part) return index_name def _constraint_names(self, model, column_names, unique=None, primary_key=None): def _constraint_names(self, model, column_names, unique=None, primary_key=None, index=None): "Returns all constraint names matching the columns and conditions" column_names = set(column_names) constraints = self.connection.introspection.get_constraints(self.connection.cursor(), model._meta.db_table) Loading @@ -501,5 +529,7 @@ class BaseDatabaseSchemaEditor(object): continue if primary_key is not None and infodict['primary_key'] != unique: continue if index is not None and infodict['index'] != index: continue result.append(name) return result tests/modeltests/schema/models.py +1 −1 Original line number Diff line number Diff line Loading @@ -21,7 +21,7 @@ class AuthorWithM2M(models.Model): class Book(models.Model): author = models.ForeignKey(Author) title = models.CharField(max_length=100) title = models.CharField(max_length=100, db_index=True) pub_date = models.DateTimeField() #tags = models.ManyToManyField("Tag", related_name="books") Loading Loading
django/db/backends/mysql/introspection.py +17 −2 Original line number Diff line number Diff line Loading @@ -104,8 +104,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): def get_constraints(self, cursor, table_name): """ Retrieves any constraints (unique, pk, fk, check) across one or more columns. Returns {'cnname': {'columns': set(columns), 'primary_key': bool, 'unique': bool, 'foreign_key': None|(tbl, col)}} Retrieves any constraints or keys (unique, pk, fk, check, index) across one or more columns. """ constraints = {} # Get the actual constraint names and columns Loading @@ -124,6 +123,8 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): 'columns': set(), 'primary_key': False, 'unique': False, 'index': False, 'check': False, 'foreign_key': (ref_table, ref_column) if ref_column else None, } constraints[constraint]['columns'].add(column) Loading @@ -142,5 +143,19 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): constraints[constraint]['unique'] = True elif kind.lower() == "unique": constraints[constraint]['unique'] = True # Now add in the indexes cursor.execute("SHOW INDEX FROM %s" % self.connection.ops.quote_name(table_name)) for table, non_unique, index, colseq, column in [x[:5] for x in cursor.fetchall()]: if index not in constraints: constraints[index] = { 'columns': set(), 'primary_key': False, 'unique': False, 'index': True, 'check': False, 'foreign_key': None, } constraints[index]['index'] = True constraints[index]['columns'].add(column) # Return return constraints
django/db/backends/mysql/schema.py +1 −1 Original line number Diff line number Diff line Loading @@ -15,7 +15,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): sql_create_fk = "ALTER TABLE %(table)s ADD CONSTRAINT %(name)s FOREIGN KEY (%(column)s) REFERENCES %(to_table)s (%(to_column)s)" sql_delete_fk = "ALTER TABLE %(table)s DROP FOREIGN KEY %(name)s" sql_delete_index = "DROP INDEX %(name)s ON %(table_name)s" sql_delete_index = "DROP INDEX %(name)s ON %(table)s" sql_delete_pk = "ALTER TABLE %(table)s DROP PRIMARY KEY" Loading
django/db/backends/postgresql_psycopg2/introspection.py +81 −26 Original line number Diff line number Diff line Loading @@ -91,32 +91,87 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): def get_constraints(self, cursor, table_name): """ Retrieves any constraints (unique, pk, fk, check) across one or more columns. Returns {'cnname': {'columns': set(columns), 'primary_key': bool, 'unique': bool}} Retrieves any constraints or keys (unique, pk, fk, check, index) across one or more columns. """ constraints = {} # Loop over the constraint tables, collecting things as constraints ifsc_tables = ["constraint_column_usage", "key_column_usage"] for ifsc_table in ifsc_tables: # Loop over the key table, collecting things as constraints # This will get PKs, FKs, and uniques, but not CHECK cursor.execute(""" SELECT kc.constraint_name, kc.column_name, c.constraint_type FROM information_schema.%s AS kc SELECT kc.constraint_name, kc.column_name, c.constraint_type, array(SELECT table_name::text || '.' || column_name::text FROM information_schema.constraint_column_usage WHERE constraint_name = kc.constraint_name) FROM information_schema.key_column_usage AS kc JOIN information_schema.table_constraints AS c ON kc.table_schema = c.table_schema AND kc.table_name = c.table_name AND kc.constraint_name = c.constraint_name WHERE kc.table_schema = %%s AND kc.table_name = %%s """ % ifsc_table, ["public", table_name]) for constraint, column, kind in cursor.fetchall(): kc.table_schema = %s AND kc.table_name = %s """, ["public", table_name]) for constraint, column, kind, used_cols in cursor.fetchall(): # If we're the first column, make the record if constraint not in constraints: constraints[constraint] = { "columns": set(), "primary_key": kind.lower() == "primary key", "unique": kind.lower() in ["primary key", "unique"], "foreign_key": set([tuple(x.split(".", 1)) for x in used_cols]) if kind.lower() == "foreign key" else None, "check": False, "index": False, } # Record the details constraints[constraint]['columns'].add(column) # Now get CHECK constraint columns cursor.execute(""" SELECT kc.constraint_name, kc.column_name FROM information_schema.constraint_column_usage AS kc JOIN information_schema.table_constraints AS c ON kc.table_schema = c.table_schema AND kc.table_name = c.table_name AND kc.constraint_name = c.constraint_name WHERE c.constraint_type = 'CHECK' AND kc.table_schema = %s AND kc.table_name = %s """, ["public", table_name]) for constraint, column, kind in cursor.fetchall(): # If we're the first column, make the record if constraint not in constraints: constraints[constraint] = { "columns": set(), "primary_key": False, "unique": False, "foreign_key": False, "check": True, "index": False, } # Record the details constraints[constraint]['columns'].add(column) # Now get indexes cursor.execute(""" SELECT c2.relname, attr.attname, idx.indkey, idx.indisunique, idx.indisprimary FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index idx, pg_catalog.pg_attribute attr WHERE c.oid = idx.indrelid AND idx.indexrelid = c2.oid AND attr.attrelid = c.oid AND attr.attnum = idx.indkey[0] AND c.relname = %s """, [table_name]) for index, column, coli, unique, primary in cursor.fetchall(): # If we're the first column, make the record if index not in constraints: constraints[index] = { "columns": set(), "primary_key": False, "unique": False, "foreign_key": False, "check": False, "index": True, } # Record the details constraints[index]['columns'].add(column) return constraints
django/db/backends/schema.py +41 −11 Original line number Diff line number Diff line Loading @@ -54,7 +54,7 @@ class BaseDatabaseSchemaEditor(object): sql_create_fk = "ALTER TABLE %(table)s ADD CONSTRAINT %(name)s FOREIGN KEY (%(column)s) REFERENCES %(to_table)s (%(to_column)s) DEFERRABLE INITIALLY DEFERRED" sql_delete_fk = "ALTER TABLE %(table)s DROP CONSTRAINT %(name)s" sql_create_index = "CREATE %(unique)s INDEX %(name)s ON %(table)s (%(columns)s)%(extra)s;" sql_create_index = "CREATE INDEX %(name)s ON %(table)s (%(columns)s)%(extra)s;" sql_delete_index = "DROP INDEX %(name)s" sql_create_pk = "ALTER TABLE %(table)s ADD CONSTRAINT %(constraint)s PRIMARY KEY (%(columns)s)" Loading Loading @@ -181,7 +181,6 @@ class BaseDatabaseSchemaEditor(object): if field.db_index: self.deferred_sql.append( self.sql_create_index % { "unique": "", "name": self._create_index_name(model, [field.column], suffix=""), "table": self.quote_name(model._meta.db_table), "columns": self.quote_name(field.column), Loading Loading @@ -350,12 +349,13 @@ class BaseDatabaseSchemaEditor(object): } self.execute(sql) def alter_field(self, model, old_field, new_field): def alter_field(self, model, old_field, new_field, strict=False): """ Allows a field's type, uniqueness, nullability, default, column, constraints etc. to be modified. Requires a copy of the old field as well so we can only perform changes that are required. If strict is true, raises errors if the old column does not match old_field precisely. """ # Ensure this field is even column-based old_type = old_field.db_type(connection=self.connection) Loading @@ -372,18 +372,36 @@ class BaseDatabaseSchemaEditor(object): if old_field.unique and not new_field.unique: # Find the unique constraint for this field constraint_names = self._constraint_names(model, [old_field.column], unique=True) if len(constraint_names) != 1: if strict and len(constraint_names) != 1: raise ValueError("Found wrong number (%s) of constraints for %s.%s" % ( len(constraint_names), model._meta.db_table, old_field.column, )) for constraint_name in constraint_names: self.execute( self.sql_delete_unique % { "table": self.quote_name(model._meta.db_table), "name": constraint_names[0], "name": constraint_name, }, ) # Removed an index? if old_field.db_index and not new_field.db_index and not old_field.unique and not new_field.unique: # Find the index for this field index_names = self._constraint_names(model, [old_field.column], index=True) if strict and len(index_names) != 1: raise ValueError("Found wrong number (%s) of indexes for %s.%s" % ( len(index_names), model._meta.db_table, old_field.column, )) for index_name in index_names: self.execute( self.sql_delete_index % { "table": self.quote_name(model._meta.db_table), "name": index_name, } ) # Have they renamed the column? if old_field.column != new_field.column: self.execute(self.sql_rename_column % { Loading Loading @@ -463,6 +481,16 @@ class BaseDatabaseSchemaEditor(object): "columns": self.quote_name(new_field.column), } ) # Added an index? if not old_field.db_index and new_field.db_index and not old_field.unique and not new_field.unique: self.execute( self.sql_create_index % { "table": self.quote_name(model._meta.db_table), "name": self._create_index_name(model, [new_field.column], suffix="_uniq"), "columns": self.quote_name(new_field.column), "extra": "", } ) def _type_for_alter(self, field): """ Loading Loading @@ -490,7 +518,7 @@ class BaseDatabaseSchemaEditor(object): index_name = '%s%s' % (table_name[:(self.connection.features.max_index_name_length - len(part))], part) return index_name def _constraint_names(self, model, column_names, unique=None, primary_key=None): def _constraint_names(self, model, column_names, unique=None, primary_key=None, index=None): "Returns all constraint names matching the columns and conditions" column_names = set(column_names) constraints = self.connection.introspection.get_constraints(self.connection.cursor(), model._meta.db_table) Loading @@ -501,5 +529,7 @@ class BaseDatabaseSchemaEditor(object): continue if primary_key is not None and infodict['primary_key'] != unique: continue if index is not None and infodict['index'] != index: continue result.append(name) return result
tests/modeltests/schema/models.py +1 −1 Original line number Diff line number Diff line Loading @@ -21,7 +21,7 @@ class AuthorWithM2M(models.Model): class Book(models.Model): author = models.ForeignKey(Author) title = models.CharField(max_length=100) title = models.CharField(max_length=100, db_index=True) pub_date = models.DateTimeField() #tags = models.ManyToManyField("Tag", related_name="books") Loading