""" Container classes for methods and attributes to be patched into django """ from enum import Enum # Framework imports from django.db import models # Project imports from django_types.utils import find_fields from patchy import super_patchy from .operations import CreateEnum, RemoveEnum, RenameEnum, AlterEnum, enum_state from .fields import EnumField class MigrationQuestioner: def ask_rename_enum(self, old_enum_key, new_enum_key, enum_set): return self.defaults.get('ask_rename_enum', False) def ask_remove_enum_values(self, db_type, values): return self.defaults.get('ask_remove_enum_values', None) class InteractiveMigrationQuestioner: def ask_rename_enum(self, old_enum_key, new_enum_key, enum_set): return self._boolean_input( 'Did you rename enum {old_key} to {new_key}? [y/N]'.format( old_key=old_enum_key, new_key=new_enum_key), default=False) def ask_remove_enum_values(self, db_type, values): """ How to treat records with deleted enum values. """ # Ordered ensures choices = [ (models.CASCADE, "Cascade - Delete records with removed values"), (models.PROTECT, "Protect - Block migrations if records contain removed values"), (models.SET_NULL, "Set NULL - Set value to NULL"), (models.SET_DEFAULT, "Set default - Set value to field default"), (models.SET, "Set value - Provide a one off default now"), (models.DO_NOTHING, "Do nothing - Consistency must be handled elsewhere"), (None, "Leave it to field definitions")] choice, _ = choices[self._choice_input( "Enum {db_type} has had {values} removed, " "existing records may need to be updated. " "Override update behaviour or do nothing and follow field behaviour.".format( db_type=db_type, values=values), [q for (k, q) in choices]) - 1] if choice == models.SET: return models.SET(self._ask_default()) return choice class BaseDatabaseFeatures: # Assume no database support for enums has_enum = False requires_enum_declaration = False class PostgresDatabaseFeatures: # Django only supports postgres 9.3+, enums added in 8.3, assume supported has_enum = True # Identify that enums must be declared prior to use requires_enum_declaration = True class MysqlDatabaseFeatures: # Supports inline declared enums has_enum = True requires_enum_declaration = False class PostgresDatabaseSchemaEditor: # CREATE TYPE enum_name AS ENUM ('value 1', 'value 2') # https://www.postgresql.org/docs/9.1/static/datatype-enum.html sql_create_enum = 'CREATE TYPE %(enum_type)s AS ENUM (%(values)s)' sql_delete_enum = 'DROP TYPE %(enum_type)s' # ALTER TYPE for schema changes. pg9.1+ only # https://www.postgresql.org/docs/9.1/static/sql-altertype.html sql_alter_enum = 'ALTER TYPE %(enum_type)s ADD VALUE %(value)s %(condition)s' sql_rename_enum = 'ALTER TYPE %(old_type)s RENAME TO %(enum_type)s' # remove_from_enum is not supported by poostgres sql_alter_column_type_using = 'ALTER COLUMN %(column)s TYPE %(type)s USING (%(column)s::text::%(type)s)' class MigrationAutodetector: def detect_enums(self): # Scan to_state new enums in use for info in find_fields(self.to_state, field_type=EnumField): if info.field.type_name not in self.to_state.db_types: self.to_state.add_type(info.field.type_name, enum_state(info.field.type_def, app_label=info.field.type_app_label)) from_enum_types = set(db_type for db_type, e in self.from_state.db_types.items() if issubclass(e, Enum)) to_enum_types = set(db_type for db_type, e in self.to_state.db_types.items() if issubclass(e, Enum)) # Look for renamed enums new_enum_sets = {k: self.to_state.db_types[k].values_set() for k in to_enum_types - from_enum_types} old_enum_sets = {k: self.from_state.db_types[k].values_set() for k in from_enum_types - to_enum_types} for db_type, enum_set in list(new_enum_sets.items()): for rem_db_type, rem_enum_set in old_enum_sets.items(): # Compare only the values if enum_set == rem_enum_set: if self.questioner.ask_rename_enum(db_type, rem_db_type, enum_set): self.add_operation( self.to_state.db_types[db_type].Meta.app_label, RenameEnum(old_type=rem_db_type, new_type=db_type), beginning=True) del old_enum_sets[rem_db_type] del new_enum_sets[db_type] break # Create new enums for db_type, values in new_enum_sets.items(): self.add_operation( self.to_state.db_types[db_type].Meta.app_label, CreateEnum(db_type=db_type, values=list(values)), beginning=True) # Remove old enums for db_type in old_enum_sets: self.add_operation( self.from_state.db_types[db_type].Meta.app_label, RemoveEnum(db_type=db_type), beginning=True) # Does not detect renamed values in enum, that's a remove + add existing_enum_sets = {k: ( self.from_state.db_types[k].values_set(), self.to_state.db_types[k].values_set()) for k in from_enum_types & to_enum_types} for db_type, (old_set, new_set) in existing_enum_sets.items(): if old_set != new_set: paras = {'db_type': db_type} removed = list(old_set - new_set) added = list(new_set - old_set) if removed: paras['remove_values'] = removed paras['on_delete'] = self.questioner.ask_remove_enum_values(db_type, removed) if added: paras['add_values'] = added self.add_operation( self.from_state.db_types[db_type].Meta.app_label, AlterEnum(**paras), beginning=True) # Better to do after model creation and then inject operations at front of list def generate_created_models(self, *args, **kwargs): super_patchy(*args, **kwargs) self.detect_enums()