Skip to content

Comments

Auto Migrations and Async Integration#86

Open
emiliano-gandini-outeda wants to merge 40 commits intojetbase-hq:mainfrom
emiliano-gandini-outeda:automatic-sql
Open

Auto Migrations and Async Integration#86
emiliano-gandini-outeda wants to merge 40 commits intojetbase-hq:mainfrom
emiliano-gandini-outeda:automatic-sql

Conversation

@emiliano-gandini-outeda
Copy link

I hate alembic.

But I hate SQL more.

So this tools fits me to an extent, so I decided to do some upgrades to make it more useful (to me, at least).

TLDR: Django make-migrations and migrate commands integrated to JetBase.

What This PR Does

This PR adds two main features to Jetbase:

  1. Auto-generate migrations from SQLAlchemy models - No more writing SQL by hand
  2. Simpler async/sync control - Just set ASYNC=true or ASYNC=false

It also lets you run Jetbase commands from anywhere in your project, not just the jetbase/ folder.


Quick Example

Before (manual SQL):

jetbase new "create users" -v 1
# Then write SQL manually in the file...

After (auto-generate):

# models.py
class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    email = Column(String(255))
jetbase make-migrations --description "add user model"
# Creates: V20260204__add_user_model.sql with proper CREATE TABLE SQL

Feature 1: Auto-Generate Migrations

How It Works

  1. Jetbase finds your SQLAlchemy models
  2. Connects to your database and reads the current schema
  3. Compares models against the database
  4. Generates the SQL needed to make them match
  5. Writes a migration file

Finding Your Models

Jetbase looks for models in three ways:

Option A: Put models in models/ folder (recommended)

project/
├── models/
│   ├── user.py
│   └── post.py
└── jetbase/

Option B: Environment variable

export JETBASE_MODELS="./models/user.py,./models/post.py"

Option C: In env.py

# jetbase/env.py
model_paths = ["./models/user.py"]

What It Detects

Change Generates
New table CREATE TABLE / DROP TABLE
New column ALTER TABLE ADD COLUMN / DROP COLUMN
New index CREATE INDEX / DROP INDEX
New foreign key ADD CONSTRAINT / DROP CONSTRAINT

Example Output

Input model:

class Category(Base):
    __tablename__ = "categories"
    id = Column(Integer, primary_key=True)
    name = Column(String(100), nullable=False)

class Post(Base):
    __tablename__ = "posts"
    id = Column(Integer, primary_key=True)
    title = Column(String(255))
    category_id = Column(Integer, ForeignKey("categories.id"))

Generated migration:

-- upgrade

CREATE TABLE categories (
    id INTEGER NOT NULL PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

CREATE TABLE posts (
    id INTEGER NOT NULL PRIMARY KEY,
    title VARCHAR(255),
    category_id INTEGER,
    CONSTRAINT fk_post_category FOREIGN KEY (category_id) REFERENCES categories (id)
);


-- rollback

DROP TABLE posts;

DROP TABLE categories;

Feature 2: Simpler Async/Sync Mode

The Old Way (Confusing)

Before, async mode could come from:

  • ASYNC environment variable
  • async_mode in env.py
  • async_mode in config

If these disagreed, things broke.

The New Way (Simple)

Just use one environment variable:

# Sync mode (default)
export ASYNC=false
jetbase upgrade

# Async mode
export ASYNC=true
jetbase upgrade

# One-time override
ASYNC=true jetbase status

Values That Work

Value Mode
true, True, TRUE, 1, yes Async
false, False, FALSE, 0, no Sync
Not set Sync

URL Handling

In sync mode, Jetbase automatically removes async suffixes:

Original URL Becomes
postgresql+asyncpg://... postgresql://...
sqlite+aiosqlite:///db.db sqlite:///db.db

So this works in both modes:

sqlalchemy_url = "postgresql+asyncpg://user:pass@localhost:5432/db"

Feature 3: Run From Anywhere

Before: You had to be in the jetbase/ folder
After: Run from anywhere in your project

# All of these work now:
cd /project && jetbase status
cd /project/src && jetbase upgrade
cd /project/jetbase && jetbase status  # Still works

Breaking Changes

1. Remove async_mode from config

Before:

# jetbase/env.py
sqlalchemy_url = "postgresql+asyncpg://..."
async_mode = True

After:

# jetbase/env.py
sqlalchemy_url = "postgresql+asyncpg://..."
# async_mode removed - use ASYNC env var instead
export ASYNC=true  # Set this instead

2. New config option

If you use auto-migrations, add this to env.py:

model_paths = ["./models.py"]  # Or use JETBASE_MODELS env var

File Changes Summary

New Files (13 total)

File What It Does
jetbase/commands/make_migrations.py The make-migrations command
jetbase/engine/model_discovery.py Finds your SQLAlchemy models
jetbase/engine/schema_introspection.py Reads database schema
jetbase/engine/schema_diff.py Compares models vs database
jetbase/engine/sql_generator.py Creates SQL from differences
jetbase/engine/jetbase_locator.py Finds jetbase folder from anywhere
tests/unit/commands/test_make_migrations.py Tests for make-migrations
Plus 7 more test files Tests for new features

Modified Files (24 total)

File Main Changes
jetbase/cli/main.py Added make-migrations command
jetbase/config.py Removed async_mode, added model_paths
jetbase/database/connection.py Simplified async/sync handling
README.md Updated with new features
docs/ Updated all documentation

Migration Steps

If You Used Async Mode

  1. Remove async_mode = True/False from env.py
  2. Set ASYNC=true or ASYNC=false in your environment

If You Want Auto-Migrations

  1. Add model paths to env.py:

    model_paths = ["./models.py"]

    Or set JETBASE_MODELS=./models.py environment variable

  2. Or put models in models/ folder as now Jetbase autodetects models in model/ and models/ folder.


Testing

All 232 tests pass.

New tests cover:

  • Make-migrations command (18 tests)
  • Model discovery (17 tests)
  • Schema introspection (15 tests)
  • Schema comparison (14 tests)
  • SQL generation (18 tests)
  • Connection handling (14 tests)

Run tests:

pytest                    # All tests
pytest -v                 # Verbose output
pytest --cov=jetbase      # With coverage

Documentation

Updated files:

  • README.md - Complete rewrite
  • docs/commands/make-migrations.md - New command docs
  • docs/getting-started.md - Added auto-migrations guide
  • docs/database-connections.md - Added async/sync modes
  • docs/index.md - Updated with new features

@emiliano-gandini-outeda
Copy link
Author

emiliano-gandini-outeda commented Feb 4, 2026

I would love to see this PR merged, as the only thing stopping me from using FastAPI instead of Django is alembic.

I would also like to implement this tool in this amazing boilerplate

@emiliano-gandini-outeda
Copy link
Author

I removed upgrade (replaced by migrate) and improved env.py handling.

@jaz-alli
Copy link
Collaborator

jaz-alli commented Feb 5, 2026

Thanks for the PR! This is a large change and I have some thoughts, so I'll set aside some time soon to do a proper review

@emiliano-gandini-outeda
Copy link
Author

Cool! Feel free to AMA.

My own personal config slipped lol
After creating migrations, if you dont migrate and create another migrations, they would be duplicate.
The function only checked ASYNC env var, ignoring async_mode=True in env.py.
Also fixed get_config call to include sqlalchemy_url in keys to avoid TypeError.
@jaz-alli
Copy link
Collaborator

jaz-alli commented Feb 7, 2026

Hey! thanks a lot for putting the time into this and for the detailed PR! I can see the amount of work that went into it, and I appreciate you exploring new directions for Jetbase.

I get that you hate SQL and automatic SQLAlchemy ORM migrations would be a great path to go down for in that case.

That said, while I really appreciate the approach, I don’t think this PR is the right fit for Jetbase, since one of the project’s core goals is to by SQL only and intentionally not have anything to do with ORMs.

One of the main reasons that Jetbase was created as an alternative to Alembic was to be SQL first and specifically avoid any ORM interaction. I started out using SQLAlchemy ORM a while back for building apps, but as things grew more complex it became friction instead of helpful. Plain SQL was often clearer and more efficient. I've laid out some reasons below:

– Complex queries were easier to write and more efficient in plain SQL
– Data pipelines (e.g. S3 → Snowflake → Postgres) didn’t benefit from ORM abstractions
– I validated SQL queries and data directly in DB tools (DBeaver, Snowflake UI) and didn’t want to rewrite that SQL in an ORM
– Sometimes we queried another team’s database and didn’t want to maintain additional ORM models just for that

Jetbase was built with inspiration from other business focused database migration tools in the Java ecosystem such as Liquibase and Flyway, which are plain SQL only. But nothing like this existed for the Python ecosystem. So until I built Jetbase, I would have to jump into the Java ecosystem just for migrations when building out Python projects.

This is intentionally different from ORM-centric migration tools such as Alembic or Prisma migrations, which were built and maintained alongside their respective ORMs.

At the moment, Jetbase does have a SQLAlchemy dependency, but it is used only for database connections. That was a choice to move faster early on. Long-term, the goal is to remove SQLAlchemy entirely, as outlined in this issue:
#79

I think your approach could work really well as a fork or companion project that builds ORM-driven migration generation on top of Jetbase’s features that Alembic doesn't have, such as schema drift detection and a linear migration history.


On the async portion of the PR: the current description mentions multiple sources of truth for async configuration, but async_mode does not actually exist as a real Jetbase config. In practice, async drivers such as asyncpg should work as normal out of the box.


That said, some of the other ideas you mentioned such as running Jetbase commands from any directory and improving logging are definitely aligned with the project direction and worth exploring further.

Thanks again for the contribution!

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