Migrating from cqlengine
This guide walks you through converting a cassandra.cqlengine application
to coodie. coodie replaces cqlengine’s custom column classes with standard
Python type annotations and Pydantic v2, giving you validation, serialisation,
and first-class async support out of the box.
Core Concepts Mapping
cqlengine |
coodie |
Notes |
|---|---|---|
|
|
— |
|
|
Use Python type annotations |
|
|
— |
|
|
Called on the class itself |
Model Definition
cqlengine
import uuid
from datetime import datetime, timezone
from cassandra.cqlengine import columns
from cassandra.cqlengine.models import Model
class Product(Model):
__table_name__ = "products"
__keyspace__ = "catalog"
__default_ttl__ = 86400 # table-level TTL (seconds)
__options__ = {"gc_grace_seconds": 864000} # table options
# — keys —
id = columns.UUID(primary_key=True, default=uuid.uuid4)
category = columns.Text(primary_key=True, partition_key=True)
created_at = columns.DateTime(
primary_key=True,
clustering_order="DESC",
default=lambda: datetime.now(timezone.utc),
)
# — scalar types —
name = columns.Text(required=True)
sku = columns.Ascii()
price = columns.Float()
weight = columns.Double()
quantity = columns.Integer()
total_sold = columns.BigInt(default=lambda: 0)
rating = columns.SmallInt()
flags = columns.TinyInt()
is_active = columns.Boolean(default=lambda: True)
cost = columns.Decimal()
# — temporal / binary / network —
revision_id = columns.TimeUUID(default=uuid.uuid1)
launch_date = columns.Date()
description = columns.Text(required=False)
thumbnail = columns.Blob()
origin_ip = columns.Inet()
# — indexed columns —
brand = columns.Text(index=True)
supplier = columns.Text(index=True)
# — collections —
tags = columns.List(columns.Text)
warehouses = columns.Set(columns.Text)
metadata = columns.Map(columns.Text, columns.Text)
dimensions = columns.Tuple(columns.Float, columns.Float, columns.Float)
coodie
from datetime import date, datetime, timezone
from decimal import Decimal
from ipaddress import IPv4Address
from typing import Annotated, Optional
from uuid import UUID, uuid1, uuid4
from pydantic import Field
from coodie.sync import Document # or coodie.aio for async
from coodie.fields import (
Ascii, BigInt, ClusteringKey, Double, Indexed,
PrimaryKey, SmallInt, TimeUUID, TinyInt,
)
class Product(Document):
# — keys —
id: Annotated[UUID, PrimaryKey(partition_key_index=0)] = Field(
default_factory=uuid4
)
category: Annotated[str, PrimaryKey(partition_key_index=1)]
created_at: Annotated[datetime, ClusteringKey(order="DESC")] = Field(
default_factory=lambda: datetime.now(timezone.utc)
)
# — scalar types —
name: str
sku: Annotated[str, Ascii()] = ""
price: float = 0.0
weight: Annotated[float, Double()] = 0.0
quantity: int = 0
total_sold: Annotated[int, BigInt()] = 0
rating: Annotated[int, SmallInt()] = 0
flags: Annotated[int, TinyInt()] = 0
is_active: bool = True
cost: Optional[Decimal] = None
# — temporal / binary / network —
revision_id: Annotated[UUID, TimeUUID()] = Field(default_factory=uuid1)
launch_date: Optional[date] = None
description: Optional[str] = None
thumbnail: Optional[bytes] = None
origin_ip: Optional[IPv4Address] = None
# — indexed columns —
brand: Annotated[str, Indexed()] = ""
supplier: Annotated[str, Indexed()] = ""
# — collections —
tags: list[str] = Field(default_factory=list)
warehouses: set[str] = Field(default_factory=set)
metadata: dict[str, str] = Field(default_factory=dict)
dimensions: Optional[tuple[float, float, float]] = None
class Settings:
name = "products"
keyspace = "catalog"
__default_ttl__ = 86400
__options__ = {"gc_grace_seconds": 864000}
Key differences:
Model→Document— inherit fromcoodie.sync.Document(orcoodie.aio.Documentfor async).columns.*→ Python type annotations — no special column classes needed.__table_name__/__keyspace__→Settingsinner class — table metadata moves to a nestedSettingsclass.required=True→ no default value — a field without a default is required by Pydantic.required=False→Optional[T] = None— nullable fields useOptionalwith aNonedefault.default=callable→Field(default_factory=callable)— Pydantic usesField(default_factory=...)for callable defaults.columns.BigInt()→Annotated[int, BigInt()]— sub-types ofintandfloatuseAnnotatedmarkers.columns.TimeUUID()→Annotated[UUID, TimeUUID()]—timeuuidrequires a marker onUUID.columns.Set(columns.Text)→set[str]— collections use native Python generics.columns.Tuple(...)→tuple[float, float, float]— tuples use standard tuple syntax.Composite partition key — use
PrimaryKey(partition_key_index=N)to define multi-column partition keys.Clustering order — use
ClusteringKey(order="DESC")instead ofclustering_order="DESC".Table options —
__default_ttl__and__options__move into theSettingsclass.Async is the same model — only the import changes (
coodie.aioinstead ofcoodie.sync).
Connection Setup
cqlengine
from cassandra.cqlengine import connection
connection.setup(["127.0.0.1"], "catalog", protocol_version=4)
coodie (sync)
from coodie.sync import init_coodie
init_coodie(hosts=["127.0.0.1"], keyspace="catalog")
coodie (async)
from coodie.aio import init_coodie
await init_coodie(hosts=["127.0.0.1"], keyspace="catalog")
Table Sync
cqlengine
from cassandra.cqlengine.management import sync_table
sync_table(Product)
coodie
Product.sync_table() # sync
await Product.sync_table() # async
In coodie, sync_table() is a class method on the Document itself — no
separate management module needed.
CRUD Operations
Create / Insert
Operation |
cqlengine |
coodie (sync) |
coodie (async) |
|---|---|---|---|
Upsert |
|
|
|
Insert if not exists |
|
|
|
With TTL |
|
|
|
Read / Query
Operation |
cqlengine |
coodie (sync) |
coodie (async) |
|---|---|---|---|
Get all |
|
|
|
Filter |
|
|
|
Get one |
|
|
|
Get one or None |
|
|
|
Count |
|
|
|
Limit |
|
|
|
Order by |
|
|
|
Allow filtering |
|
|
|
Update
Operation |
cqlengine |
coodie (sync) |
coodie (async) |
|---|---|---|---|
Instance update |
|
|
|
Bulk update |
|
|
|
Delete
Operation |
cqlengine |
coodie (sync) |
coodie (async) |
|---|---|---|---|
Instance delete |
|
|
|
Bulk delete |
|
|
|
Column Type Reference
cqlengine |
coodie (Python type annotation) |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
See Field Types & Annotations for the full list of type annotations and markers.
Column Options
cqlengine Option |
coodie Equivalent |
|---|---|
|
|
|
|
|
|
|
|
|
Field has no default value |
|
|
|
|
|
|
See Primary Keys, Clustering Keys & Indexes for detailed usage of keys and indexes.
Batch Operations
cqlengine
from cassandra.cqlengine.query import BatchQuery
with BatchQuery() as b:
Product.batch(b).create(id=uuid4(), name="A", ...)
Product.batch(b).create(id=uuid4(), name="B", ...)
coodie
from coodie.sync import BatchQuery # or coodie.aio.AsyncBatchQuery
with BatchQuery() as batch:
Product(id=uuid4(), name="A", ...).save(batch=batch)
Product(id=uuid4(), name="B", ...).save(batch=batch)
Real-World Example: scylladb/argus
For a detailed, end-to-end migration walkthrough — covering 5 Argus-inspired models (User, TestRun, Notification, Event, Comment) and 7 operation patterns — see the dedicated page:
What’s Better in coodie
coodie is not just a 1:1 port of cqlengine — it improves the developer experience in several ways:
Pydantic v2 validation — every field is validated at instantiation, with clear error messages. No more silent type coercion.
Standard Python type hints — your IDE autocompletes fields, catches typos, and understands your models. No more
columns.Text()— juststr.First-class async support — the same model works with both sync and async drivers. Add
awaitin front of terminal methods, and you’re done.Pluggable drivers — switch between
scylla-driver(cassandra-driver fork) andacsylla(C++ async driver) without changing model code.Schema sync returns CQL —
sync_table()returns the list of CQL statements it plans to execute, so you can review or log them.
Migrating User-Defined Types (UDTs)
coodie provides full UDT support via the UserType base class.
Before (cqlengine)
from cassandra.cqlengine.usertype import UserType
from cassandra.cqlengine import columns
from cassandra.cqlengine.management import sync_type
class Address(UserType):
__type_name__ = "address"
street = columns.Text()
city = columns.Text()
zipcode = columns.Integer()
class User(Model):
id = columns.UUID(primary_key=True)
home = columns.UserDefinedType(Address)
others = columns.List(columns.UserDefinedType(Address))
# Must sync types manually in dependency order
sync_type("my_ks", Address)
sync_table(User)
After (coodie)
from coodie.usertype import UserType
from coodie.sync import Document
from coodie.fields import PrimaryKey
class Address(UserType):
street: str
city: str
zipcode: int
class Settings:
__type_name__ = "address" # optional — defaults to snake_case
class User(Document):
id: Annotated[UUID, PrimaryKey()]
home: Address # auto-detected as frozen<address>
others: list[Address] = [] # list<frozen<address>>
# sync_type() auto-resolves dependencies
Address.sync_type()
User.sync_table()
Key differences:
No
columns.UserDefinedType()wrapper — just use theUserTypeclass directly as a type annotation.No
columns.*field declarations — use standard Python type annotations.Automatic dependency resolution —
sync_type()recursively syncs nested UDT dependencies.Settings.__type_name__instead of class-level__type_name__.
See the User-Defined Types (UDT) guide for full details.
cqlengine Features Not Yet in coodie
The following cqlengine features are not available in coodie today. If your application relies on any of them, you will need a workaround (often raw CQL via the driver) or wait for a future coodie release.
cqlengine Feature |
Notes |
|---|---|
Static columns — |
Not implemented. Use a separate table or denormalise the static data. |
|
Use |
|
No LIKE queries. SASI/SAI pattern matching ( |
Token-range queries — |
No token-aware paging. Use |
Per-model |
coodie has a single global driver registry. Use separate |
|
Only simple keyspace creation via |
Counter columns on regular models |
Counter columns require inheriting from |
Common Gotchas
No
Model.objectsattribute. cqlengine usesModel.objects.filter(...). In coodie, useModel.find(...)directly on the class.No
Model.create()class method. Instead, instantiate the model and call.save():Product(id=uuid4(), name="Widget").save().__table_name__moves toSettings. Don’t put table metadata as class attributes. Use the innerSettingsclass:class Settings: name = "my_table" keyspace = "my_ks"
required=Trueis the default. In cqlengine, fields withoutrequired=Trueare optional. In coodie (via Pydantic), a field without a default value is required. Add= NoneorOptional[T] = Noneto make a field optional.Async needs
await. If you import fromcoodie.aio, all terminal operations (save(),delete(),get(),find().all(), etc.) must be awaited. The model definition itself is identical.sync_table()is a class method. No separatemanagement.sync_table()function — callProduct.sync_table()on the Document class directly.
Migration Checklist
Use this checklist when converting a cqlengine application to coodie:
[ ] Replace imports:
cassandra.cqlengine→coodie/coodie.sync/coodie.aio[ ] Convert models:
Model→Document;columns.*→ Python type annotations withAnnotatedmarkers[ ] Convert column options:
primary_key=True→PrimaryKey();index=True→Indexed(); etc.[ ] Convert
__table_name__/__keyspace__: Move to innerSettingsclass[ ] Convert connection setup:
connection.setup()→init_coodie()[ ] Convert table sync:
sync_table(Model)→Model.sync_table()[ ] Convert creates:
Model.create(...)→Model(...).save()[ ] Convert queries:
Model.objects.filter(...)→Model.find(...)[ ] Convert
objects.get():Model.objects.get(...)→Model.get(...)[ ] Convert batch operations:
BatchQuery()context manager → coodieBatchQuery()with.save(batch=batch)[ ] Handle async: If migrating to async, add
awaitbefore all Document and QuerySet terminal methods[ ] Check for unsupported features: Review the feature gaps table — if you use static columns, plan workarounds
[ ] Migrate UDTs: Convert
UserType+columns.*toUserType+ type annotations; replacesync_type()withAddress.sync_type()(see above)[ ] Test thoroughly: Run your existing test suite against coodie to verify parity
What’s Next?
Quick Start — get started with coodie in 60 seconds
Defining Documents — learn how Document classes work
Field Types & Annotations — all the type annotations you can use
CRUD Operations — the full CRUD reference