Skip to content

Conversation

Charlie-BU
Copy link
Contributor

Description

This PR adds a built-in database migration tool to Robyn, powered by Alembic. It introduces a new robyn db CLI command group that provides a full suite of migration capabilities, including init, migrate, upgrade, downgrade, stamp, and more.

Summary

This PR does the following:

  • Adds a fully integrated migration CLI (robyn db) for SQLAlchemy-based projects
  • Wraps Alembic’s command interface with a consistent Robyn-style CLI structure
  • Auto-generates and configures alembic.ini and env.py based on user input or intelligent detection (e.g., database URL, model metadata path)
  • Supports common Alembic commands: init, revision, migrate, upgrade, downgrade, merge, edit, stamp, show, history, etc.
  • Includes support for migration templates (e.g., --template robyn)
  • Handles command-line errors gracefully with user-friendly messages
  • Provides extension class Migrate that can be optionally used inside Robyn apps for future integration

This aims to make database versioning and schema evolution first-class citizens in the Robyn developer workflow, reducing friction for backend developers.

PR Checklist

Please ensure that:

  • The PR contains a descriptive title
  • The PR contains a descriptive summary of the changes
  • You build and test your changes before submitting a PR.
  • You have added relevant documentation
  • You have added relevant tests. We prefer integration tests wherever possible

Pre-Commit Instructions:

Copy link

vercel bot commented Jul 10, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
robyn ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jul 19, 2025 6:55am

Copy link

recurseml bot commented Jul 10, 2025

✨ No issues found! Your code is sparkling clean! ✨

⚠️ Only 5 files were analyzed due to processing limits.

Need help? Join our Discord for support!
https://discord.gg/qEjHQk64Z9

Copy link

codspeed-hq bot commented Jul 10, 2025

CodSpeed Performance Report

Merging #1205 will not alter performance

Comparing Charlie-BU:main (2c0a4aa) with main (d8339a7)

Summary

✅ 150 untouched benchmarks

@sansyrox
Copy link
Member

acknowledging the PR 😄

Will need to learn a few things before review

Comment on lines +87 to +93
parser.add_argument(
"db",
nargs="?",
default=None,
help="Database migration commands. Use 'robyn db' to see more information.",
)
Copy link
Member

Choose a reason for hiding this comment

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

@Charlie-BU , this supposed to invoke alembic cli?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, but the alembic cli is being encapsulated in migrate.py. The arguments passed by robyn and that passed by alembic are not exactly the same, so i need to preprocess the robyn cli argument to make it available to alembic. The details are all in migrate.py.

Here, it just adds a positional argument for robyn. Then user would be able to revoke the encapsulated alembic cli using robyn db ..., which being handled here:
image
image

import subprocess
import sys
import webbrowser
import argparse
Copy link
Member

Choose a reason for hiding this comment

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

do we need this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we need this to define a sub-argument for robyn, which is robyn db .... We might need argparse to process the argument passed after robyn db.
image

robyn/cli.py Outdated
if db_migration == "Y":
print("Installing the latest version of alembic...")
try:
subprocess.run([sys.executable, "-m", "pip", "install", "alembic", "-q"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
Copy link
Member

Choose a reason for hiding this comment

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

there should be a better way here, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was considering to add alembic as a dependency when people execute pip install robyn. But in view that not everyone needs this database migration tool, I decided to make it an option for people when they do robyn create. If they said yes then alembic is a must in this case.
I modified this self-installing process like this:
image
Pls check if there's something that could be optimized. Thx! :D

Copy link
Member

Choose a reason for hiding this comment

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

@Charlie-BU , maybe we can allow it to be installed like pip install robyn[db] and then it installs alembic too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Cool! I removed this as you said. And set the pyproject.toml like this. So people will install alembic when they do pip install robyn[db]
image

image

robyn/migrate.py Outdated
Comment on lines 44 to 59
def catch_errors(f):
"""Decorator to catch and handle errors."""

@wraps(f)
def wrapped(*args, **kwargs):
try:
return f(*args, **kwargs)
except Exception as e:
print(f"ERROR: {str(e)}")
sys.exit(1)

return wrapped
Copy link
Member

Choose a reason for hiding this comment

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

I don't like this pattern tbh. We should have specific exception handling. not a generic catch all at the library level.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let me think how to refactor this...😊

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I optimized my decorator. Let it catch specific exception while executing alembic commands with command.*, and inform the exception detail. Thanks for your advice!!:D

Comment on lines +39 to +45
if template is None:
return Path(__file__).parent / "templates" / "robyn"
return Path(template)
Copy link
Member

Choose a reason for hiding this comment

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

This path validation might be incorrect.

Copy link
Contributor Author

@Charlie-BU Charlie-BU Jul 15, 2025

Choose a reason for hiding this comment

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

Could you possibly explain more? I think it works.
image

robyn/migrate.py Outdated
Comment on lines 118 to 129
import_statement = f"sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))\nfrom {module_path} import {class_name}\ntarget_metadata = {class_name}.metadata"

# Replace the import statement
content = content.replace(
"# add your model's MetaData object here\n# for 'autogenerate' support\n# from myapp import mymodel\n# target_metadata = mymodel.Base.metadata",
f"# add your model's MetaData object here\n# for 'autogenerate' support\n{import_statement}",
)

# Replace the target_metadata setting
content = content.replace(
"target_metadata = config.attributes.get('sqlalchemy.metadata', None)",
"# target_metadata = config.attributes.get('sqlalchemy.metadata', None)\n# Already set by the import above",
Copy link
Member

Choose a reason for hiding this comment

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

This file can be rewritten.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This env.py file and alembic.ini file are automatically created by alembic when handling alembic init alembic, which is robyn db init now. And we have to modify the configuration here to adapt it to the real case.

Copy link
Member

Choose a reason for hiding this comment

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

@Charlie-BU , so do we need to commit this file?

Copy link
Contributor Author

@Charlie-BU Charlie-BU Jul 19, 2025

Choose a reason for hiding this comment

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

yeah I think we do. That's a template whose path is needed passing into alembic init alembic, which is command.init(config, directory, template=template_path, package=package) in our case.
image

So that's why I wrote a _get_template_path() method in class Config. It's a must when we initialize the alembic using python code instead of command alembic init alembic.
image

Copy link
Member

Choose a reason for hiding this comment

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

might be better to create a db module. This file will be incredible hard to test otherwise.

Copy link
Member

Choose a reason for hiding this comment

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

@Charlie-BU , what do you think here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm really sorrrrrrrry I'm not quite familiar about to design a separate module. I don't know if i need to redesign the whole robyn-db-migration structure to do that?

@sansyrox
Copy link
Member

Hey @Charlie-BU 👋

Thanks for the PR. This is a good start :D But I have some inline comments.

Copy link
Member

Choose a reason for hiding this comment

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

is this file autogenerated @Charlie-BU ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, autogenerated by alembic init alembic.

…ion module

- Replace generic error decorator with typed migration-specific error handling
- Add proper type hints and validation for migration operations
- Rename database dependency key from "database" to "db" for consistency
- Remove redundant db_migration option from CLI
- Improve error messages and logging for migration operations
@Charlie-BU
Copy link
Contributor Author

@sansyrox I double tested almost every possible situation on my localhost. With alembic installed, without alembic installed, pip install robyn[db], before robyn db init, after robyn db init, wrong commands like robyn db c8iufbfsa, ... All worked out fine! ☺️

@sansyrox
Copy link
Member

@recurseml re-review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants