Migrate from Datastore to Native Mode¶
Move your app from Firestore in Datastore mode to Firestore in Native mode, using Cendry as the bridge.
Prerequisites¶
- A GCP project running Firestore in Datastore mode
- Python >= 3.13
- Docker (for testing with emulators)
Install Cendry with Datastore support:
Step 1: Define your models¶
Map your existing Datastore Kinds to Cendry Model classes. Use the same Kind name as the collection= value — this name is shared between Datastore mode and Native mode.
from cendry import Cendry, Field, Model, field
class City(Model, collection="City"):
name: Field[str]
state: Field[str]
population: Field[int]
capital: Field[bool]
tags: Field[list[str]] = field(default_factory=list)
Collection naming
collection= is used as the Datastore Kind name and later as the Firestore collection name. Pick one convention and stick with it.
Step 2: Validate with Datastore backend¶
Connect Cendry to your existing Datastore data and verify models deserialize correctly.
from cendry import Cendry
from cendry.backends.datastore import DatastoreBackend
backend = DatastoreBackend(project="my-gcp-project")
db = Cendry(backend=backend)
# Read existing entities
cities = db.select(City).to_list()
for city in cities:
print(f"{city.name} ({city.state}): pop {city.population}")
# Fetch by ID
sf = db.get(City, "SF")
print(sf.name) # San Francisco
Test with the Datastore emulator first
Before pointing at production data, test with a local emulator:
Step 3: Run your app on Datastore backend¶
Replace your existing data access layer with Cendry. Your app logic uses the same Cendry API regardless of backend — get, save, select, batch, transaction all work.
# All the same API — only the backend changes
db.save(City(name="Portland", state="OR", population=650000, capital=False))
with db.batch() as b:
b.save(city1)
b.save(city2)
results = db.select(City, City.state == "CA").to_list()
TOCTOU race on create() and update()
Datastore has no atomic "create if not exists" or partial update. On the Datastore backend:
create()checks existence then writes — not atomic outside transactionsupdate()fetches, merges, then writes — not atomic outside transactions
Wrap these calls in transactions for safety:
Feature differences¶
Not all Cendry features are available on the Datastore backend:
| Feature | Datastore | Native |
|---|---|---|
| CRUD (get, save, delete, update) | Yes | Yes |
| Queries (filter, order, limit) | Yes | Yes |
| AND filters | Yes | Yes |
| OR filters | No | Yes |
| Batch writes | Yes | Yes |
| Transactions | Yes | Yes |
| Subcollections / parent | Yes (ancestor keys) | Yes |
| Collection group queries | No | Yes |
Real-time listeners (on_snapshot) |
No | Yes |
Async (AsyncCendry) |
No | Yes |
Transforms (Increment, SERVER_TIMESTAMP) |
No | Yes |
Optimistic locking (if_unchanged) |
No | Yes |
Document metadata (update_time, create_time) |
No | Yes |
Unsupported features raise a CendryError with a message guiding you to migrate.
Step 4: Migrate the database¶
Use Google's built-in migration tool to convert your database from Datastore mode to Native mode.
This is a one-way, irreversible operation
Once migrated, you cannot go back to Datastore mode. Test thoroughly with emulators before migrating production.
- Go to the Cloud Console → Firestore
- Click "Upgrade to Firestore in Native mode"
- Follow the prompts
See Google's migration documentation for details.
Step 5: Switch to Firestore backend¶
One line change:
# Before (Datastore mode)
from cendry.backends.datastore import DatastoreBackend
db = Cendry(backend=DatastoreBackend(project="my-gcp-project"))
# After (Native mode)
db = Cendry() # uses FirestoreBackend by default
All your models, queries, and app logic stay exactly the same.
Step 6: Unlock Native-only features¶
Now you can use everything Cendry offers:
from cendry import AsyncCendry, Increment, SERVER_TIMESTAMP
# Async support
async with AsyncCendry() as db:
city = await db.get(City, "SF")
# Real-time listeners
watch = db.on_snapshot(City, "SF", lambda instance, changes, time: ...)
# Collection group queries
all_neighborhoods = db.select_group(Neighborhood).to_list()
# Transforms
db.update(city, {"population": Increment(1000)})
# Optimistic locking
db.update(city, {"name": "SF"}, if_unchanged=True)