Skip to content

Rewrite SQLAlchemy with Type Annotations Has Fix

Description

SQLAlchemy 2.0 recommends using type annotations with Mapped type for modern declarative mapping. The mapped_column() construct can derive its configuration from PEP 484 type annotations.

This rule helps migrate legacy SQLAlchemy code that explicitly uses String type and nullable=True to the modern type annotation approach using Mapped[str | None].

The key technique demonstrated here is using rewriters to selectively filter arguments. The rewriter:

  1. Matches each argument inside the argument_list
  2. Excludes the String type argument
  3. Excludes the nullable=True keyword argument
  4. Keeps all other arguments

YAML

yaml
id: remove-nullable-arg
language: python
rule:
  pattern: $X = mapped_column($$$ARGS)
  any:
    - pattern: $X = mapped_column($$$BEFORE, String, $$$MID, nullable=True, $$$AFTER)
    - pattern: $X = mapped_column($$$BEFORE, String, $$$MID, nullable=True)
rewriters:
- id: filter-string-nullable
  rule:
    pattern: $ARG
    inside:
      kind: argument_list
    all:
    - not:
        pattern: String
    - not:
        pattern:
          context: a(nullable=True)
          selector: keyword_argument
  fix: $ARG

transform:
  NEWARGS:
    rewrite:
      rewriters: [filter-string-nullable]
      source: $$$ARGS
      joinBy: ', '
fix: |-
  $X: Mapped[str | None] = mapped_column($NEWARGS)

Example

python
message = mapped_column(String, default="hello", nullable=True)

message = mapped_column(String, nullable=True)

_message = mapped_column("message", String, nullable=True)

message = mapped_column(String, nullable=True, unique=True)

message = mapped_column(
  String, index=True, nullable=True, unique=True)

# Should not be transformed
message = mapped_column(String, default="hello")

message = mapped_column(String, default="hello", nullable=False)

message = mapped_column(Integer, default="hello")

Diff

python
message = mapped_column(String, default="hello", nullable=True) 
message: Mapped[str | None] = mapped_column(default="hello") 

message = mapped_column(String, nullable=True) 
message: Mapped[str | None] = mapped_column() 

_message = mapped_column("message", String, nullable=True) 
_message: Mapped[str | None] = mapped_column("message") 

message = mapped_column(String, nullable=True, unique=True) 
message: Mapped[str | None] = mapped_column(unique=True) 

message = mapped_column( 
  String, index=True, nullable=True, unique=True) 
message: Mapped[str | None] = mapped_column( 
  index=True, unique=True) 

Contributed by

Inspired by discussion #2319

Made with ❤️ with Rust