# Copyright (c) 2018 Alexander Todorov # Licensed under the GPL 2.0: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html # For details: https://github.com/PyCQA/pylint-django/blob/master/LICENSE """ Various DB performance suggestions. Disabled by default! Enable with pylint --load-plugins=pylint_django.checkers.db_performance """ import astroid from pylint import interfaces from pylint import checkers from pylint.checkers import utils from pylint_django.__pkginfo__ import BASE_ID from pylint_django import compat def _is_addfield_with_default(call): if not isinstance(call.func, astroid.Attribute): return False if not call.func.attrname == 'AddField': return False for keyword in call.keywords: # looking for AddField(..., field=XXX(..., default=Y, ...), ...) if keyword.arg == 'field' and isinstance(keyword.value, astroid.Call): # loop over XXX's keywords # NOTE: not checking if XXX is an actual field type because there could # be many types we're not aware of. Also the migration will probably break # if XXX doesn't instantiate a field object! for field_keyword in keyword.value.keywords: if field_keyword.arg == 'default': return True return False def _is_migrations_module(node): if not isinstance(node, astroid.Module): return False return 'migrations' in node.path[0] and not node.path[0].endswith('__init__.py') class NewDbFieldWithDefaultChecker(checkers.BaseChecker): """ Looks for migrations which add new model fields and these fields have a default value. According to Django docs this may have performance penalties especially on large tables: https://docs.djangoproject.com/en/2.0/topics/migrations/#postgresql The prefered way is to add a new DB column with null=True because it will be created instantly and then possibly populate the table with the desired default values. """ __implements__ = (interfaces.IAstroidChecker,) # configuration section name name = 'new-db-field-with-default' msgs = {'W%s98' % BASE_ID: ("%s AddField with default value", 'new-db-field-with-default', 'Used when Pylint detects migrations adding new ' 'fields with a default value.')} _migration_modules = [] _possible_offences = {} def visit_module(self, node): if _is_migrations_module(node): self._migration_modules.append(node) def visit_call(self, node): try: module = node.frame().parent except: # noqa: E722, pylint: disable=bare-except return if not _is_migrations_module(module): return if _is_addfield_with_default(node): if module not in self._possible_offences: self._possible_offences[module] = [] if node not in self._possible_offences[module]: self._possible_offences[module].append(node) @utils.check_messages('new-db-field-with-default') def close(self): def _path(node): return node.path # sort all migrations by name in reverse order b/c # we need only the latest ones self._migration_modules.sort(key=_path, reverse=True) # filter out the last migration modules under each distinct # migrations directory, iow leave only the latest migrations # for each application last_name_space = '' latest_migrations = [] for module in self._migration_modules: name_space = module.path[0].split('migrations')[0] if name_space != last_name_space: last_name_space = name_space latest_migrations.append(module) for module, nodes in self._possible_offences.items(): if module in latest_migrations: for node in nodes: self.add_message('new-db-field-with-default', args=module.name, node=node) def load_configuration(linter): # don't blacklist migrations for this checker new_black_list = list(linter.config.black_list) if 'migrations' in new_black_list: new_black_list.remove('migrations') linter.config.black_list = new_black_list def register(linter): """Required method to auto register this checker.""" linter.register_checker(NewDbFieldWithDefaultChecker(linter)) if not compat.LOAD_CONFIGURATION_SUPPORTED: load_configuration(linter)