Field Types & Annotations
coodie maps Python types to CQL types automatically. For most cases you just write standard Python type hints and coodie does the right thing.
Python → CQL Type Mappings
Scalar Types
Python Type |
CQL Type |
Notes |
|---|---|---|
|
|
Default string type |
|
|
32-bit signed integer |
|
|
32-bit IEEE 754 |
|
|
|
|
|
Raw binary data |
|
|
From |
|
|
From |
|
|
From |
|
|
From |
|
|
From |
|
|
From |
Collection Types
Python Type |
CQL Type |
Example |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Type Override Markers
Sometimes you need a CQL type that doesn’t map one-to-one to a Python type.
Use type override markers inside Annotated[]:
from coodie.fields import BigInt, SmallInt, TinyInt, VarInt, Double, Ascii, TimeUUID, Time, Frozen, Static
class SensorReading(Document):
sensor_id: Annotated[UUID, PrimaryKey()]
# Integer overrides
reading_big: Annotated[int, BigInt()] # CQL: bigint (64-bit)
reading_small: Annotated[int, SmallInt()] # CQL: smallint (16-bit)
reading_tiny: Annotated[int, TinyInt()] # CQL: tinyint (8-bit)
reading_var: Annotated[int, VarInt()] # CQL: varint (arbitrary precision)
# Float override
precise_value: Annotated[float, Double()] # CQL: double (64-bit IEEE 754)
# String override
code: Annotated[str, Ascii()] # CQL: ascii (US-ASCII only)
# UUID override
event_id: Annotated[UUID, TimeUUID()] # CQL: timeuuid (time-based UUID)
# Time override
sampled_at: Annotated[int, Time()] # CQL: time (nanoseconds since midnight)
Override Summary
Marker |
Python Type |
CQL Type |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Frozen Collections
Use Frozen() to wrap a collection type as frozen<...> in CQL. Frozen
collections are stored as a single serialised blob and can be used in
primary keys or as elements of other collections:
from coodie.fields import PrimaryKey, Frozen
class GeoPoint(Document):
id: Annotated[UUID, PrimaryKey()]
coordinates: Annotated[tuple[float, float], Frozen()] # frozen<tuple<float, float>>
tags: Annotated[set[str], Frozen()] # frozen<set<text>>
Key & Index Markers
These markers control how columns participate in the table’s primary key and indexing:
Marker |
Purpose |
Parameters |
|---|---|---|
|
Partition key column |
|
|
Clustering column |
|
|
Secondary index |
|
|
Counter column |
— |
|
Static column (shared across partition) |
— |
See Primary Keys, Clustering Keys & Indexes for detailed usage of keys and indexes.
Static Columns
In Cassandra, a static column is shared across all rows within the same partition. This is useful when you have data that belongs to the partition as a whole, not to individual clustering rows.
Use Static() to mark a column as static:
from coodie.fields import PrimaryKey, ClusteringKey, Static
class SensorReading(Document):
sensor_id: Annotated[str, PrimaryKey()]
reading_time: Annotated[str, ClusteringKey()]
sensor_name: Annotated[str, Static()] = "" # shared across partition
value: float = 0.0
This produces:
CREATE TABLE sensor_reading (
sensor_id text,
reading_time text,
sensor_name text STATIC,
value float,
PRIMARY KEY (sensor_id, reading_time)
);
Every row for the same sensor_id shares the same sensor_name value.
Updating sensor_name on any row updates it for all rows in that partition.
Note
Static columns require at least one clustering column in the table. A table with only a partition key and no clustering key cannot have static columns.
Combining Markers
You can combine multiple markers in a single Annotated[]:
# A TimeUUID that is also a primary key
event_id: Annotated[UUID, PrimaryKey(), TimeUUID()]
# A BigInt with a secondary index
population: Annotated[int, Indexed(), BigInt()]
Optional Fields
Use Optional[X] (or X | None on Python 3.10+) for nullable fields:
class Profile(Document):
user_id: Annotated[UUID, PrimaryKey()]
name: str # Required
bio: Optional[str] = None # Optional, defaults to None
age: int | None = None # Same as Optional[int]
When a field is Optional and the stored value is NULL in Cassandra,
coodie returns None.
User-Defined Types
coodie supports Cassandra User-Defined Types via the UserType base class.
UDT fields are automatically mapped to frozen<type_name> in CQL:
from coodie.usertype import UserType
class Address(UserType):
street: str
city: str
zipcode: int
class Profile(Document):
user_id: Annotated[UUID, PrimaryKey()]
home: Address # frozen<address>
offices: list[Address] = [] # list<frozen<address>>
contacts: dict[str, Address] = {} # map<text, frozen<address>>
UDTs are always frozen — the Frozen() marker is accepted but redundant.
Nested UDTs (a UDT containing another UDT) are fully supported.
See User-Defined Types (UDT) for the full UDT guide.
What’s Next?
User-Defined Types (UDT) — full UDT guide with nested types and sync_type()
Primary Keys, Clustering Keys & Indexes — primary keys, clustering keys, and secondary indexes
CRUD Operations — save, insert, update, delete, and query operations