Loading django/db/backends/__init__.py +12 −0 Original line number Diff line number Diff line Loading @@ -427,6 +427,9 @@ class BaseDatabaseFeatures(object): # Can we issue more than one ALTER COLUMN clause in an ALTER TABLE? supports_combined_alters = False # What's the maximum length for index names? max_index_name_length = 63 def __init__(self, connection): self.connection = connection Loading Loading @@ -1056,6 +1059,15 @@ class BaseDatabaseIntrospection(object): """ raise NotImplementedError def get_constraints(self, cursor, table_name): """ Returns {'cnname': {'columns': set(columns), 'primary_key': bool, 'unique': bool}} Both single- and multi-column constraints are introspected. """ raise NotImplementedError class BaseDatabaseClient(object): """ This class encapsulates all backend-specific methods for opening a Loading django/db/backends/creation.py +2 −1 Original line number Diff line number Diff line Loading @@ -21,7 +21,8 @@ class BaseDatabaseCreation(object): def __init__(self, connection): self.connection = connection def _digest(self, *args): @classmethod def _digest(cls, *args): """ Generates a 32-bit digest of a set of arguments that can be used to shorten identifying names. Loading django/db/backends/postgresql_psycopg2/introspection.py +32 −0 Original line number Diff line number Diff line Loading @@ -88,3 +88,35 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): continue indexes[row[0]] = {'primary_key': row[3], 'unique': row[2]} return indexes def get_constraints(self, cursor, table_name): """ Retrieves any constraints (unique, pk, check) across one or more columns. Returns {'cnname': {'columns': set(columns), 'primary_key': bool, 'unique': bool}} """ constraints = {} # Loop over the constraint tables, collecting things as constraints ifsc_tables = ["constraint_column_usage", "key_column_usage"] for ifsc_table in ifsc_tables: cursor.execute(""" SELECT kc.constraint_name, kc.column_name, c.constraint_type FROM information_schema.%s 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(): # 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"], } # Record the details constraints[constraint]['columns'].add(column) return constraints django/db/backends/schema.py +71 −11 Original line number Diff line number Diff line Loading @@ -4,6 +4,8 @@ import time from django.conf import settings from django.db import transaction from django.db.utils import load_backend from django.db.backends.creation import BaseDatabaseCreation from django.db.backends.util import truncate_name from django.utils.log import getLogger from django.db.models.fields.related import ManyToManyField Loading Loading @@ -294,7 +296,23 @@ class BaseDatabaseSchemaEditor(object): old_field, new_field, )) # First, have they renamed the column? # Has unique been removed? 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: raise ValueError("Found wrong number (%s) of constraints for %s.%s" % ( len(constraint_names), model._meta.db_table, old_field.column, )) self.execute( self.sql_delete_unique % { "table": self.quote_name(model._meta.db_table), "name": constraint_names[0], }, ) # Have they renamed the column? if old_field.column != new_field.column: self.execute(self.sql_rename_column % { "table": self.quote_name(model._meta.db_table), Loading Loading @@ -347,6 +365,7 @@ class BaseDatabaseSchemaEditor(object): }, [], )) if actions: # Combine actions together if we can (e.g. postgres) if self.connection.features.supports_combined_alters: sql, params = tuple(zip(*actions)) Loading @@ -360,3 +379,44 @@ class BaseDatabaseSchemaEditor(object): }, params, ) # Added a unique? if not old_field.unique and new_field.unique: self.execute( self.sql_create_unique % { "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), } ) def _create_index_name(self, model, column_names, suffix=""): "Generates a unique name for an index/unique constraint." # If there is just one column in the index, use a default algorithm from Django if len(column_names) == 1 and not suffix: return truncate_name( '%s_%s' % (model._meta.db_table, BaseDatabaseCreation._digest(column_names[0])), self.connection.ops.max_name_length() ) # Else generate the name for the index by South table_name = model._meta.db_table.replace('"', '').replace('.', '_') index_unique_name = '_%x' % abs(hash((table_name, ','.join(column_names)))) # If the index name is too long, truncate it index_name = ('%s_%s%s%s' % (table_name, column_names[0], index_unique_name, suffix)).replace('"', '').replace('.', '_') if len(index_name) > self.connection.features.max_index_name_length: part = ('_%s%s%s' % (column_names[0], index_unique_name, suffix)) 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): "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) result = [] for name, infodict in constraints.items(): if column_names == infodict['columns']: if unique is not None and infodict['unique'] != unique: continue if primary_key is not None and infodict['primary_key'] != unique: continue result.append(name) return result tests/modeltests/schema/models.py +13 −0 Original line number Diff line number Diff line Loading @@ -12,10 +12,23 @@ class Author(models.Model): managed = False class AuthorWithM2M(models.Model): name = models.CharField(max_length=255) class Meta: managed = False class Book(models.Model): author = models.ForeignKey(Author) title = models.CharField(max_length=100) pub_date = models.DateTimeField() #tags = models.ManyToManyField("Tag", related_name="books") class Meta: managed = False class Tag(models.Model): title = models.CharField(max_length=255) slug = models.SlugField(unique=True) Loading
django/db/backends/__init__.py +12 −0 Original line number Diff line number Diff line Loading @@ -427,6 +427,9 @@ class BaseDatabaseFeatures(object): # Can we issue more than one ALTER COLUMN clause in an ALTER TABLE? supports_combined_alters = False # What's the maximum length for index names? max_index_name_length = 63 def __init__(self, connection): self.connection = connection Loading Loading @@ -1056,6 +1059,15 @@ class BaseDatabaseIntrospection(object): """ raise NotImplementedError def get_constraints(self, cursor, table_name): """ Returns {'cnname': {'columns': set(columns), 'primary_key': bool, 'unique': bool}} Both single- and multi-column constraints are introspected. """ raise NotImplementedError class BaseDatabaseClient(object): """ This class encapsulates all backend-specific methods for opening a Loading
django/db/backends/creation.py +2 −1 Original line number Diff line number Diff line Loading @@ -21,7 +21,8 @@ class BaseDatabaseCreation(object): def __init__(self, connection): self.connection = connection def _digest(self, *args): @classmethod def _digest(cls, *args): """ Generates a 32-bit digest of a set of arguments that can be used to shorten identifying names. Loading
django/db/backends/postgresql_psycopg2/introspection.py +32 −0 Original line number Diff line number Diff line Loading @@ -88,3 +88,35 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): continue indexes[row[0]] = {'primary_key': row[3], 'unique': row[2]} return indexes def get_constraints(self, cursor, table_name): """ Retrieves any constraints (unique, pk, check) across one or more columns. Returns {'cnname': {'columns': set(columns), 'primary_key': bool, 'unique': bool}} """ constraints = {} # Loop over the constraint tables, collecting things as constraints ifsc_tables = ["constraint_column_usage", "key_column_usage"] for ifsc_table in ifsc_tables: cursor.execute(""" SELECT kc.constraint_name, kc.column_name, c.constraint_type FROM information_schema.%s 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(): # 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"], } # Record the details constraints[constraint]['columns'].add(column) return constraints
django/db/backends/schema.py +71 −11 Original line number Diff line number Diff line Loading @@ -4,6 +4,8 @@ import time from django.conf import settings from django.db import transaction from django.db.utils import load_backend from django.db.backends.creation import BaseDatabaseCreation from django.db.backends.util import truncate_name from django.utils.log import getLogger from django.db.models.fields.related import ManyToManyField Loading Loading @@ -294,7 +296,23 @@ class BaseDatabaseSchemaEditor(object): old_field, new_field, )) # First, have they renamed the column? # Has unique been removed? 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: raise ValueError("Found wrong number (%s) of constraints for %s.%s" % ( len(constraint_names), model._meta.db_table, old_field.column, )) self.execute( self.sql_delete_unique % { "table": self.quote_name(model._meta.db_table), "name": constraint_names[0], }, ) # Have they renamed the column? if old_field.column != new_field.column: self.execute(self.sql_rename_column % { "table": self.quote_name(model._meta.db_table), Loading Loading @@ -347,6 +365,7 @@ class BaseDatabaseSchemaEditor(object): }, [], )) if actions: # Combine actions together if we can (e.g. postgres) if self.connection.features.supports_combined_alters: sql, params = tuple(zip(*actions)) Loading @@ -360,3 +379,44 @@ class BaseDatabaseSchemaEditor(object): }, params, ) # Added a unique? if not old_field.unique and new_field.unique: self.execute( self.sql_create_unique % { "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), } ) def _create_index_name(self, model, column_names, suffix=""): "Generates a unique name for an index/unique constraint." # If there is just one column in the index, use a default algorithm from Django if len(column_names) == 1 and not suffix: return truncate_name( '%s_%s' % (model._meta.db_table, BaseDatabaseCreation._digest(column_names[0])), self.connection.ops.max_name_length() ) # Else generate the name for the index by South table_name = model._meta.db_table.replace('"', '').replace('.', '_') index_unique_name = '_%x' % abs(hash((table_name, ','.join(column_names)))) # If the index name is too long, truncate it index_name = ('%s_%s%s%s' % (table_name, column_names[0], index_unique_name, suffix)).replace('"', '').replace('.', '_') if len(index_name) > self.connection.features.max_index_name_length: part = ('_%s%s%s' % (column_names[0], index_unique_name, suffix)) 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): "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) result = [] for name, infodict in constraints.items(): if column_names == infodict['columns']: if unique is not None and infodict['unique'] != unique: continue if primary_key is not None and infodict['primary_key'] != unique: continue result.append(name) return result
tests/modeltests/schema/models.py +13 −0 Original line number Diff line number Diff line Loading @@ -12,10 +12,23 @@ class Author(models.Model): managed = False class AuthorWithM2M(models.Model): name = models.CharField(max_length=255) class Meta: managed = False class Book(models.Model): author = models.ForeignKey(Author) title = models.CharField(max_length=100) pub_date = models.DateTimeField() #tags = models.ManyToManyField("Tag", related_name="books") class Meta: managed = False class Tag(models.Model): title = models.CharField(max_length=255) slug = models.SlugField(unique=True)