Database Management Operations
All persistence is Azure Cosmos DB with MongoDB API via pymongo. There is no SQLAlchemy, no SQLite, no relational layer. Three Python classes wrap it; nothing instantiates MongoClient directly.
Layout
| File |
Class / role |
[src/database/dbfactory.py](../../../src/database/dbfactory.py) |
Connection helpers (getcosmosclient, getdatabaseclient, getdatabaseconfig) |
[src/database/dictionarydb.py](../../../src/database/dictionarydb.py) |
DictionaryDB — entries, words, phrases, paragraphs, pages |
[src/database/userdb.py](../../../src/database/userdb.py) |
UserDB — users, role/permissions, sessions, page-tracking |
[src/database/publicationmanager.py](../../../src/database/publicationmanager.py) |
PublicationManager — publication metadata + uploads/ filesystem |
Connection resolution order
[dbfactory.getcosmosclient()](../../../src/database/dbfactory.py#L62) tries, in order:
COSMOSMONGOCONNECTIONSTRING — direct MongoDB connection string. Preferred in production. (Older docs reference COSMOSDBCONNECTIONSTRING; that name is not read.)
- Managed Identity if
USEMANAGEDIDENTITY=true + COSMOSACCOUNTNAME are set ([dbfactory.py](../../../src/database/dbfactory.py#L20)).
COSMOSDBURI + COSMOSDBKEY — builds the connection string locally with URL-encoded key.
- Local MongoDB at
mongodb://localhost:27017/ as fallback.
Environment variables actually consulted:
| Var |
Used by |
COSMOSMONGOCONNECTION_STRING |
preferred path |
USEMANAGEDIDENTITY (true/false) |
managed-identity gate |
COSMOSACCOUNTNAME (default chuuk-dictionary-cosmos) |
both managed identity + URI build |
COSMOSDBURI, COSMOSDBKEY |
key-based auth |
retryWrites=False and appName=@<account>@ are required by Cosmos's MongoDB API and are baked into the generated connection string.
Database & collection names
From [getdatabaseconfig()](../../../src/database/db_factory.py#L130):
{
"database_name": "chuuk_dictionary",
"container_name": "dictionary_entries", # → DictionaryDB.dictionary_collection
"pages_container": "dictionary_pages", # → pages_collection
"words_container": "words", # → words_collection
"phrases_container": "phrases", # → phrases_collection
"paragraphs_container": "paragraphs", # → paragraphs_collection
"users_container": "users", # → UserDB.users_collection
}
DictionaryDB — actual API
The methods that actually exist (see [dictionarydb.py](../../../src/database/dictionarydb.py#L972)):
search_word(word: str) -> dict | None
search_words(query: str, limit: int = 50) -> list[dict]
add_word(word: str, translation: str, **meta) -> str
search_phrases(query: str, limit: int = 50) -> list[dict]
add_phrase(chuukese: str, english: str, **meta) -> str
- Plus direct collection access (
dictdb.dictionarycollection.find(...)) for ad-hoc queries.
There is no searchentries, bulkinsertentries, getall_entries, etc. Older skill docs invented those.
UserDB
[UserDB](../../../src/database/user_db.py#L11) handles auth-adjacent state:
getuser(email), upsertuser(email, role)
startsession(email) — issues a sessionid, invalidates prior active session for that email
issessionvalid(email, session_id) — single-active-session enforcement
trackpage(email, page) — appends to pagesaccessed, updates lastactivityat ([userdb.py](../../../src/database/userdb.py#L164))
- Schema fields:
email, role, sessionid, sessionstartat, lastactivityat, pagesaccessed, acceptedtermsat
PublicationManager
[PublicationManager](../../../src/database/publication_manager.py#L10) coordinates DB metadata + the filesystem under uploads/:
create_publication(title, author, ...) — writes Cosmos doc + creates uploads/<id>/ dir
addpage(pubid, file) — saves file, adds page metadata
getpublication(pubid), list_publications()
- Page metadata is persisted both in Cosmos and in a per-publication JSON sidecar ([publicationmanager.py](../../../src/database/publicationmanager.py#L18)) — keep them in sync if you mutate either directly.
Common patterns
from src.database.dictionary_db import DictionaryDB
from src.database.user_db import UserDB
dict_db = DictionaryDB() # Singleton-ish — instantiate once per worker
user_db = UserDB()
# Search (escape user input!)
results = dict_db.search_words(user_query, limit=50)
# Direct collection query when method doesn't fit
import re
pattern = re.escape(user_input)
rows = dict_db.dictionary_collection.find(
{"chuukese_word": {"$regex": pattern, "$options": "i"}},
limit=50,
)
# Insert with audit fields
from datetime import datetime, timezone
dict_db.dictionary_collection.insert_one({
"chuukese_word": word,
"english_translation": meaning,
"grammar_type": pos,
"confidence_score": 0.9,
"edited_by": user_email,
"created_at": datetime.now(timezone.utc),
})
Cosmos DB constraints
pymongo version is pinned in [requirements.txt](../../../requirements.txt) for Cosmos wire-protocol compatibility — don't bump unilaterally.
retryWrites=False is mandatory (already in connection string).
- RU budget matters: avoid full collection scans; prefer indexed
chuukeseword / englishtranslation queries.
- Cosmos's MongoDB API ignores some
$regex flags silently — case-insensitive search via $options: "i" is fine; lookahead/lookbehind are not.
- The factory's local-MongoDB fallback is for tests/dev only — production must have Cosmos credentials.
Pitfalls
- The 2-worker gunicorn setup means
DictionaryDB() is instantiated twice. Don't add per-instance caches and expect them to be coherent across requests.
- When adding a new collection, plumb it into
getdatabaseconfig() AND the DictionaryDB.init block so _collection attributes stay consistent.
- Managed-identity path requires the workload identity to have a Cosmos RBAC role assigned — see [docs/AZUREDEPLOYMENT.md](../../../docs/AZUREDEPLOYMENT.md).
- Renaming a collection in
getdatabaseconfig() does not rename the underlying Cosmos container — you must run an Azure-side migration.