Skip to content

py-v1.15.0 - Catch risky Alembic migrations before they lock a table

Choose a tag to compare

@github-actions github-actions released this 31 Oct 04:12
· 16 commits to main since this release
cb0bbf8

Added

  • migration-risk reads Alembic revisions (preview). SQLAlchemy projects
    without Django (FastAPI, Flask) get the same static migration review: any
    versions/ revision is parsed with ast and never run. Eleven rules cover
    a NOT NULL add_column without server_default (SQLAlchemy's default= only
    applies on INSERT), create_index without CONCURRENTLY, alter_column to NOT
    NULL or to a new type, renamed and dropped columns and tables, unique,
    foreign-key and check constraints, and multiple heads. batch_alter_table
    blocks and Flask-Migrate layouts are covered. The populated-table rules
    learned from the Django census apply too: a same-revision create_table or
    the base revision means an empty table, and a data operation first makes the
    finding a warning. Over ~1170 revisions from Superset, Airflow, Prefect,
    Polar, Dispatch, Mealie and the FastAPI template there are 9 critical
    findings, each a NOT NULL column that fails on a populated table. Rules:
    docs/rules/alembic.md.

Fixed

  • signals / MCP signal_graph missed receivers registered with
    .connect().
    Only @receiver(...) decorators were read, so
    post_delete.connect(delete_file, sender=Attachment) in an AppConfig.ready()
    was invisible, and cascade left it out of the DB_CASCADE
    skipped_receivers list. signal.connect(handler, sender=Model) is now an
    edge like a decorator, with a bare or dotted signal (post_delete,
    models.signals.post_delete), receiver= as a keyword, and no sender
    meaning every model. To keep sockets and database clients out, a .connect()
    call only counts when the signal is a Django built-in, a Signal() defined in
    the workspace, or the call passes sender=.
    (#183) Contributed by @1AbdullahButt.

  • nplusone read assignments and instance methods as relation reads.
    Two more patterns from the real-world census:

    • device.cluster = cluster inside a loop (NetBox) assigns the attribute
      and loads nothing. device.cluster.name = x still loads
      device.cluster, so that one is still reported.
    • check.matches_tag_set(tags) (Healthchecks) is a method on the loop
      variable, but its name matched the <model>_set reverse-manager
      heuristic. A call made directly on the loop variable is never a
      relation read; managers are always one attribute deeper.