CentauroDB Logo
Core Concepts

Models

Define your data with CentauroModel and CentauroModelSeries — the foundation of CentauroDB.

Every piece of data in CentauroDB is a Pydantic model. You define your schema as a Python class, and CentauroDB handles the rest — serialization, storage, versioning, and deserialization. This guide covers how to define models correctly and which base class to choose.

Defining your first model

Inherit from CentauroModel and set a __centauro_name__:

from centaurodb import CentauroModel

class Sensor(CentauroModel):
    __centauro_name__ = "Sensor"
    unit: str = ""
    location: str = ""
    threshold: float = 100.0

That's it — this model is ready to be stored in a Collection.

The three rules

CentauroDB enforces three constraints at class-definition time. Break any of them and you get a clear error immediately — not a runtime surprise weeks later.

Every model needs a __centauro_name__

This ClassVar[str] is the model's stable identity in the database — it's stored in the name column and used to filter rows in views and name-based queries. Without it, renaming a Python class would silently change the stored name, causing views to miss old rows with no error or warning.

class SensorReading(CentauroModel):
    __centauro_name__ = "Sensor"   # DB identity stays "Sensor"
    unit: str = ""

# Even after renaming the class, old rows still match
# because __centauro_name__ hasn't changed

All fields must have defaults

Without defaults, old rows that lack a new field would fail to load. Defaults make adding fields safe — old data simply gets the default value on read. A field without a default raises TypeError at class definition time.

class Sensor(CentauroModel):
    __centauro_name__ = "Sensor"
    unit: str = ""              # OK — default = ""
    location: str = "unknown"   # OK — default = "unknown"
    pages: int | None = None    # OK — Optional with None
    rating: float               # TypeError at class definition

extra="forbid" is banned

Old rows may contain fields that no longer exist in your current model. Pydantic needs to silently ignore them, so extra="forbid" would cause read failures. CentauroDB rejects it at class definition time.

These three rules are what make zero-migration schema evolution possible. They're validated when Python loads your class, so mistakes surface immediately.

Working with stored objects

After writing or reading an object, it carries database metadata in the row attribute:

from centaurodb import Engine, Collection

engine = Engine("monitoring.db")
coll = Collection(engine, "monitoring")

obj = Sensor(unit="Celsius", location="pelion")
coll.write_object(obj)

print(obj.row.id)          # int — auto-incremented primary key
print(obj.row.name)        # "Sensor" — the __centauro_name__
print(obj.row.write_time)  # datetime — when first written
print(obj.row.edit_time)   # datetime — when last updated

See RowFields API for the complete field list.

Validation on assignment

By default, CentauroModel validates field values whenever you set an attribute. This catches type errors early:

sensor = Sensor(unit="Celsius")
sensor.threshold = "oops"  # raises ValidationError immediately

For performance-critical code (e.g., ingesting thousands of objects in a tight loop), you can opt out:

from pydantic import ConfigDict

class FastSensor(CentauroModel):
    __centauro_name__ = "FastSensor"
    model_config = ConfigDict(validate_assignment=False, populate_by_name=True)
    value: float = 0.0
Always include populate_by_name=True when overriding model_config. Omitting it breaks renamed_from() — see the Performance guide for details.

Choosing between CentauroModel and CentauroModelSeries

Use CentauroModel when your objects represent entities with metadata only — configuration, records, documents, settings:

class Config(CentauroModel):
    __centauro_name__ = "Config"
    theme: str = "dark"
    language: str = "en"

Use CentauroModelSeries when each object also carries a series of timestamped numeric values — sensor readings, stock prices, metrics:

from centaurodb import CentauroModelSeries, CentauroValues

class WeatherStation(CentauroModelSeries):
    __centauro_name__ = "WeatherStation"
    city: str = ""
    elevation: float = 0.0

CentauroModelSeries extends CentauroModel with a values list and a .df property for Polars DataFrame access. Each instance tracks one measurement series. Metadata lives in JSON, values live in a separate normalized table.

The values field accepts either a list of CentauroValues or a Polars DataFrame with time and value columns:

import polars as pl

df = pl.DataFrame({
    "time": ["2025-06-01T08:00:00", "2025-06-01T09:00:00"],
    "value": [22.5, 24.1],
})
station = WeatherStation(city="Sparta", values=df)
See Time-Series for the complete guide on writing values, update modes, and Polars integration.

CentauroValues

A single time-series data point with two fields:

from centaurodb import CentauroValues

point = CentauroValues(
    time="2025-06-01T08:00:00",  # ISO timestamp (auto-coerced by Pydantic)
    value=22.5                   # numeric value
)

If you need multiple series per entity (e.g., temperature and humidity for the same station), create a separate subclass and object for each. The combination of (object_id, time) forms the composite primary key in the values table.

Nested models

Not all related data needs its own collection entry. If the nested data is small, bounded, and always loaded with its parent, embed it as a plain Pydantic model inside the JSON blob:

from pydantic import BaseModel

class Preferences(BaseModel):
    theme: str = "dark"
    language: str = "en"

class User(CentauroModel):
    __centauro_name__ = "User"
    name: str = ""
    prefs: Preferences = Preferences()   # embedded in the parent's JSON

When the child needs its own lifecycle — independent queries, growing over time, visible in views — use parent_ref instead.

See Relations — parent_ref vs nested models for a detailed comparison and rules of thumb.

Renaming fields safely

Use renamed_from() when you need to rename a field without breaking old data. See Schema Evolution for the full guide.

from centaurodb import renamed_from

class Sensor(CentauroModel):
    __centauro_name__ = "Sensor"
    temperature: float = renamed_from("temp", default=0.0)
    # Old JSON {"temp": 42.0} reads correctly as temperature=42.0