Skip to content

py-v1.14.0 - migration-risk: 407 criticals on six big Django codebases, 2 real

Choose a tag to compare

@github-actions github-actions released this 30 Oct 21:45
· 16 commits to main since this release
cec184d

Fixed

  • migration-risk: 407 criticals on six big Django codebases, 2 real.
    Run over NetBox, PostHog, authentik, Plane, paperless-ngx and Healthchecks,
    it reported 407 critical findings, and 2 of them were real. The rules now
    follow what Django actually does to existing rows:

    • ManyToManyField, TaggableManager and GenericRelation add no column,
      so they are never a NOT NULL risk.
    • blank=True on a text field, including project subclasses such as
      ColorField(max_length=6), gets "". auto_now / auto_now_add get the
      current time, and db_default and GeneratedField fill themselves.
    • A table created earlier in the same migration is empty. A squashed
      migration (replaces = [...]) only ever runs on a fresh database.
    • unique=True with null=True and no default, or default=None, fills
      rows with NULL, which never collides.
    • A field removed and then re-added, renamed onto the same name, or a model
      deleted and re-created, in that migration or a later one, is no longer
      "still referenced".
    • The migration graph collapses squashed migrations the way Django's loader
      does, so a squash and the migrations it replaces are not reported as
      conflicting leaves.
    • A NOT NULL column added after a RunPython, or after a RunSQL that
      deletes, updates or truncates rows, is a warning instead of a certain
      critical, because that step may have emptied the table.
  • nplusone reported writes and test code as N+1 reads. Checked against
    NetBox, PostHog, authentik, Plane, paperless-ngx and Healthchecks:

    • post.tags.set(...), .add(), .remove(), .clear(), .create() and
      .get_or_create() on a relation were caught by the attribute-chain
      fallback and given a prefetch, which cannot serve a write. They are not
      reported now. Loading a single-valued relation before writing to it
      (profile.user.delete()) is still an N+1, and its fix is
      select_related. QuerySet writes such as update() and delete() on a
      related manager stay findings, as before.
    • One field is never suggested in both select_related and
      prefetch_related. .select_related("user").prefetch_related("user")
      came from a schema-less guess on x.user.delete().
    • Test modules (test_*.py, *_test.py, tests.py, conftest.py, and
      anything under tests/ or test/) are skipped, because a query per row
      in a test is not a production N+1. Pass --include-tests to scan them.
      This also halves the scan time on large repositories (PostHog: 80 s to
      39 s).