Schema Evolution
Zero-migration schema evolution is CentauroDB's core design goal. You change your Python model, and old data keeps working. No migration files, no ALTER TABLE, no downtime. This guide shows you each operation step by step.
CentauroDB enforces guardrails at class-definition time that make this safe by default. Each of the operations below explains what happens under the hood and why it works.
Adding a field
Just add the field with a default value. Old rows don't have the new key in their JSON — Pydantic fills in the default on read.
# v1 — your model today
class Sensor(CentauroModel):
__centauro_name__ = "Sensor"
unit: str = ""
# v2 — one week later, you need a location
class Sensor(CentauroModel):
__centauro_name__ = "Sensor"
unit: str = ""
location: str = "unknown" # old rows get "unknown" on read
Why it works: CentauroDB requires every field to have a default. When Pydantic deserializes old JSON that lacks "location", it uses the default value. No database change needed.
0, False, or None are common choices depending on the field semantics.Removing a field
Delete the field from your class. Old rows still store the key in their JSON, but Pydantic silently ignores unknown keys.
# v1
class Sensor(CentauroModel):
__centauro_name__ = "Sensor"
unit: str = ""
location: str = ""
# v2 — location is no longer needed
class Sensor(CentauroModel):
__centauro_name__ = "Sensor"
unit: str = ""
Why it works: CentauroDB bans extra="forbid" on models. Pydantic's default behavior is to silently ignore extra keys in JSON, so old rows with "location" load without error.
The old data remains in the JSON blob but is invisible to your Python code. If you later re-add a field with the same name, the old values reappear.
Renaming a field
Use renamed_from() to create an alias chain. Old JSON reads correctly under the new name, and new writes use the new name:
from centaurodb import renamed_from
# v1: field was called "temp"
# v2: renamed to "temperature"
class Sensor(CentauroModel):
__centauro_name__ = "Sensor"
temperature: float = renamed_from("temp", default=0.0)
What happens:
- Old JSON
{"temp": 42.0}→ reads astemperature=42.0 - New writes serialize as
{"temperature": 42.0} - SQL views use
COALESCE(json_extract(meta, '$.temperature'), json_extract(meta, '$.temp'))to read both
After renaming, refresh the view to update the SQL column:
coll.refresh_view("dashboard", Sensor)
Renaming more than once
renamed_from() accepts multiple old names for fields that have been renamed across several versions:
# v1: "temp" → v2: "temperature" → v3: "temp_celsius"
class Sensor(CentauroModel):
__centauro_name__ = "Sensor"
temp_celsius: float = renamed_from("temp", "temperature", default=0.0)
CentauroDB and Pydantic try each name in order until one matches. The full alias chain is preserved in views via COALESCE.
Renaming a class
__centauro_name__ decouples your Python class name from the database identity. Rename the class freely — the stored name stays the same:
# v1
class Sensor(CentauroModel):
__centauro_name__ = "Sensor"
unit: str = ""
# v2 — class renamed, DB identity unchanged
class SensorReading(CentauroModel):
__centauro_name__ = "Sensor" # old rows still match
unit: str = ""
Why it works: CentauroDB filters rows by __centauro_name__, not the Python class name. As long as the __centauro_name__ value matches, old and new rows are read correctly.
Combining operations
You can apply multiple schema changes at once:
# v1
class Sensor(CentauroModel):
__centauro_name__ = "Sensor"
temp: float = 0.0
old_field: str = ""
# v2 — rename field, add new field, remove old_field
class Sensor(CentauroModel):
__centauro_name__ = "Sensor"
temperature: float = renamed_from("temp", default=0.0)
location: str = "unknown"
# old_field removed — Pydantic ignores it in old rows
Then refresh your views:
coll.refresh_view("dashboard", Sensor)
Summary
| Operation | What you do | Why it works |
|---|---|---|
| Add field | Add field with default | Pydantic fills the default on old rows |
| Remove field | Delete the field | Pydantic ignores unknown JSON keys |
| Rename field | Use renamed_from() | Pydantic AliasChoices reads old names |
| Rename class | Keep same __centauro_name__ | DB identity is independent of class name |