Defining Documents

A Document in coodie is a Python class that maps to a Cassandra table. It inherits from pydantic.BaseModel, so you get validation, serialisation, and type hints for free.

Basic Document

from coodie.sync import Document
from coodie.fields import PrimaryKey, ClusteringKey
from typing import Annotated, Optional
from uuid import UUID
from datetime import datetime

class BlogPost(Document):
    blog_id: Annotated[UUID, PrimaryKey()]
    published_at: Annotated[datetime, ClusteringKey(order="DESC")]
    title: str
    body: str
    author: Optional[str] = None
    views: int = 0

    class Settings:
        name = "blog_posts"
        keyspace = "my_ks"

This maps to the CQL table:

CREATE TABLE my_ks.blog_posts (
    blog_id uuid,
    published_at timestamp,
    title text,
    body text,
    author text,
    views int,
    PRIMARY KEY (blog_id, published_at)
) WITH CLUSTERING ORDER BY (published_at DESC);

The Settings Inner Class

Every Document can declare a Settings inner class to control table metadata:

Setting

Default

Description

name

Snake-cased class name

CQL table name

keyspace

Driver’s default keyspace

Target keyspace

class Product(Document):
    id: Annotated[UUID, PrimaryKey()]
    name: str

    class Settings:
        name = "products"          # Table name in Cassandra
        keyspace = "ecommerce"     # Keyspace (overrides the driver default)

If you omit Settings.name, coodie uses the snake-cased class name: BlogPostblog_post, HTTPRequesthttp_request.

Field Defaults

Because Document inherits from Pydantic’s BaseModel, you can use all the standard default-value patterns:

from pydantic import Field
from uuid import uuid4

class Order(Document):
    # Required field — must be provided at instantiation
    customer_name: str

    # Default value
    status: str = "pending"

    # Default factory — generates a new UUID each time
    id: Annotated[UUID, PrimaryKey()] = Field(default_factory=uuid4)

    # Optional field — can be None
    notes: Optional[str] = None

Pydantic Integration

Since Document extends pydantic.BaseModel, you get:

  • Validation — type mismatches raise ValidationError at instantiation

  • Serialisation.model_dump() returns a plain dict

  • Schema generation.model_json_schema() produces JSON Schema

# Pydantic validation in action
try:
    p = Product(id="not-a-uuid", name=42)
except Exception as e:
    print(e)  # Pydantic ValidationError with details

# Serialise to dict
p = Product(id=uuid4(), name="Widget")
print(p.model_dump())
# {'id': UUID('...'), 'name': 'Widget'}

Schema Sync

After defining a Document, call sync_table() to create or update the table in Cassandra:

# Sync — creates the table if it doesn't exist,
# or adds new columns if you've added fields
Product.sync_table()

# Async equivalent
await Product.sync_table()

sync_table() is idempotent — call it as many times as you like. It will not drop columns or change column types.

Counter Documents

For Cassandra counter tables, use CounterDocument:

from coodie.sync import CounterDocument
from coodie.fields import PrimaryKey, Counter

class PageViews(CounterDocument):
    page_url: Annotated[str, PrimaryKey()]
    views: Annotated[int, Counter()]
    unique_visitors: Annotated[int, Counter()]

    class Settings:
        name = "page_views"

Counter documents use increment() / decrement() instead of save(). See the CRUD guide for details.

Materialized Views

For Cassandra materialized views, use MaterializedView:

from coodie.sync import MaterializedView
from coodie.fields import PrimaryKey, ClusteringKey

class ProductsByCategory(MaterializedView):
    category: Annotated[str, PrimaryKey()]
    id: Annotated[UUID, ClusteringKey()]
    name: str
    price: float

    class Settings:
        name = "products_by_category"
        __base_table__ = "products"

Materialized views are read-only — save(), insert(), update(), and delete() will raise InvalidQueryError.

What’s Next?