Skip to content

Commit 48f5302

Browse files
committed
.
1 parent 4082ee0 commit 48f5302

File tree

2 files changed

+162
-4
lines changed

2 files changed

+162
-4
lines changed
Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,139 @@
1-
.
1+
# SQLAlchemy Soft Delete Codemod
2+
3+
This codemod automatically adds soft delete conditions to SQLAlchemy join queries in your codebase. It ensures that joins only include non-deleted records by adding appropriate `deleted_at` checks.
4+
5+
## Overview
6+
7+
The codemod analyzes your codebase and automatically adds soft delete conditions to SQLAlchemy join methods (`join`, `outerjoin`, `innerjoin`) for specified models. This helps prevent accidentally including soft-deleted records in query results.
8+
9+
## How It Works
10+
11+
The codemod processes your codebase in several steps:
12+
13+
1. **Join Detection**
14+
```python
15+
def should_process_join_call(call, soft_delete_models, join_methods):
16+
if str(call.name) not in join_methods:
17+
return False
18+
19+
call_args = list(call.args)
20+
if not call_args:
21+
return False
22+
23+
model_name = str(call_args[0].value)
24+
return model_name in soft_delete_models
25+
```
26+
- Scans for SQLAlchemy join method calls (`join`, `outerjoin`, `innerjoin`)
27+
- Identifies joins involving soft-deletable models
28+
- Analyzes existing join conditions
29+
30+
2. **Condition Addition**
31+
```python
32+
def add_deleted_at_check(file, call, model_name):
33+
call_args = list(call.args)
34+
deleted_at_check = f"{model_name}.deleted_at.is_(None)"
35+
36+
if len(call_args) == 1:
37+
call_args.append(deleted_at_check)
38+
return
39+
40+
second_arg = call_args[1].value
41+
if isinstance(second_arg, FunctionCall) and second_arg.name == "and_":
42+
second_arg.args.append(deleted_at_check)
43+
else:
44+
call_args[1].edit(f"and_({second_arg.source}, {deleted_at_check})")
45+
```
46+
- Adds `deleted_at.is_(None)` checks to qualifying joins
47+
- Handles different join condition patterns:
48+
- Simple joins with no conditions
49+
- Joins with existing conditions (combines using `and_`)
50+
- Preserves existing conditions while adding soft delete checks
51+
52+
3. **Import Management**
53+
```python
54+
def ensure_and_import(file):
55+
if not any("and_" in imp.name for imp in file.imports):
56+
file.add_import_from_import_string("from sqlalchemy import and_")
57+
```
58+
- Automatically adds required SQLAlchemy imports (`and_`)
59+
- Prevents duplicate imports
60+
61+
## Configuration
62+
63+
### Soft Delete Models
64+
65+
The codemod processes joins for the following models:
66+
```python
67+
soft_delete_models = {
68+
"User",
69+
"Update",
70+
"Proposal",
71+
"Comment",
72+
"Project",
73+
"Team",
74+
"SavedSession"
75+
}
76+
```
77+
78+
### Join Methods
79+
80+
The codemod handles these SQLAlchemy join methods:
81+
```python
82+
join_methods = {"join", "outerjoin", "innerjoin"}
83+
```
84+
85+
## Code Transformations
86+
87+
### Simple Join
88+
```python
89+
# Before
90+
query.join(User)
91+
92+
# After
93+
from sqlalchemy import and_
94+
query.join(User, User.deleted_at.is_(None))
95+
```
96+
97+
### Join with Existing Condition
98+
```python
99+
# Before
100+
query.join(User, User.id == Post.user_id)
101+
102+
# After
103+
from sqlalchemy import and_
104+
query.join(User, and_(User.id == Post.user_id, User.deleted_at.is_(None)))
105+
```
106+
107+
## Graph Disable Mode
108+
109+
This codemod includes support for running without the graph feature enabled. This is useful for the faster processing of large codebases and reduced memory usage.
110+
111+
To run in no-graph mode:
112+
```python
113+
codebase = Codebase(
114+
str(repo_path),
115+
programming_language=ProgrammingLanguage.PYTHON,
116+
config=CodebaseConfig(
117+
feature_flags=GSFeatureFlags(disable_graph=True)
118+
)
119+
)
120+
```
121+
122+
## Running the Conversion
123+
124+
```bash
125+
# Install Codegen
126+
pip install codegen
127+
128+
# Run the conversion
129+
python run.py
130+
```
131+
132+
## Learn More
133+
134+
- [SQLAlchemy Documentation](https://docs.sqlalchemy.org/en/20/)
135+
- [Codegen Documentation](https://docs.codegen.com)
136+
137+
## Contributing
138+
139+
Feel free to submit issues and enhancement requests!

examples/sqlalchemy_soft_delete/run.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
from codegen import Codebase
33
from codegen.sdk.core.detached_symbols.function_call import FunctionCall
44
from codegen.sdk.enums import ProgrammingLanguage
5+
import shutil
6+
import subprocess
7+
from pathlib import Path
58

69

710
def should_process_join_call(call, soft_delete_models, join_methods):
@@ -52,6 +55,13 @@ def ensure_and_import(file):
5255
file.add_import_from_import_string("from sqlalchemy import and_")
5356

5457

58+
def clone_repo(repo_url: str, repo_path: Path) -> None:
59+
"""Clone a git repository to the specified path."""
60+
if repo_path.exists():
61+
shutil.rmtree(repo_path)
62+
subprocess.run(["git", "clone", repo_url, str(repo_path)], check=True)
63+
64+
5565
@codegen.function("sqlalchemy-soft-delete")
5666
def process_soft_deletes(codebase):
5767
"""Process soft delete conditions for join methods in the codebase."""
@@ -75,11 +85,21 @@ def process_soft_deletes(codebase):
7585
print(f"Found join method for model {model_name} in file {file.filepath}")
7686
add_deleted_at_check(file, call, model_name)
7787

88+
codebase.commit()
7889
print("commit")
7990
print(codebase.get_diff())
8091

8192

8293
if __name__ == "__main__":
83-
codebase = Codebase.from_repo("hasgeek/funnel", programming_language=ProgrammingLanguage.PYTHON)
84-
print(codebase.files)
85-
process_soft_deletes(codebase)
94+
from codegen.sdk.core.codebase import Codebase
95+
from codegen.sdk.codebase.config import CodebaseConfig, GSFeatureFlags
96+
97+
repo_path = Path("/tmp/core")
98+
repo_url = "https://github.com/hasgeek/funnel.git"
99+
100+
try:
101+
clone_repo(repo_url, repo_path)
102+
codebase = Codebase(str(repo_path), programming_language=ProgrammingLanguage.PYTHON, config=CodebaseConfig(feature_flags=GSFeatureFlags(disable_graph=True)))
103+
process_soft_deletes(codebase)
104+
finally:
105+
shutil.rmtree(repo_path)

0 commit comments

Comments
 (0)