CentauroDB Logo
Core Concepts

Schema Evolution

Add, remove, and rename fields without migrations — just update your Python class and redeploy.

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.

Unlike traditional ORMs, there are no migration files to create, review, or apply. Just update your Python class and re-run your application.

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.

Choose your defaults thoughtfully — they represent the value old data will appear to have. An empty string, 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 as temperature=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

OperationWhat you doWhy it works
Add fieldAdd field with defaultPydantic fills the default on old rows
Remove fieldDelete the fieldPydantic ignores unknown JSON keys
Rename fieldUse renamed_from()Pydantic AliasChoices reads old names
Rename classKeep same __centauro_name__DB identity is independent of class name
For the full renamed_from() parameter reference and AliasChoices details, see the API reference.