def set_name_with_model(self, model): """ Generate a unique name for the index. The name is divided into 3 parts - table name (12 chars), field name (8 chars) and unique hash + suffix (10 chars). Each part is made to fit its size by truncating the excess length. """ _, table_name = split_identifier(model._meta.db_table) column_names = [ model._meta.get_field(field_name).column for field_name, order in self.fields_orders ] column_names_with_order = [ (("-%s" if order else "%s") % column_name) for column_name, (field_name, order) in zip(column_names, self.fields_orders) ] # The length of the parts of the name is based on the default max # length of 30 characters. hash_data = [table_name] + column_names_with_order + [self.suffix] self.name = "%s_%s_%s" % ( table_name[:11], column_names[0][:7], "%s_%s" % (names_digest(*hash_data, length=6), self.suffix), ) assert len(self.name) <= self.max_name_length, ( "Index too long for multiple database support. Is self.suffix " "longer than 3 characters?") self.check_name()
def set_name_with_model(self, model): """ Generate a unique name for the index. The name is divided into 3 parts - table name (12 chars), field name (8 chars) and unique hash + suffix (10 chars). Each part is made to fit its size by truncating the excess length. """ _, table_name = split_identifier(model._meta.db_table) column_names = [model._meta.get_field(field_name).column for field_name, order in self.fields_orders] column_names_with_order = [ (('-%s' if order else '%s') % column_name) for column_name, (field_name, order) in zip(column_names, self.fields_orders) ] # The length of the parts of the name is based on the default max # length of 30 characters. hash_data = [table_name] + column_names_with_order + [self.suffix] self.name = '%s_%s_%s' % ( table_name[:11], column_names[0][:7], '%s_%s' % (names_digest(*hash_data, length=6), self.suffix), ) assert len(self.name) <= self.max_name_length, ( 'Index too long for multiple database support. Is self.suffix ' 'longer than 3 characters?' ) self.check_name()
def set_name_with_model(self, model): """ Generate a unique name for the index. The name is divided into 3 parts - table name (12 chars), field name (8 chars) and unique hash + suffix (10 chars). Each part is made to fit its size by truncating the excess length. """ _, table_name = split_identifier(model._meta.db_table) column_names = [model._meta.get_field(field_name).column for field_name, order in self.fields_orders] column_names_with_order = [ (('-%s' if order else '%s') % column_name) for column_name, (field_name, order) in zip(column_names, self.fields_orders) ] # The length of the parts of the name is based on the default max # length of 30 characters. hash_data = [table_name] + column_names_with_order + [self.suffix] self.name = '%s_%s_%s' % ( table_name[:11], column_names[0][:7], '%s_%s' % (names_digest(*hash_data, length=6), self.suffix), ) if len(self.name) > self.max_name_length: raise ValueError( 'Index too long for multiple database support. Is self.suffix ' 'longer than 3 characters?' ) if self.name[0] == '_' or self.name[0].isdigit(): self.name = 'D%s' % self.name[1:]
def _create_index_name(self, table_name, column_names, suffix=""): """ Generate a unique name for an index/unique constraint. The name is divided into 3 parts: the table name, the column names, and a unique digest and suffix. """ _, table_name = split_identifier(table_name) hash_suffix_part = '%s%s' % (names_digest(table_name, *column_names, length=8), suffix) max_length = self.connection.ops.max_name_length() or 200 # If everything fits into max_length, use that name. index_name = '%s_%s_%s' % (table_name, '_'.join(column_names), hash_suffix_part) if len(index_name) <= max_length: return index_name # Shorten a long suffix. if len(hash_suffix_part) > max_length / 3: hash_suffix_part = hash_suffix_part[:max_length // 3] other_length = (max_length - len(hash_suffix_part)) // 2 - 1 index_name = '%s_%s_%s' % ( table_name[:other_length], '_'.join(column_names)[:other_length], hash_suffix_part, ) # Prepend D if needed to prevent the name from starting with an # underscore or a number (not permitted on Oracle). if index_name[0] == "_" or index_name[0].isdigit(): index_name = "D%s" % index_name[:-1] return index_name
def digest(connection, *args): """Return a digest hash for a set of arguments. This is mostly used as part of the index/constraint name generation processes. It offers compatibility with a range of Django versions. Args: connection (object): The database connection. *args (tuple): The positional arguments used to build the digest hash out of. Returns: str: The resulting digest hash. """ if names_digest is not None: # Django >= 2.2 return names_digest(args[0], *args[1:], length=8) elif (BaseDatabaseSchemaEditor and hasattr(BaseDatabaseSchemaEditor, '_digest')): # Django >= 1.8, < 2.2 # # Note that _digest() is a classmethod that is common across all # database backends. We don't need to worry about using a # per-instance version. If that changes, we'll need to create a # SchemaEditor. return BaseDatabaseSchemaEditor._digest(*args) else: # Django < 1.8 return connection.creation._digest(*args)
def _sql_indexes_for_field(self, model, field): """ Return the CREATE INDEX SQL statements for a single model field :param model: :param field: :return: """ def qn(name): if name.startswith('"') and name.endswith('"'): return name # Quoting once is enough. return '"%s"' % name max_name_length = 63 if field.db_index and not field.unique: i_name = names_digest(model._meta.db_table, field.column, length=8) return [ "CREATE INDEX %s ON %s(%s)" % ( qn(truncate_name(i_name, max_name_length)), qn(model._meta.db_table), qn(field.column), ) ] return []
import django # noqa: F401 HAS_DJANGO = True except ImportError: HAS_DJANGO = False else: from django.db.backends.base import schema from django.db.models import indexes from django.db.backends.utils import names_digest from django.db import connection if HAS_DJANGO is True: # See upgrade blocker note in requirements/README.md try: names_digest('foo', 'bar', 'baz', length=8) except ValueError: def names_digest(*args, length): """ Generate a 32-bit digest of a set of arguments that can be used to shorten identifying names. Support for use in FIPS environments. """ h = hashlib.md5(usedforsecurity=False) for arg in args: h.update(arg.encode()) return h.hexdigest()[:length] schema.names_digest = names_digest indexes.names_digest = names_digest
def create_jsonb_index_sql(editor, model, field): options = field.db_index_options json_path_op = "->" sqls = {} for option in options: paths = option.get("path", "") if not paths: path = "%(columns)s" else: path_elements = paths.split("__") path = "(%(columns)s{}{})".format(json_path_op, json_path_op.join(["'%s'" % element for element in path_elements])) ops_cls = " jsonb_path_ops" if option.get("only_contains") else "" sql = editor.create_jsonb_index_sql.format(path=path, ops_cls=ops_cls) sqls[get_jsonb_index_name(editor, model, field, option)] = editor._create_index_sql(model, [field], sql=sql, suffix=(names_digest(paths, length=8) if paths else "")) return sqls
def get_jsonb_index_name(editor, model, field, index_info): return editor._create_index_name(model.__name__, [field.name], suffix=names_digest(index_info.get("path") or "", length=8))
def set_name_with_model(self, model): self.fields_orders = [(model._meta.pk.name, '')] _, table_name = split_identifier(model._meta.db_table) digest = names_digest(table_name, *self.fields, length=6) self.name = f"{table_name[:19]}_{digest}_{self.suffix}"