Giter Site home page Giter Site logo

3yourmind / django-migration-linter Goto Github PK

View Code? Open in Web Editor NEW
509.0 11.0 55.0 581 KB

:rocket: Detect backward incompatible migrations for your django project

Home Page: https://pypi.python.org/pypi/django-migration-linter/

License: Apache License 2.0

Python 100.00%
django python linter database mysql postgresql migrations

django-migration-linter's People

Contributors

ambientlight avatar atadau avatar blimmer avatar blueyed avatar britonad avatar christianbundy avatar dathoangse avatar david-wobrock avatar davidcain avatar dependabot[bot] avatar devend711 avatar drjeep avatar fevral13 avatar flixx avatar igeligel avatar jasonlee-alation avatar kekekekule avatar kotyara1005 avatar kytta avatar lieryan avatar linuxmaniac avatar phillipuniverse avatar pod666 avatar skarzi avatar sobolevn avatar soerface avatar sondrelg avatar tuky avatar vladmalynych avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

django-migration-linter's Issues

NOT_NULL not validating properly?

I have a model as such, where I am adding ONLY the description field. Which should allow null and blank values:

class BlogPost(models.Model):
    """
    This model is used just as an example.

    With it we show how one can:
    - Use fixtures and factoris
    - Use migrations testing

    """

    title = models.CharField(max_length=_POST_TITLE_MAX_LENGTH)
    body = models.TextField()
    # only this line is new
    description = models.TextField(null=True, blank=True)

    created_at = models.DateTimeField(auto_now_add=True)
    modifiet_at = models.DateTimeField(auto_now=True)

    def __str__(self) -> str:
        """All django models should have this method."""
        return textwrap.wrap(self.title, _POST_TITLE_MAX_LENGTH // 4)[0]

and running makemigrations creates this migration:

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('main', '0001_initial'),
    ]

    operations = [
        migrations.AddField(
            model_name='blogpost',
            name='description',
            field=models.TextField(blank=True, null=True),
        ),
    ]

and the migration seems correct. I am adding a new column which is backwards compatible by allowing NULL and blank values. But when running ./manage.py lintmigrations I get this error:

โžœ ./manage.py lintmigrations     
(axes, 0001_initial)... OK (cached)
(axes, 0002_auto_20151217_2044)... ERR (cached)
	NOT NULL constraint on columns
(axes, 0003_auto_20160322_0929)... ERR (cached)
	NOT NULL constraint on columns
(axes, 0004_auto_20181024_1538)... ERR (cached)
	NOT NULL constraint on columns
(axes, 0005_remove_accessattempt_trusted)... ERR (cached)
	NOT NULL constraint on columns
(axes, 0006_remove_accesslog_trusted)... ERR (cached)
	NOT NULL constraint on columns
(db, 0001_initial)... OK (cached)
(main, 0001_initial)... OK (cached)
(main, 0002_blogpost_description)... ERR
	NOT NULL constraint on columns
*** Summary:
Valid migrations: 3/9 - erroneous migrations: 6/9 - ignored migrations: 0/9

Where it is giving me a NOT NULL constraint. Which should not be true as I want to allow it to be nullable. the new migration is 0002_blogpost_description So I cannot figure out why I am getting this error.

Alternative functionality to "since this commit"

I need similar functionality to the "since this commit" commit ID parameter but since I will be running this within a container I will not have access to the git repository. I can manually determine which migrations are new but once I have the list I cannot use them with the current functionality (aside from running the lint once for each name).

I have a couple ideas on how this could be achieved:

  1. Adjust --ignore-name-contains or --ignore-name parameters to accept a list of values. This way one could pass in all the migration names to ignore
  2. Utilize the sequential Django migration numbers and add a "since this migration" parameter that would work similar to "since this commit". This way one could pass a migration number such as 0006, and the linter would run from 0007 through n migrations.

Im curious what your thoughts are on this. Im going to explore using the unapplied-migrations flag for now but in a test environment that configures a mock database this wont work since all migrations will be applied or unapplied.

drop check of .git dir on django_path

There's no need to check for .git dir, git diff already fails if not inside a git repository

# git diff --name-only --diff-filter=A whatever
fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).

On a repo that the django project is not in the root, there's no $django_path/.git dir there and is a proper git repo

Add type hinting

Maybe not now, but once we only support python 3.6, we can start to type annotate the linter in a uniform way.
And add mypy as static linter

Calling next() on empty array

======================================================================
ERROR: test_latest_migrations (apps.b3_core.tests.test_migrations.MigrationTest)
----------------------------------------------------------------------
Traceback (most recent call last):
...
  File "/home/melvyn/.local/py-env/django-ECigcvew/lib/python3.6/site-packages/django_migration_linter/migration_linter.py", line 102, in lint_all_migrations
    self.lint_migration(*m)
  File "/home/melvyn/.local/py-env/django-ECigcvew/lib/python3.6/site-packages/django_migration_linter/migration_linter.py", line 69, in lint_migration
    sql_statements)
  File "/home/melvyn/.local/py-env/django-ECigcvew/lib/python3.6/site-packages/django_migration_linter/sql_analyser.py", line 72, in analyse_sql_statements
    if test['fn'](statement, errors=errors):
  File "/home/melvyn/.local/py-env/django-ECigcvew/lib/python3.6/site-packages/django_migration_linter/sql_analyser.py", line 24, in has_default
    for err in kwargs['errors']
StopIteration

----------------------------------------------------------------------
Ran 1 test in 10.889

Because next() is called on an array with no elements (none match the if statement in the generator).

Ignore unique_together being removed

We can't call sqlmigrate on migrations that remove unique_together constraints because we get ValueError: Found wrong number (0) of constraints for model(field_ref, field_ref). This because Django has smart logic to find the name of the contraint to drop by inspecting the table, but once this migration has been applied, it cannot find the constraint anymore.

Do more than backward compat' linting

Django migrations need more linting than just backward incompatible checks.

Some ideas/issues to be thought about/created:

  • easy: lint if a merge migration is necessary #54
  • lint data migrations (at least show a warning for data migrations, that they can be dangerous)
  • backend specific deployment migration (expected downtime/table locking for postgresql migrations for example)
  • deeper study if more SQL can be backward incompatible

Add warning for data migrations

When a migration contains a data migration, and custom function calls, raise warnings on this migration.
Add warnings when a RunPython function is called in a migration.
Also warn when no reverse migration is specified (and it would fail to roll it back).
Maybe warn when a no-op is the reverse? ๐Ÿค”

Warnings should stack with errors.
Warnings should be able to be ignored, like some errors.
Warnings won't raise a error code != 0 (apart if bonus is done ;) )

Bonus: add an option to handle warnings as errors

Add caching

Linting a lot of migrations can take quite some time, and migrations rarely change.
We can cache the result of linting a migration and consult the cache when

  • By default, create a cache in the user's home. Maybe take inspiration from the black cache system https://github.com/ambv/black#ignoring-unmodified-files
  • Optional --no-cache option, which ignores the cache and lints everything
  • Optional --cache-path PATH which looks for the cache in this location and creates it there. Overrides the default path
  • For the checksum, use robust enough and fast enough function: MD5, or a SHA algorithm.
  • For now, the cache can be only composed of one JSON file, maybe with one entry per migration file.
{
  "hash_1": DATA_1,
  "hash_2": DATA_2
  ...

There are 3 states of migrations: OK, IGNORED, or ERROR. Evaluate if we should cache IGNORED and/or ERROR, and if yes, what information needs to be stored along with the state

  • Think about cache pruning. If an existing migration is modified, we don't want to keep the hash of this migration for ever in the cache and infinitely grow storage size. Maybe when we lint all migrations (no options provided?), we can delete the hashes that have not been checked.

Adding NOT NULL column with a default is flagged as backwards-incompatible

Adding a new column to an existing table seems to trigger an error for the NOT_NULL test even if a default is provided. This is contrary to the docs list of incompatible migrations. Here's example output from sqlmigrate using postgresql and django 1.11:

BEGIN;
--
-- Add field surname to person
--
ALTER TABLE "contacts_person" ADD COLUMN "surname" varchar(100) DEFAULT '' NOT NULL;
ALTER TABLE "contacts_person" ALTER COLUMN "surname" DROP DEFAULT;
COMMIT;

It looks like the NOT_NULL test looks for SET DEFAULT, but it doesn't look for just DEFAULT. As far as I can tell from my searching, SET DEFAULT is sql syntax for altering columns and just DEFAULT is the sql syntax for creating columns and this syntax is consistent between mysql and postgresql.

Hopefully I'm not missing something silly ๐Ÿ˜…but if this issue seems legitimate, I'm happy to try out making a PR.

(Thanks for open-sourcing this package!)

IndexError: list index out of range

Hi, I am using this project template for my project: https://github.com/wemake-services/wemake-django-template

I have installed django-migration-linter and used it like so: django-migration-linter .

That's how it failed:

ยป django-migration-linter .
Traceback (most recent call last):
  File "/Users/sobolev/Documents/PyCharmProjects/rostelecom_svetochka/.venv/bin/django-migration-linter", line 11, in <module>
    sys.exit(_main())
  File "/Users/sobolev/Documents/PyCharmProjects/rostelecom_svetochka/.venv/lib/python3.6/site-packages/django_migration_linter/migration_linter.py", line 339, in _main
    no_cache=args.no_cache,
  File "/Users/sobolev/Documents/PyCharmProjects/rostelecom_svetochka/.venv/lib/python3.6/site-packages/django_migration_linter/migration_linter.py", line 70, in __init__
    self.old_cache = Cache(self.django_path, self.cache_path)
  File "/Users/sobolev/Documents/PyCharmProjects/rostelecom_svetochka/.venv/lib/python3.6/site-packages/django_migration_linter/cache.py", line 26, in __init__
    project_name = split_path(django_folder)[-2]
IndexError: list index out of range

I have also tried to use django-migration-linter server, but is said that server is not a django app.
As a result I was not able to run this linter for my setup.

Do you have any ideas how it can be solved? Thanks!

Improve tests

Currently, the tests are mostly integration and we have dozens of "fake" django projects in this lib to simulate more or less the real-world usage. This is not very intuitive.

Investigate and implement a way to have more unit tests. Remove some of the integration tests (only few should be enough)

Lint only unapplied migrations

It would great if there is a build-in support for running the linter only on pending migrations, so CI will fail if any pending migrations are non backward compatible

Proper project structure

Right now every file is in the root directory. This is not really good.

We should have a proper source folder called src or similar and a folder called tests.

I would suggest following structure

๐Ÿ“ docs
๐Ÿ“ src
๐Ÿ“ tests
๐Ÿ“„ LICENSE
๐Ÿ“„ README.md
๐Ÿ“„ .gitignore

Handle warnings

Blocked by #90

^ adds "warnings" as returned values of the linter

Handle the warnings by adding to the options:

  • new option: "warnings as error"
  • add to quiet: "warning"

Publish package

Probably it would be good to publish this package on PyPi, so a lot of people can easily access this project and try it out.

Adding ManyToManyField triggers unique constraint

On the through model a unique constraint is added, which then triggers error. This is a classic case for inspecting the operation before calling sqlmigrate, as you cannot safely determine if the alter table statement that Django issues is for a through table or not or even for an existing table now used as through table.

Verbose logging broken

From the README:

--verbose or -v | Print more information during execution.

However, neither -v nor --verbose arguments to the lintmigrations command don't seem to work. Specifically:

$ python src/manage.py lintmigrations -v
manage.py lintmigrations: error: argument -v/--verbosity: expected one argument
$ python src/manage.py lintmigrations --verbose
manage.py lintmigrations: error: unrecognized arguments: --verbose

django-migration-linter==1.4.0
django==2.2.7
python 3.7.5

Drop table isn't catched

Let's say we have the following migration:

operations = [
        migrations.DeleteModel(
            name='A',
        ),
    ]

It's not a backward compatible change so should be catched by linter but it's not.

(app_drop_table, 0001_initial)... OK
(app_drop_table, 0002_delete_a)... OK

New tables *can* have NOT NULL on creation

The tool cries when you try to add a new table with a NOT NULL. This is not nice, and should be improved.

Eg:
new Table A with a FKey which cannot be NULL --> should be fine
new Table A with a field that cannot be NULL --> should be fine.

Integrate CI service

Would be really nice to integrate a CI service like CircleCI which would check if our code is still runnable.

We wrote tests already so we can test them there too.

Make the linter work through a management command

Speed-up the linter by changing the usage from a command line tool to a Django management command.
Basically, instead of doing
django-migration-linter my_project/
you'll do
python my_project/manage.py lintmigrations

This means:

  • a significant speed-up. We will instantiate Django once for the entire linter and every 'sqlmigrate' commands. Instead of instantiating Django each time we call 'sqlmigrate' for a migration.
  • thus less command line calls (which aren't very robust and are a source of bugs) - only one command line call will be need to get the git history
  • usage of Django internals in order to detect the migrations (so we are not re-writing logic)
  • having to add the linter to the INSTALLED_APPS of your project
  • easy possibility to add more management commands (like a makelintermigrations to override makemigrations with linting)
  • a breaking change in the usage of the linter โš ๏ธ I would prefer to deprecate the current command line usage

What do you think about this @flixx? I started writing a small Proof of Concept to see if it works.

Doesn't support Python 3 executable

I tried this library, but found that it didn't work in cases where python3 is a different executable than python. This setup is pretty common when running both python 2 and python 3 together. The line that causes the issue is here I believe

https://github.com/3YOURMIND/django-migration-linter/blob/master/django_migration_linter/migration_linter.py#L57

I am not sure how other libraries handle it, but ideally it could detect if it was installed with python 3 or python 2. Currently I get a syntax error on all python3 code.

App 'django_celery_monitor' does not have migrations

Linting process fails with an exception when using django_celery_monitor.
I expect it just to ignore apps without migrations.

Traceback:

(django_celery_monitor, 0001_initial)... Traceback (most recent call last):
  File "/Users/sobolev/Documents/PyCharmProjects/rostelecom_svetochka/.venv/bin/django-migration-linter", line 11, in <module>
    sys.exit(_main())
  File "/Users/sobolev/Documents/PyCharmProjects/rostelecom_svetochka/.venv/lib/python3.6/site-packages/django_migration_linter/migration_linter.py", line 341, in _main
    linter.lint_all_migrations(git_commit_id=args.commit_id)
  File "/Users/sobolev/Documents/PyCharmProjects/rostelecom_svetochka/.venv/lib/python3.6/site-packages/django_migration_linter/migration_linter.py", line 145, in lint_all_migrations
    self.lint_migration(m)
  File "/Users/sobolev/Documents/PyCharmProjects/rostelecom_svetochka/.venv/lib/python3.6/site-packages/django_migration_linter/migration_linter.py", line 104, in lint_migration
    sql_statements = self.get_sql(app_name, migration_name)
  File "/Users/sobolev/Documents/PyCharmProjects/rostelecom_svetochka/.venv/lib/python3.6/site-packages/django_migration_linter/migration_linter.py", line 193, in get_sql
    "sqlmigrate command failed {0}".format(err.decode("utf-8"))
RuntimeError: sqlmigrate command failed CommandError: App 'django_celery_monitor' does not have migrations

Version: django-celery-monitor==1.1.2

Usage:

INSTALLED_APPS: Tuple[str, ...] = (
    # Default django apps:
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    # django-admin:
    'django.contrib.admin',
    'django.contrib.admindocs',

    # Celery:
    'django_celery_monitor',

    ...
)

Related: wemake-services/wemake-django-template#634

Integrate to Django through the makemigrations command

Improve the linter to integrate directly yo the django makemigrations command.

This way only backward compatible migrations can be generated.

Suggestion of presentation:

    makemigrations
    0003_auto_1923423432 ...
    This migration may not be backwards compatible:
    [Show linting errors here]
    You can now edit the migration manually and check again. How do you want to proceed?
    (1) Check again.
    (2) Ignore and proceed.
    (3) Delete migration and abort.

One way to integrate it, could be adding the linter to the INSTALLED_APPS in the Django settings, and then the manage.py makemigrations would get our custom behaviour.

ping FYI @flixx

Bug in NOT_NULL migration test

I'm trying to rename a field in backward-compatible manner:

class Comment(models.Model):
    room = deprecate_field(models.CharField(max_length=32, db_index=True))
    room2 = models.CharField(max_length=32, db_index=True, null=True)

It creates the follofing migration:

operations = [
        migrations.AddField(
            model_name='comment',
            name='room2',
            field=models.CharField(db_index=True, max_length=32, null=True),
        ),
        migrations.AlterField(
            model_name='comment',
            name='room',
            field=models.CharField(db_index=True, max_length=32, null=True),
        ),
    ]

But linting fails:

(comments, 0002_auto_20190717_1208)... ERR
	NOT NULL constraint on columns
*** Summary:
Valid migrations: 0/1 - erroneous migrations: 1/1 - ignored migrations: 0/1

Raw SQL created by Django 2.2 for postgresql looks like this:

$ python manage.py sqlmigrate comments 0002_auto_20190717_1208
BEGIN;
--
-- Add field room2 to comment
--
ALTER TABLE "comments_comment" ADD COLUMN "room2" varchar(32) NULL;
--
-- Alter field room on comment
--
ALTER TABLE "comments_comment" ALTER COLUMN "room" DROP NOT NULL;
CREATE INDEX "comments_comment_room2_b53b68fe" ON "comments_comment" ("room2");
CREATE INDEX "comments_comment_room2_b53b68fe_like" ON "comments_comment" ("room2" varchar_pattern_ops);
COMMIT;

So, the related test should be fixed somehow:
https://github.com/3YOURMIND/django-migration-linter/blob/master/django_migration_linter/sql_analyser.py#L39

Add --version option

Pretty straightforward. Have a --version that prints the current version of the linter.

No migrations found when GIT_COMMIT_ID specified

When GIT_COMMIT_ID option is specified, MigrationLinter executes diff command on settings directory with --relative parameter sellected:

            git_diff_command = (
                "cd {0} && git diff --relative --name-only --diff-filter=AR {1}"
            ).format(self.django_path, git_commit_id)

If settings directory is not located in the root directory of the project, no migrations fill be found.

Proposal to use current directory, and run python manage.py lintmigrations always from the project root directory.

    def handle(self, *args, **options):
>>      current_path = os.getcwd()

        if options["verbosity"] > 1:
            logging.basicConfig(format="%(message)s", level=logging.DEBUG)
        else:
            logging.basicConfig(format="%(message)s")

        linter = MigrationLinter(
>>          current_path,
            ignore_name_contains=options["ignore_name_contains"],
            ignore_name=options["ignore_name"],
            include_apps=options["include_apps"],
            exclude_apps=options["exclude_apps"],
            database=options["database"],
            cache_path=options["cache_path"],
            no_cache=options["no_cache"],
            only_applied_migrations=options["applied_migrations"],
            only_unapplied_migrations=options["unapplied_migrations"],
        )

Other sollution: not to use --relative option.

Option to select what migration states to print

For example adding options like --hide-error --hide-ok --hide-ignored
Which would then not show output if any of those states occurred.
I can see the latter two being more useful for development, the second to be more useful for CI and the first one is there for completeness

Cache does not work if apps are not located in a subfolder

Our apps live in a subfolder called apps/. When I execute the linter I get this:

Traceback (most recent call last):
  File "/var/www/django/apps/b3_core/tests/test_migrations.py", line 39, in test_latest_migrations
    linter.lint_all_migrations()
  File "/usr/local/lib/python3.6/site-packages/django_migration_linter/migration_linter.py", line 149, in lint_all_migrations
    self.lint_migration(*m)
  File "/usr/local/lib/python3.6/site-packages/django_migration_linter/migration_linter.py", line 79, in lint_migration
    md5hash = self.old_cache.md5(app_name, migration_name)
  File "/usr/local/lib/python3.6/site-packages/django_migration_linter/cache.py", line 48, in md5
    with open(path, "rb") as f:
FileNotFoundError: [Errno 2] No such file or directory: '/var/www/django/voucher/migrations/0001_initial.py'

Probably we would need a better way to get the actual migration file for hashing. Ideas?

Improve README and documentation

Improve documentation by:

  • stripping down the README to the bare minimum as a good ad for the migration linter (add examples of output to be more striking when scrolling through the README)
  • in the README, link to different docs where necessary
  • add different documentation pages in a doc/ folder by topics (command line options, what is linted, how caching works...)

Bonus: reflect on changing the documentation format from rst to markdown

don't assume git_root is the django_path

My django project is allocated in a subdirectory of a git repo like: xpbx/xpbx so after c6df7e5 I get this error:

DJANGO_SETTINGS_MODULE=xpbx.test django-migration-linter --no-cache $(pwd)/xpbx origin/2.4.1
(customers, 0010_auto_20181026_1330)... Traceback (most recent call last):
  File "/home/vseva/Env/xpbx/bin/django-migration-linter", line 11, in <module>
    sys.exit(_main())
  File "/home/vseva/Env/xpbx/lib/python3.6/site-packages/django_migration_linter/migration_linter.py", line 341, in _main
    linter.lint_all_migrations(git_commit_id=args.commit_id)
  File "/home/vseva/Env/xpbx/lib/python3.6/site-packages/django_migration_linter/migration_linter.py", line 145, in lint_all_migrations
    self.lint_migration(m)
  File "/home/vseva/Env/xpbx/lib/python3.6/site-packages/django_migration_linter/migration_linter.py", line 81, in lint_migration
    md5hash = self.old_cache.md5(migration.abs_path)
  File "/home/vseva/Env/xpbx/lib/python3.6/site-packages/django_migration_linter/cache.py", line 47, in md5
    with open(path, "rb") as f:
FileNotFoundError: [Errno 2] No such file or directory: '/home/vseva/Foehn/xpbx/xpbx/xpbx/customers/migrations/0010_auto_20181026_1330.py'

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.