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
Connection resolution order
db_factory.get_cosmos_client() tries, in order:
COSMOS_MONGO_CONNECTION_STRING — direct MongoDB connection string. Preferred in production. (Older docs reference COSMOS_DB_CONNECTION_STRING; that name is not read.)
- Managed Identity if
USE_MANAGED_IDENTITY=true + COSMOS_ACCOUNT_NAME are set (db_factory.py).
COSMOS_DB_URI + COSMOS_DB_KEY — builds the connection string locally with URL-encoded key.
- Local MongoDB at
mongodb://localhost:27017/ as fallback.
Environment variables actually consulted:
| Var |
Used by |
COSMOS_MONGO_CONNECTION_STRING |
preferred path |
USE_MANAGED_IDENTITY (true/false) |
managed-identity gate |
COSMOS_ACCOUNT_NAME (default chuuk-dictionary-cosmos) |
both managed identity + URI build |
COSMOS_DB_URI, COSMOS_DB_KEY |
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 get_database_config():
{
"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 dictionary_db.py):
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 (
dict_db.dictionary_collection.find(...)) for ad-hoc queries.
There is no search_entries, bulk_insert_entries, get_all_entries, etc. Older skill docs invented those.
UserDB
UserDB handles auth-adjacent state:
get_user(email), upsert_user(email, role)
start_session(email) — issues a session_id, invalidates prior active session for that email
is_session_valid(email, session_id) — single-active-session enforcement
track_page(email, page) — appends to pages_accessed, updates last_activity_at (user_db.py)
- Schema fields:
email, role, session_id, session_start_at, last_activity_at, pages_accessed, accepted_terms_at
PublicationManager
PublicationManager coordinates DB metadata + the filesystem under uploads/:
create_publication(title, author, ...) — writes Cosmos doc + creates uploads/<id>/ dir
add_page(pub_id, file) — saves file, adds page metadata
get_publication(pub_id), list_publications()
- Page metadata is persisted both in Cosmos and in a per-publication JSON sidecar (publication_manager.py) — 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 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
chuukese_word / english_translation 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
get_database_config() 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/AZURE_DEPLOYMENT.md.
- Renaming a collection in
get_database_config() does not rename the underlying Cosmos container — you must run an Azure-side migration.
1---2name: database-management-operations3description: Conventions for the Chuuk Dictionary persistence layer — Azure Cosmos DB (MongoDB API) via `db_factory`, `DictionaryDB`, `UserDB`, and `PublicationManager`. Covers connection-resolution order, the actual collection/method names in use, and the managed-identity path. Use when adding queries, debugging connection issues, or extending the schema.4---56# Database Management Operations78All 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.910## Layout1112| File | Class / role |13|---|---|14| [`src/database/db_factory.py`](../../../src/database/db_factory.py) | Connection helpers (`get_cosmos_client`, `get_database_client`, `get_database_config`) |15| [`src/database/dictionary_db.py`](../../../src/database/dictionary_db.py) | `DictionaryDB` — entries, words, phrases, paragraphs, pages |16| [`src/database/user_db.py`](../../../src/database/user_db.py) | `UserDB` — users, role/permissions, sessions, page-tracking |17| [`src/database/publication_manager.py`](../../../src/database/publication_manager.py) | `PublicationManager` — publication metadata + uploads/ filesystem |1819## Connection resolution order2021[`db_factory.get_cosmos_client()`](../../../src/database/db_factory.py#L62) tries, in order:22231. `COSMOS_MONGO_CONNECTION_STRING` — direct MongoDB connection string. **Preferred** in production. (Older docs reference `COSMOS_DB_CONNECTION_STRING`; that name is **not** read.)242. **Managed Identity** if `USE_MANAGED_IDENTITY=true` + `COSMOS_ACCOUNT_NAME` are set ([db_factory.py](../../../src/database/db_factory.py#L20)).253. `COSMOS_DB_URI` + `COSMOS_DB_KEY` — builds the connection string locally with URL-encoded key.264. Local MongoDB at `mongodb://localhost:27017/` as fallback.2728Environment variables actually consulted:2930| Var | Used by |31|---|---|32| `COSMOS_MONGO_CONNECTION_STRING` | preferred path |33| `USE_MANAGED_IDENTITY` (`true`/`false`) | managed-identity gate |34| `COSMOS_ACCOUNT_NAME` (default `chuuk-dictionary-cosmos`) | both managed identity + URI build |35| `COSMOS_DB_URI`, `COSMOS_DB_KEY` | key-based auth |3637`retryWrites=False` and `appName=@<account>@` are required by Cosmos's MongoDB API and are baked into the generated connection string.3839## Database & collection names4041From [`get_database_config()`](../../../src/database/db_factory.py#L130):4243```python44{45 "database_name": "chuuk_dictionary",46 "container_name": "dictionary_entries", # → DictionaryDB.dictionary_collection47 "pages_container": "dictionary_pages", # → pages_collection48 "words_container": "words", # → words_collection49 "phrases_container": "phrases", # → phrases_collection50 "paragraphs_container": "paragraphs", # → paragraphs_collection51 "users_container": "users", # → UserDB.users_collection52}53```5455## DictionaryDB — actual API5657The methods that actually exist (see [dictionary_db.py](../../../src/database/dictionary_db.py#L972)):5859- `search_word(word: str) -> dict | None`60- `search_words(query: str, limit: int = 50) -> list[dict]`61- `add_word(word: str, translation: str, **meta) -> str`62- `search_phrases(query: str, limit: int = 50) -> list[dict]`63- `add_phrase(chuukese: str, english: str, **meta) -> str`64- Plus direct collection access (`dict_db.dictionary_collection.find(...)`) for ad-hoc queries.6566There is **no** `search_entries`, `bulk_insert_entries`, `get_all_entries`, etc. Older skill docs invented those.6768## UserDB6970[`UserDB`](../../../src/database/user_db.py#L11) handles auth-adjacent state:7172- `get_user(email)`, `upsert_user(email, role)`73- `start_session(email)` — issues a `session_id`, invalidates prior active session for that email74- `is_session_valid(email, session_id)` — single-active-session enforcement75- `track_page(email, page)` — appends to `pages_accessed`, updates `last_activity_at` ([user_db.py](../../../src/database/user_db.py#L164))76- Schema fields: `email`, `role`, `session_id`, `session_start_at`, `last_activity_at`, `pages_accessed`, `accepted_terms_at`7778## PublicationManager7980[`PublicationManager`](../../../src/database/publication_manager.py#L10) coordinates DB metadata + the filesystem under `uploads/`:8182- `create_publication(title, author, ...)` — writes Cosmos doc + creates `uploads/<id>/` dir83- `add_page(pub_id, file)` — saves file, adds page metadata84- `get_publication(pub_id)`, `list_publications()`85- Page metadata is persisted both in Cosmos and in a per-publication JSON sidecar ([publication_manager.py](../../../src/database/publication_manager.py#L18)) — keep them in sync if you mutate either directly.8687## Common patterns8889```python90from src.database.dictionary_db import DictionaryDB91from src.database.user_db import UserDB9293dict_db = DictionaryDB() # Singleton-ish — instantiate once per worker94user_db = UserDB()9596# Search (escape user input!)97results = dict_db.search_words(user_query, limit=50)9899# Direct collection query when method doesn't fit100import re101pattern = re.escape(user_input)102rows = dict_db.dictionary_collection.find(103 {"chuukese_word": {"$regex": pattern, "$options": "i"}},104 limit=50,105)106107# Insert with audit fields108from datetime import datetime, timezone109dict_db.dictionary_collection.insert_one({110 "chuukese_word": word,111 "english_translation": meaning,112 "grammar_type": pos,113 "confidence_score": 0.9,114 "edited_by": user_email,115 "created_at": datetime.now(timezone.utc),116})117```118119## Cosmos DB constraints120121- `pymongo` version is pinned in [requirements.txt](../../../requirements.txt) for Cosmos wire-protocol compatibility — don't bump unilaterally.122- `retryWrites=False` is mandatory (already in connection string).123- RU budget matters: avoid full collection scans; prefer indexed `chuukese_word` / `english_translation` queries.124- Cosmos's MongoDB API ignores some `$regex` flags silently — case-insensitive search via `$options: "i"` is fine; lookahead/lookbehind are not.125- The factory's local-MongoDB fallback is for tests/dev only — production must have Cosmos credentials.126127## Pitfalls128129- The 2-worker gunicorn setup means `DictionaryDB()` is instantiated twice. Don't add per-instance caches and expect them to be coherent across requests.130- When adding a new collection, plumb it into `get_database_config()` AND the `DictionaryDB.__init__` block so `_collection` attributes stay consistent.131- Managed-identity path requires the workload identity to have a Cosmos RBAC role assigned — see [docs/AZURE_DEPLOYMENT.md](../../../docs/AZURE_DEPLOYMENT.md).132- Renaming a collection in `get_database_config()` does **not** rename the underlying Cosmos container — you must run an Azure-side migration.