|
| 1 | +## Rewrite SQLAlchemy `mapped_column` with Type Annotations <Badge type="tip" text="Has Fix" /> |
| 2 | + |
| 3 | +* [Playground Link](/playground.html#eyJtb2RlIjoiQ29uZmlnIiwibGFuZyI6InB5dGhvbiIsInF1ZXJ5IjoiYShudWxsYWJsZT1UcnVlKSIsInJld3JpdGUiOiIxMjMiLCJzdHJpY3RuZXNzIjoic21hcnQiLCJzZWxlY3RvciI6ImtleXdvcmRfYXJndW1lbnQiLCJjb25maWciOiJpZDogcmVtb3ZlLW51bGxhYmxlLWFyZ1xubGFuZ3VhZ2U6IHB5dGhvblxucnVsZTpcbiAgcGF0dGVybjogJFggPSBtYXBwZWRfY29sdW1uKCQkJEFSR1MpXG4gIGFueTpcbiAgICAtIHBhdHRlcm46ICRYID0gbWFwcGVkX2NvbHVtbigkJCRCRUZPUkUsIFN0cmluZywgJCQkTUlELCBudWxsYWJsZT1UcnVlLCAkJCRBRlRFUilcbiAgICAtIHBhdHRlcm46ICRYID0gbWFwcGVkX2NvbHVtbigkJCRCRUZPUkUsIFN0cmluZywgJCQkTUlELCBudWxsYWJsZT1UcnVlKVxucmV3cml0ZXJzOlxuLSBpZDogZmlsdGVyLXN0cmluZy1udWxsYWJsZVxuICBydWxlOlxuICAgIHBhdHRlcm46ICRBUkdcbiAgICBpbnNpZGU6XG4gICAgICBraW5kOiBhcmd1bWVudF9saXN0XG4gICAgYWxsOlxuICAgIC0gbm90OiBcbiAgICAgICAgcGF0dGVybjogU3RyaW5nXG4gICAgLSBub3Q6XG4gICAgICAgIHBhdHRlcm46XG4gICAgICAgICAgY29udGV4dDogYShudWxsYWJsZT1UcnVlKVxuICAgICAgICAgIHNlbGVjdG9yOiBrZXl3b3JkX2FyZ3VtZW50XG4gIGZpeDogJEFSR1xuXG50cmFuc2Zvcm06XG4gIE5FV0FSR1M6XG4gICAgcmV3cml0ZTpcbiAgICAgIHJld3JpdGVyczogW2ZpbHRlci1zdHJpbmctbnVsbGFibGVdXG4gICAgICBzb3VyY2U6ICQkJEFSR1NcbiAgICAgIGpvaW5CeTogJywgJ1xuZml4OiB8LVxuICAkWDogTWFwcGVkW3N0ciB8IE5vbmVdID0gbWFwcGVkX2NvbHVtbigkTkVXQVJHUykiLCJzb3VyY2UiOiJtZXNzYWdlID0gbWFwcGVkX2NvbHVtbihTdHJpbmcsIGRlZmF1bHQ9XCJoZWxsb1wiLCBudWxsYWJsZT1UcnVlKVxuXG5tZXNzYWdlID0gbWFwcGVkX2NvbHVtbihTdHJpbmcsIG51bGxhYmxlPVRydWUpXG5cbl9tZXNzYWdlID0gbWFwcGVkX2NvbHVtbihcIm1lc3NhZ2VcIiwgU3RyaW5nLCBudWxsYWJsZT1UcnVlKVxuXG5tZXNzYWdlID0gbWFwcGVkX2NvbHVtbihTdHJpbmcsIG51bGxhYmxlPVRydWUsIHVuaXF1ZT1UcnVlKVxuXG5tZXNzYWdlID0gbWFwcGVkX2NvbHVtbihcbiAgU3RyaW5nLCBpbmRleD1UcnVlLCBudWxsYWJsZT1UcnVlLCB1bmlxdWU9VHJ1ZSlcblxuIyBTaG91bGQgbm90IGJlIHRyYW5zZm9ybWVkXG5tZXNzYWdlID0gbWFwcGVkX2NvbHVtbihTdHJpbmcsIGRlZmF1bHQ9XCJoZWxsb1wiKVxuXG5tZXNzYWdlID0gbWFwcGVkX2NvbHVtbihTdHJpbmcsIGRlZmF1bHQ9XCJoZWxsb1wiLCBudWxsYWJsZT1GYWxzZSlcblxubWVzc2FnZSA9IG1hcHBlZF9jb2x1bW4oSW50ZWdlciwgZGVmYXVsdD1cImhlbGxvXCIpIn0=) |
| 4 | + |
| 5 | +### Description |
| 6 | + |
| 7 | +[SQLAlchemy 2.0](https://docs.sqlalchemy.org/en/20/orm/declarative_tables.html) recommends using type annotations with `Mapped` type for modern declarative mapping. The `mapped_column()` construct can derive its configuration from [PEP 484](https://peps.python.org/pep-0484/) type annotations. |
| 8 | + |
| 9 | +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]`. |
| 10 | + |
| 11 | +The key technique demonstrated here is using **rewriters** to selectively filter arguments. The rewriter: |
| 12 | +1. Matches each argument inside the `argument_list` |
| 13 | +2. Excludes the `String` type argument |
| 14 | +3. Excludes the `nullable=True` keyword argument |
| 15 | +4. Keeps all other arguments |
| 16 | + |
| 17 | +### YAML |
| 18 | +```yaml |
| 19 | +id: remove-nullable-arg |
| 20 | +language: python |
| 21 | +rule: |
| 22 | + pattern: $X = mapped_column($$$ARGS) |
| 23 | + any: |
| 24 | + - pattern: $X = mapped_column($$$BEFORE, String, $$$MID, nullable=True, $$$AFTER) |
| 25 | + - pattern: $X = mapped_column($$$BEFORE, String, $$$MID, nullable=True) |
| 26 | +rewriters: |
| 27 | +- id: filter-string-nullable |
| 28 | + rule: |
| 29 | + pattern: $ARG |
| 30 | + inside: |
| 31 | + kind: argument_list |
| 32 | + all: |
| 33 | + - not: |
| 34 | + pattern: String |
| 35 | + - not: |
| 36 | + pattern: |
| 37 | + context: a(nullable=True) |
| 38 | + selector: keyword_argument |
| 39 | + fix: $ARG |
| 40 | + |
| 41 | +transform: |
| 42 | + NEWARGS: |
| 43 | + rewrite: |
| 44 | + rewriters: [filter-string-nullable] |
| 45 | + source: $$$ARGS |
| 46 | + joinBy: ', ' |
| 47 | +fix: |- |
| 48 | + $X: Mapped[str | None] = mapped_column($NEWARGS) |
| 49 | +``` |
| 50 | +
|
| 51 | +### Example |
| 52 | +
|
| 53 | +<!-- highlight matched code in curly-brace {lineNum} --> |
| 54 | +```python {1,3,5,7-8} |
| 55 | +message = mapped_column(String, default="hello", nullable=True) |
| 56 | + |
| 57 | +message = mapped_column(String, nullable=True) |
| 58 | + |
| 59 | +_message = mapped_column("message", String, nullable=True) |
| 60 | + |
| 61 | +message = mapped_column(String, nullable=True, unique=True) |
| 62 | + |
| 63 | +message = mapped_column( |
| 64 | + String, index=True, nullable=True, unique=True) |
| 65 | + |
| 66 | +# Should not be transformed |
| 67 | +message = mapped_column(String, default="hello") |
| 68 | + |
| 69 | +message = mapped_column(String, default="hello", nullable=False) |
| 70 | + |
| 71 | +message = mapped_column(Integer, default="hello") |
| 72 | +``` |
| 73 | + |
| 74 | +### Diff |
| 75 | +<!-- use # [!code --] and # [!code ++] to annotate diff --> |
| 76 | +```python |
| 77 | +message = mapped_column(String, default="hello", nullable=True) # [!code --] |
| 78 | +message: Mapped[str | None] = mapped_column(default="hello") # [!code ++] |
| 79 | + |
| 80 | +message = mapped_column(String, nullable=True) # [!code --] |
| 81 | +message: Mapped[str | None] = mapped_column() # [!code ++] |
| 82 | + |
| 83 | +_message = mapped_column("message", String, nullable=True) # [!code --] |
| 84 | +_message: Mapped[str | None] = mapped_column("message") # [!code ++] |
| 85 | + |
| 86 | +message = mapped_column(String, nullable=True, unique=True) # [!code --] |
| 87 | +message: Mapped[str | None] = mapped_column(unique=True) # [!code ++] |
| 88 | + |
| 89 | +message = mapped_column( # [!code --] |
| 90 | + String, index=True, nullable=True, unique=True) # [!code --] |
| 91 | +message: Mapped[str | None] = mapped_column( # [!code ++] |
| 92 | + index=True, unique=True) # [!code ++] |
| 93 | +``` |
| 94 | + |
| 95 | +### Contributed by |
| 96 | +Inspired by [discussion #2319](https://github.com/ast-grep/ast-grep/discussions/2319) |
0 commit comments