Skip to content

Commit 6d18f1b

Browse files
author
daxx
committed
docs: add Cosmos DB PostgreSQL migration guide
Pre-staged database at c-todo-app-db.qhb3l52muuda2z.postgres.cosmos.azure.com Step-by-step: SQLite→PostgreSQL with dual-mode fallback Includes schema changes, query differences, Dockerfile updates, Container App env var
1 parent bb0ee1a commit 6d18f1b

1 file changed

Lines changed: 196 additions & 0 deletions

File tree

docs/DATABASE-MIGRATION.md

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
# Database Migration: SQLite → Cosmos DB PostgreSQL
2+
3+
## Overview
4+
5+
The todo-app currently uses SQLite (in-memory/local file). A Cosmos DB for PostgreSQL
6+
instance has been pre-staged and is ready to connect. This guide covers how to wire it up.
7+
8+
## Pre-Staged Infrastructure
9+
10+
| Setting | Value |
11+
|---------|-------|
12+
| Cluster | `todo-app-db` |
13+
| Host | `c-todo-app-db.qhb3l52muuda2z.postgres.cosmos.azure.com` |
14+
| Port | `5432` |
15+
| Database | `citus` (default) |
16+
| Admin user | `citus` |
17+
| Password | `PSEA-Lab-2026!` |
18+
| SSL | Required |
19+
| Resource Group | `rg-todo-app` |
20+
| Firewall | Azure services + all IPs (workshop only) |
21+
22+
## Connection String
23+
24+
```
25+
postgresql://citus:PSEA-Lab-2026!@c-todo-app-db.qhb3l52muuda2z.postgres.cosmos.azure.com:5432/citus?sslmode=require
26+
```
27+
28+
## Step 1: Add psycopg2 to requirements.txt
29+
30+
```
31+
psycopg2-binary==2.9.9
32+
```
33+
34+
## Step 2: Update app.py — Database Connection
35+
36+
Replace the SQLite connection logic with:
37+
38+
```python
39+
import os
40+
import psycopg2
41+
from psycopg2.extras import RealDictCursor
42+
43+
DATABASE_URL = os.environ.get(
44+
"DATABASE_URL",
45+
"sqlite:///todos.db" # fallback to SQLite for local dev
46+
)
47+
48+
def get_db():
49+
"""Get a database connection."""
50+
if DATABASE_URL.startswith("postgresql"):
51+
conn = psycopg2.connect(DATABASE_URL, cursor_factory=RealDictCursor)
52+
conn.autocommit = True
53+
return conn
54+
else:
55+
# Original SQLite logic
56+
import sqlite3
57+
conn = sqlite3.connect("todos.db")
58+
conn.row_factory = sqlite3.Row
59+
return conn
60+
```
61+
62+
## Step 3: Update Schema (PostgreSQL version)
63+
64+
Create `schema.sql`:
65+
66+
```sql
67+
CREATE TABLE IF NOT EXISTS todos (
68+
id SERIAL PRIMARY KEY,
69+
title TEXT NOT NULL,
70+
description TEXT DEFAULT '',
71+
completed BOOLEAN DEFAULT FALSE,
72+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
73+
);
74+
```
75+
76+
Add an init function:
77+
78+
```python
79+
def init_db():
80+
"""Initialize the database schema."""
81+
conn = get_db()
82+
if DATABASE_URL.startswith("postgresql"):
83+
cur = conn.cursor()
84+
cur.execute("""
85+
CREATE TABLE IF NOT EXISTS todos (
86+
id SERIAL PRIMARY KEY,
87+
title TEXT NOT NULL,
88+
description TEXT DEFAULT '',
89+
completed BOOLEAN DEFAULT FALSE,
90+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
91+
)
92+
""")
93+
cur.close()
94+
conn.close()
95+
else:
96+
# Original SQLite init
97+
pass
98+
```
99+
100+
## Step 4: Update SQL Queries
101+
102+
Key differences from SQLite to PostgreSQL:
103+
104+
| SQLite | PostgreSQL |
105+
|--------|-----------|
106+
| `INTEGER PRIMARY KEY AUTOINCREMENT` | `SERIAL PRIMARY KEY` |
107+
| `?` parameter placeholder | `%s` parameter placeholder |
108+
| `datetime('now')` | `CURRENT_TIMESTAMP` |
109+
| `BOOLEAN` stored as 0/1 | `BOOLEAN` stored as true/false |
110+
111+
Example — insert:
112+
```python
113+
# SQLite
114+
cur.execute("INSERT INTO todos (title) VALUES (?)", (title,))
115+
116+
# PostgreSQL
117+
cur.execute("INSERT INTO todos (title) VALUES (%s) RETURNING id", (title,))
118+
```
119+
120+
Example — fetch:
121+
```python
122+
# SQLite (returns sqlite3.Row)
123+
todos = cur.execute("SELECT * FROM todos").fetchall()
124+
125+
# PostgreSQL (returns dicts via RealDictCursor)
126+
cur.execute("SELECT * FROM todos ORDER BY created_at DESC")
127+
todos = cur.fetchall()
128+
```
129+
130+
## Step 5: Update Dockerfile
131+
132+
Add PostgreSQL client library:
133+
134+
```dockerfile
135+
# Add before pip install
136+
RUN apt-get update && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/*
137+
```
138+
139+
## Step 6: Set Environment Variable on Container App
140+
141+
```bash
142+
az containerapp update \
143+
--resource-group rg-todo-app \
144+
--name todo-app \
145+
--set-env-vars \
146+
DATABASE_URL="postgresql://citus:PSEA-Lab-2026!@c-todo-app-db.qhb3l52muuda2z.postgres.cosmos.azure.com:5432/citus?sslmode=require"
147+
```
148+
149+
## Step 7: Deploy
150+
151+
Push to main — CI/CD will build and deploy automatically. The app will:
152+
1. Read `DATABASE_URL` from environment
153+
2. Connect to Cosmos DB PostgreSQL
154+
3. Create the `todos` table if it doesn't exist
155+
4. All todos persist across redeploys
156+
157+
## Testing Locally
158+
159+
You can connect to the database from any student VM or local machine:
160+
161+
```bash
162+
# Using psql
163+
psql "postgresql://citus:PSEA-Lab-2026!@c-todo-app-db.qhb3l52muuda2z.postgres.cosmos.azure.com:5432/citus?sslmode=require"
164+
165+
# Test query
166+
SELECT * FROM todos;
167+
```
168+
169+
Or run the app locally with the env var:
170+
```bash
171+
export DATABASE_URL="postgresql://citus:PSEA-Lab-2026!@c-todo-app-db.qhb3l52muuda2z.postgres.cosmos.azure.com:5432/citus?sslmode=require"
172+
python app.py
173+
```
174+
175+
Without `DATABASE_URL`, the app falls back to SQLite for local development.
176+
177+
## Workshop Notes
178+
179+
This migration is a great exercise for students — it touches:
180+
- Environment variables and configuration
181+
- SQL dialect differences
182+
- Docker dependencies
183+
- Cloud database connectivity
184+
- CI/CD (change deploys automatically)
185+
186+
Consider making this a Sprint 1 issue for students to tackle with Claude Code.
187+
188+
## Cleanup
189+
190+
After the workshop, delete the database to stop charges:
191+
```bash
192+
az cosmosdb postgres cluster delete \
193+
--resource-group rg-todo-app \
194+
--cluster-name todo-app-db \
195+
--yes
196+
```

0 commit comments

Comments
 (0)