Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions examples/sqlalchemy_1.6_to_2.0/output_repo/database.py

This file was deleted.

102 changes: 0 additions & 102 deletions examples/sqlalchemy_1.6_to_2.0/output_repo/main.py

This file was deleted.

32 changes: 0 additions & 32 deletions examples/sqlalchemy_1.6_to_2.0/output_repo/models.py

This file was deleted.

31 changes: 0 additions & 31 deletions examples/sqlalchemy_1.6_to_2.0/output_repo/schemas.py

This file was deleted.

105 changes: 105 additions & 0 deletions examples/sqlalchemy_1.6_to_2.0/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import codegen
from codegen import Codebase
from codegen.sdk.core.detached_symbols.function_call import FunctionCall
from codegen.sdk.core.expressions.chained_attribute import ChainedAttribute


@codegen.function("sqlalchemy-1.6-to-2.0")
def run(codebase: Codebase):
"""
Convert SQLAlchemy 1.6 codebases to 2.0.
"""
files_modified = 0
functions_modified = 0

print("\nStarting SQLAlchemy 1.6 to 2.0 migration...")
Copy link

@clee-codegen clee-codegen Jan 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this type of logging can happen as part of the function wrapper in the CLI.


for file in codebase.files:
file_modified = False
print(f"\nProcessing file: {file.path}")

# Step 1: Convert Query to Select
for call in file.function_calls:
if call.name == "query":
chain = call
while chain.parent and isinstance(chain.parent, ChainedAttribute):
chain = chain.parent

original_code = chain.source
new_query = chain.source.replace("query(", "select(")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sqlalchemy 2.0 conventions are to use from sqlalchemy import select for everything right?

Wasn't 1.6 syntax more like

Model.query.filter()
session.query(Model)

and both of these should change to

session.scalars(select(Model).where(...))
or session.scalar(select(Modle).where(...))

and in cases where you're not querying the whole ORM object it would have to be

session.execute(select(Model.a, Model.b))

i believe filter_by is still valid too on the new select

if "filter(" in new_query:
new_query = new_query.replace(".filter(", ".where(")
if "filter_by(" in new_query:
model = call.args[0].value
conditions = chain.source.split("filter_by(")[1].split(")")[0]
new_conditions = [f"{model}.{cond.strip().replace('=', ' == ')}" for cond in conditions.split(",")]
new_query = f".where({' & '.join(new_conditions)})"
if "execute" not in chain.parent.source:
new_query = f"execute({new_query}).scalars()"

print(f"\nConverting query in {file.path}:\n")
print("Original code:")
print(original_code)
print("\nNew code:")
print(new_query)
print("-" * 50)

chain.edit(new_query)
file_modified = True
functions_modified += 1

# Step 2: Modernize ORM Relationships
for cls in file.classes:
for attr in cls.attributes:
if isinstance(attr.value, FunctionCall) and attr.value.name == "relationship":
if "lazy=" not in attr.value.source:
original_rel = attr.value.source
new_rel = original_rel + ', lazy="select"'
if "backref" in new_rel:
new_rel = new_rel.replace("backref", "back_populates")

print(f"\nUpdating relationship in class {cls.name}:\n")
print("Original code:")
print(original_rel)
print("\nNew code:")
print(new_rel)
print("-" * 50)

attr.value.edit(new_rel)
file_modified = True
functions_modified += 1

# Step 3: Convert Column Definitions to Type Annotations
for cls in file.classes:
for attr in cls.attributes:
if "Column(" in attr.source:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might want to make sure Column( is the beginning of the assignment

original_attr = attr.source
new_attr = original_attr.replace("Column", "mapped_column")
type_hint = "Mapped" + original_attr.split("= Column")[1]
Copy link

@clee-codegen clee-codegen Jan 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused by this here.

a = Column(Integer) should become
a: Mapped[int] = mapped_column()

I'm actually not sure what this new type hint would be based on this logic..

a: Mapped[(Integer)] ?

new_attr = f"{attr.name}: {type_hint}"

print(f"\nUpdating column definition in class {cls.name}:\n")
print("Original code:")
print(original_attr)
print("\nNew code:")
print(new_attr)
print("-" * 50)

attr.edit(new_attr)
file_modified = True
functions_modified += 1

if file_modified:
files_modified += 1

print("\nMigration complete:")
print(f"Files modified: {files_modified}")
print(f"Functions modified: {functions_modified}")


if __name__ == "__main__":
repo_path = "./input_repo"
print("Initializing codebase...")
codebase = Codebase(repo_path)
print("Running SQLAlchemy 1.6 to 2.0 codemod...")
run(codebase)