CentauroDB Logo

Pydantic ModelsNo Migrations. Ever.

Store Pydantic models as JSON. Query them with plain SQL. Change your schema whenever you want — old data keeps working.

This is what you're doing right now

Every time your schema changes with a traditional ORM, this is the ceremony:
  1. Edit model class
  2. Run alembic revision --autogenerate -m "add location field"
  3. Review the generated migration
  4. Run alembic upgrade head
  5. Repeat for staging. Repeat for production.
  6. Hope nothing breaks.

With CentauroDB, you edit the model and you're done. No step 2. No step 3. No step 6. Old data still loads. New data uses the new schema. Views update with one call.

# That's it. Add a field, add a default. Done.
class Sensor(CentauroModel):
    __centauro_name__ = "Sensor"
    unit: str = ""
    location: str = "unknown"  # old rows get "unknown" automatically

What you get out of the box

Zero Migrations
Add, remove, or rename fields — no migration files, no ALTER TABLE, no downtime. Old data still loads. New data uses the new shape.
Auto SQL Views
One call turns your JSON into real SQL columns. Query with DBeaver, Metabase, Jupyter, or raw SELECT * FROM.
Built-in Time-Series
Sensor readings, financial ticks, metrics — TimeSeriesCollection stores them and gives you a Polars DataFrame in one line.
Pydantic v2 Native
Your models are standard Pydantic classes. Validation on every read and write. No new ORM to learn.
SQLite + PostgreSQL
Start with a single .db file. Move to PostgreSQL by changing one connection string. Same code, same API.
Async Ready
AsyncCollection drops into FastAPI and any async framework. No wrapper hacks, no thread pool juggling.

Four schema changes. Zero migrations.

Your model is live and the database is full of rows. Now change it. Each tab below is a schema change that would cost you an Alembic migration — here you edit the class, redeploy, and the old rows keep loading. The last lines of each example are the proof.

Old rows don't have the new key — Pydantic fills in the default on read.

# v1 — in production, thousands of rows already written
class Sensor(CentauroModel):
    __centauro_name__ = "Sensor"
    unit: str = ""

# v2 — add the field, give it a default. That's the whole change.
class Sensor(CentauroModel):
    __centauro_name__ = "Sensor"
    unit: str = ""
    location: str = "unknown"

# the old rows? Still load fine:
sensors = coll.read_objects(Sensor)
sensors[0].location  # "unknown" — filled in on read

Ready to drop the migrations?