CentauroDB Logo
Core Concepts

Collections

Store, read, update, and query your models with Collection and TimeSeriesCollection.

A collection is a named container backed by a database table. You'll interact with collections for every read and write operation in CentauroDB. This guide walks you through the complete workflow — from storing your first object to building filtered queries.

CentauroDB provides two collection types:

  • Collection — for regular models (CentauroModel)
  • TimeSeriesCollection — for models with time-series data (CentauroModelSeries)

Setting up a collection

from centaurodb import Engine, Collection, CentauroModel

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

The collection name must match [a-z][a-z0-9_]* — lowercase letters, digits, and underscores, starting with a letter. The backing table (inventory_objects) is created automatically on first use. Creating a Collection with the same name twice is safe and idempotent.

Choosing a collection name

The collection name becomes a table prefix in the database — {name}_objects, {name}_values (for time-series), and {name}_view_*. Because a single collection can hold many model types (distinguished by __centauro_name__), the name should describe the domain or project, not a single model type.

# Good — domain-level names
Collection(engine, "inventory")     # inventory_objects
Collection(engine, "research")      # research_objects
Collection(engine, "trading")       # trading_objects

# Bad — too narrow, too vague, or redundant
Collection(engine, "notes")         # what happens when you add Tags, Users?
Collection(engine, "data")          # meaningless
Collection(engine, "my_objects")    # table becomes my_objects_objects

One collection or many?

Start with one collection per project. Multiple model types in one collection work well — __centauro_name__ separates them, and views let you query each type independently with SQL.

You must use separate collections when mixing CentauroModel with CentauroModelSeries, because Collection and TimeSeriesCollection enforce their respective types.

Beyond that, consider an additional collection when you have unrelated domains in the same database, extreme write-volume skew, or independent data retention needs. The Performance Guide covers each scenario in detail with examples.

Do not split collections for organizational tidiness alone — __centauro_name__ and views already separate model types logically. Parent–child relationships (parent_ref, write_child, children_of) only work within a single collection. Splitting related models across collections breaks them.

Storing objects

Let's define a model and write some data:

class Product(CentauroModel):
    __centauro_name__ = "Product"
    name: str = ""
    category: str = ""
    price: float = 0.0
    in_stock: bool = True

laptop = Product(name="ThinkPad X1", category="electronics", price=1299.99)
coll.write_object(laptop)

print(laptop.row.id)          # 1 — auto-assigned after write
print(laptop.row.write_time)  # datetime of the insert

After write_object, the object gains a row attribute with database metadata (id, name, write_time, edit_time).

Batch writes

Insert multiple objects in a single atomic transaction. If any object fails, none are persisted:

coll.write_objects([
    Product(name="MacBook Air", category="electronics", price=999.99),
    Product(name="Standing Desk", category="furniture", price=549.00),
    Product(name="Monitor Arm", category="furniture", price=89.99),
])

Re-inserting an object

Calling write_object() on an already-persisted object raises ObjectAlreadyPersistedError. You have two options:

# Option 1: Update the existing row
laptop.price = 1199.99
coll.update_object(laptop)

# Option 2: Insert a duplicate row (new id)
coll.write_object(laptop, duplicate=True)

Reading objects back

By id

Fetch a single object when you know its row id:

loaded = coll.read_object_by_id(1, Product)
print(loaded.name)   # "ThinkPad X1"
print(loaded.price)  # 1299.99

Pass raw=True to get the database row as a dict instead of a hydrated model — useful for debugging or when you need the raw JSON:

row = coll.read_object_by_id(1, Product, raw=True)
print(row["meta"])  # '{"name":"ThinkPad X1","category":"electronics",...}'

The latest object

Retrieve the most recently edited object of a given type:

latest = coll.read_latest_object(Product)
print(latest.name)  # the last-updated Product

Returns None if no objects of that type exist. You can also filter:

# Latest electronics product
latest_electronics = coll.read_latest_object(
    Product.fields.category == "electronics"
)

Querying with conditions

This is where CentauroDB's query DSL shines. Use Model.fields to build type-safe conditions:

# All products
all_products = coll.read_objects(Product)

# Filter by equality
electronics = coll.read_objects(
    Product.fields.category == "electronics"
)

# Combine multiple conditions (AND)
cheap_electronics = coll.read_objects(
    Product.fields.category == "electronics",
    Product.fields.price < 1000,
)

Pattern matching with like

Use SQL LIKE patterns for partial string matches:

# Products with names starting with "Mac"
macs = coll.read_objects(
    Product.fields.name.like("Mac%")
)

# Products with "Desk" anywhere in the name
desks = coll.read_objects(
    Product.fields.name.like("%Desk%")
)

Matching a set of values with is_in

Check if a field matches any value in a list:

# Products in specific categories
results = coll.read_objects(
    Product.fields.category.is_in(["electronics", "furniture"])
)

OR conditions

Combine conditions with | for OR logic:

# Cheap OR out-of-stock products
results = coll.read_objects(
    (Product.fields.price < 100) | (Product.fields.in_stock == False),
)

# OR + AND: cheap or premium electronics
results = coll.read_objects(
    (Product.fields.price < 100) | (Product.fields.price > 1000),
    Product.fields.category == "electronics",
)

Aggregates: max_field / min_field

Find the row(s) with the maximum or minimum value of a numeric or datetime field:

# Most expensive product
priciest = coll.read_objects(Product, max_field=Product.fields.price)

# Cheapest electronics product
cheapest = coll.read_objects(
    Product.fields.category == "electronics",
    min_field=Product.fields.price,
)

# Oldest product by write_time
oldest = coll.read_objects(Product, min_field=Product.fields.write_time)
max_field and min_field are mutually exclusive — pass only one at a time. They work with int, float, date, and datetime fields.

Counting and pagination

Count matching objects without loading any rows:

total = coll.count_objects(Product)
in_stock = coll.count_objects(Product.fields.in_stock == True)

# Equivalent via the aggregate API
total = coll.aggregate(Product, count=True)

Page through large result sets with limit / offsetcount_objects gives you the total for the pager:

page_size = 20
total = coll.count_objects(Product)

page_1 = coll.read_objects(Product, limit=page_size)
page_3 = coll.read_objects(Product, limit=page_size, offset=2 * page_size)

offset requires limit — pagination without an upper bound raises ValueError.

As of 0.9.0 only count=True is implemented in aggregate()sum, avg, and group_by are reserved and raise NotImplementedError. For SQL aggregations today, create a view and query it with sql_select().

Updating objects

Modify fields on a loaded object and persist the changes. The edit_time is updated automatically; write_time is preserved:

loaded = coll.read_object_by_id(1, Product)
loaded.price = 1099.99
loaded.in_stock = False
coll.update_object(loaded)
Calling update_object() on an object that hasn't been written raises ObjectNotPersistedError.

Deleting objects

Remove a single object by passing the persisted instance:

coll.delete_object(loaded)
# loaded is no longer persisted — can be re-written if needed

Delete multiple objects with conditions (same calling styles as read_objects):

# Delete all furniture
deleted_count = coll.delete_objects(
    Product.fields.category == "furniture"
)
print(deleted_count)  # number of rows removed

# Delete all products of a given type
coll.delete_objects(Product)
delete_object() raises ObjectNotPersistedError if the object hasn't been written yet. Deleting a parent that still has children linked via parent_ref raises ParentHasChildrenError under the default on_delete="restrict" — see Deleting parents and children.

Refreshing from the database

If another process might have updated a row, refresh your in-memory copy:

coll.refresh(loaded)
print(loaded.price)  # reflects the latest database state

Multiple model types in one collection

You can store different model types in the same collection. CentauroDB uses __centauro_name__ to distinguish between them:

class Supplier(CentauroModel):
    __centauro_name__ = "Supplier"
    company: str = ""
    contact: str = ""

# Same collection — both types share inventory_objects
coll.write_object(Product(name="Keyboard", category="electronics"))
coll.write_object(Supplier(company="Acme", contact="john@acme.com"))

# Read back by specifying the model class
product = coll.read_object_by_id(1, Product)
supplier = coll.read_object_by_id(2, Supplier)

# Query only products — __centauro_name__ filter is automatic
all_products = coll.read_objects(Product)
All models in a collection share the same underlying _objects table. The __centauro_name__ filter is applied automatically whenever you query by model class.

TimeSeriesCollection

Use TimeSeriesCollection when your models carry time-series data (CentauroModelSeries). It creates both _objects and _values tables, and adds values_mode control on updates.

from centaurodb import TimeSeriesCollection, CentauroModelSeries

class WeatherStation(CentauroModelSeries):
    __centauro_name__ = "WeatherStation"
    city: str = ""

stations = TimeSeriesCollection(engine, "stations")

TimeSeriesCollection shares the same core API — write, read, query, update, views, indexes, relations — with additional time-series features like hydrate_values and values_mode.

See the Time-Series guide for writing values, Polars integration, update modes, and multi-series patterns. For the full API, see TimeSeriesCollection API.