Skip to content

Commit 7db2e25

Browse files
committed
Add literate programming docs and C4 annotations
1 parent 963873c commit 7db2e25

6 files changed

Lines changed: 1479 additions & 14 deletions

File tree

app/database.py

Lines changed: 278 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,306 @@
1+
"""
2+
================================================================================
3+
DATABASE LAYER - PERSISTENCE AND ORM
4+
================================================================================
5+
6+
Module: app.database
7+
Purpose: Database connection management, ORM models, and session handling
8+
Author: Notes API Team
9+
Last Updated: 2025-01-21
10+
11+
ARCHITECTURAL CONTEXT
12+
---------------------
13+
This module provides the persistence layer for the Notes API. It manages all
14+
interactions with the SQLite database using SQLAlchemy ORM, handling connection
15+
lifecycle, session management, and data model definitions.
16+
17+
C4 MODEL MAPPING
18+
----------------
19+
@c4-container: Database Layer
20+
@c4-technology: Python 3.12, SQLAlchemy 2.0, SQLite 3
21+
@c4-description: ORM-based persistence layer managing note storage and retrieval
22+
@c4-responsibilities:
23+
- Maintain database connection and session lifecycle
24+
- Define database schema through ORM models
25+
- Provide database session to API endpoints
26+
- Initialize database schema on application startup
27+
- Abstract SQL queries behind Python objects
28+
29+
DESIGN DECISIONS
30+
----------------
31+
1. **Database Choice: SQLite**
32+
- Rationale: Zero-configuration, perfect for simple applications
33+
- Rationale: File-based storage, no separate database server needed
34+
- Rationale: ACID compliance for data integrity
35+
- Trade-off: Limited concurrency (single-writer lock)
36+
- Trade-off: Not suitable for high-traffic production deployments
37+
- Production Alternative: PostgreSQL or MySQL for multi-user scenarios
38+
- Use Case: Development, testing, small single-user deployments
39+
40+
2. **ORM Choice: SQLAlchemy 2.0**
41+
- Rationale: Industry-standard Python ORM with excellent documentation
42+
- Rationale: Type-safe queries with modern 2.0 API
43+
- Rationale: Automatic SQL generation reduces errors
44+
- Rationale: Database agnostic (easy to switch to PostgreSQL later)
45+
- Trade-off: Slight performance overhead vs raw SQL
46+
- Alternative Considered: Direct SQL with aiosqlite (rejected for simplicity)
47+
48+
3. **Session Management Pattern**
49+
- Pattern: Dependency injection via get_db() generator
50+
- Rationale: Automatic session cleanup (even on exceptions)
51+
- Rationale: Each request gets isolated database session
52+
- Rationale: Prevents session leaks and connection pool exhaustion
53+
- Implementation: Python generator with try/finally ensures cleanup
54+
55+
4. **Schema Initialization**
56+
- Strategy: Automatic table creation via Base.metadata.create_all()
57+
- Behavior: Creates tables only if they don't exist (idempotent)
58+
- Timing: On application startup (via lifespan manager)
59+
- Future: Replace with Alembic migrations for schema versioning
60+
61+
5. **Timezone Handling**
62+
- Decision: Use timezone-aware UTC timestamps
63+
- Rationale: Prevents Python 3.12+ deprecation warnings
64+
- Rationale: Explicit timezone handling prevents ambiguity
65+
- Implementation: datetime.now(timezone.utc) instead of deprecated utcnow()
66+
- Storage: SQLite stores as UTC ISO8601 strings
67+
68+
SYSTEM INTERACTIONS
69+
-------------------
70+
@c4-uses: SQLite Database - "Reads and writes note data" - "SQL/SQLite3"
71+
@c4-used-by: API Application (app.main) - "Requests database sessions" - "SQLAlchemy ORM"
72+
73+
DATABASE SCHEMA
74+
---------------
75+
Table: notes
76+
Columns:
77+
- id (INTEGER): Primary key, auto-increment
78+
- title (TEXT): Note title, indexed for search performance
79+
- content (TEXT): Note content (unlimited length in SQLite)
80+
- created_at (DATETIME): UTC timestamp, auto-set on creation
81+
82+
Indexes:
83+
- PRIMARY KEY on id (automatic)
84+
- INDEX on title (for future search functionality)
85+
86+
PERFORMANCE CONSIDERATIONS
87+
---------------------------
88+
- Connection pooling: NullPool for SQLite (single connection)
89+
- Check same thread: Disabled for multi-threaded FastAPI compatibility
90+
- Query optimization: Primary key lookups are O(log n) via B-tree index
91+
- Pagination: Uses OFFSET/LIMIT (works well for small datasets)
92+
93+
FUTURE ENHANCEMENTS
94+
-------------------
95+
- [ ] Implement Alembic migrations for schema versioning
96+
- [ ] Add full-text search index on title and content
97+
- [ ] Implement soft deletes with 'archived_at' column
98+
- [ ] Add 'updated_at' timestamp with automatic updates
99+
- [ ] Consider adding user_id for multi-tenancy
100+
- [ ] Add database connection pooling for production
101+
- [ ] Implement read replicas for scaling read-heavy workloads
102+
103+
================================================================================
104+
"""
105+
1106
from sqlalchemy import create_engine, Column, Integer, String, DateTime
2-
from sqlalchemy.ext.declarative import declarative_base
3-
from sqlalchemy.orm import sessionmaker
4-
from datetime import datetime
107+
from sqlalchemy.orm import declarative_base, sessionmaker
108+
from datetime import datetime, timezone
5109

110+
# Database URL Configuration
111+
# ---------------------------
112+
# Format: sqlite:///./notes.db
113+
# - sqlite:/// = SQLite database (file-based)
114+
# - ./ = Current working directory
115+
# - notes.db = Database file name
116+
#
117+
# The database file will be created in the directory where the app is run.
118+
# For production, consider using absolute paths or environment variables.
6119
SQLALCHEMY_DATABASE_URL = "sqlite:///./notes.db"
7120

121+
# Database Engine Creation
122+
# -------------------------
123+
# The engine is the starting point for any SQLAlchemy application.
124+
# It manages connections to the database.
125+
#
126+
# Configuration:
127+
# - check_same_thread=False: Required for SQLite with multi-threaded servers
128+
# SQLite is usually single-threaded, but FastAPI runs on multiple threads
129+
# This setting makes SQLite thread-safe for our use case
130+
#
131+
# Note: For production with PostgreSQL/MySQL, this connect_args would change
8132
engine = create_engine(
9-
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
133+
SQLALCHEMY_DATABASE_URL,
134+
connect_args={"check_same_thread": False}
10135
)
136+
137+
# Session Factory
138+
# ---------------
139+
# SessionLocal is a factory for creating database sessions.
140+
# Each session represents a "workspace" for database operations.
141+
#
142+
# Configuration:
143+
# - autocommit=False: Requires explicit db.commit() (safer, more control)
144+
# - autoflush=False: Manual control over when queries are sent to DB
145+
# - bind=engine: Associates sessions with our database engine
146+
#
147+
# Sessions are created per-request and automatically cleaned up
11148
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
12149

150+
# Declarative Base
151+
# ----------------
152+
# Base class for all ORM models. All database tables inherit from this.
153+
# Provides metadata and mapping capabilities for SQLAlchemy 2.0
13154
Base = declarative_base()
14155

15156

16157
class NoteDB(Base):
158+
"""
159+
Note ORM Model
160+
161+
Represents the 'notes' table in the database. Maps Python objects to
162+
database rows, enabling object-oriented database interactions.
163+
164+
DESIGN CHOICES
165+
--------------
166+
1. Separate from Pydantic models (NoteCreate, Note) for separation of concerns
167+
- NoteDB: Database representation (ORM)
168+
- Note: API response representation (Pydantic)
169+
- NoteCreate: API request representation (Pydantic)
170+
171+
2. Auto-incrementing primary key for simplicity
172+
- Alternative: UUIDs for distributed systems (overkill for this app)
173+
174+
3. No foreign keys yet (single table)
175+
- Future: Could add user_id for multi-user support
176+
- Future: Could add tags table with many-to-many relationship
177+
178+
4. Text fields use SQLite's TEXT type (no length limit)
179+
- SQLite doesn't enforce VARCHAR limits anyway
180+
- Application-level validation via Pydantic
181+
182+
@c4-component: Note Data Model
183+
@c4-technology: SQLAlchemy ORM
184+
185+
Attributes:
186+
id: Unique identifier, automatically assigned by database
187+
title: Note title, indexed for search performance
188+
content: Note content, unlimited length
189+
created_at: UTC timestamp, set automatically on creation
190+
"""
191+
17192
__tablename__ = "notes"
18-
193+
194+
# Primary Key
195+
# -----------
196+
# Integer primary key with autoincrement
197+
# SQLite optimizes primary key lookups via B-tree index
19198
id = Column(Integer, primary_key=True, index=True)
199+
200+
# Title Field
201+
# -----------
202+
# Indexed for future search functionality
203+
# No length constraint (SQLite TEXT type)
204+
# Application validates via Pydantic (could add max length)
20205
title = Column(String, index=True)
206+
207+
# Content Field
208+
# -------------
209+
# Main note content, no length limit
210+
# SQLite TEXT can store up to 2GB
211+
# Consider adding full-text search index for large datasets
21212
content = Column(String)
22-
created_at = Column(DateTime, default=datetime.utcnow)
213+
214+
# Created Timestamp
215+
# -----------------
216+
# Timezone-aware UTC timestamp
217+
# Automatically set on row creation
218+
# Stored as ISO8601 string in SQLite ("YYYY-MM-DD HH:MM:SS.ffffff+00:00")
219+
#
220+
# Note: Using lambda to call datetime.now(timezone.utc) on each insert
221+
# This ensures the timestamp is generated at insertion time, not at
222+
# server startup
223+
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
23224

24225

25226
def get_db():
227+
"""
228+
Database Session Dependency
229+
230+
FastAPI dependency that provides a database session to endpoint handlers.
231+
Implements the dependency injection pattern with automatic cleanup.
232+
233+
LIFECYCLE
234+
---------
235+
1. Create new session when endpoint is called
236+
2. Yield session to endpoint handler (dependency injection)
237+
3. Endpoint handler uses session for queries
238+
4. Finally block ensures session is closed (even if exception occurs)
239+
240+
TRANSACTION MANAGEMENT
241+
----------------------
242+
- Endpoint is responsible for calling db.commit()
243+
- Uncommitted changes are rolled back on session close
244+
- Exceptions trigger automatic rollback
245+
246+
THREAD SAFETY
247+
-------------
248+
- Each request gets its own session
249+
- No session sharing between requests
250+
- Safe for concurrent requests
251+
252+
@c4-provides: Database session for request handling
253+
254+
Yields:
255+
Session: SQLAlchemy session for database operations
256+
257+
Example Usage:
258+
@app.get("/notes")
259+
def get_notes(db: Session = Depends(get_db)):
260+
return db.query(NoteDB).all()
261+
"""
26262
db = SessionLocal()
27263
try:
28264
yield db
29265
finally:
266+
# Cleanup: Always close session, even if exception occurred
267+
# This prevents connection leaks and ensures proper resource cleanup
30268
db.close()
31269

32270

33271
def init_db():
272+
"""
273+
Initialize Database Schema
274+
275+
Creates all tables defined by ORM models if they don't already exist.
276+
This is called once on application startup.
277+
278+
BEHAVIOR
279+
--------
280+
- Idempotent: Safe to call multiple times (won't recreate existing tables)
281+
- Creates tables in dependency order (respects foreign keys)
282+
- Uses engine's metadata to determine what needs creation
283+
284+
LIMITATIONS
285+
-----------
286+
- Does NOT handle schema migrations (adding/removing columns)
287+
- Does NOT version the schema
288+
- For production, use Alembic for proper migration management
289+
290+
FUTURE
291+
------
292+
Replace with Alembic migrations to support:
293+
- Schema versioning
294+
- Rollback capability
295+
- Column additions/modifications
296+
- Data migrations
297+
298+
@c4-initializes: Database schema on application startup
299+
300+
Example:
301+
# In app startup
302+
init_db() # Creates 'notes' table if it doesn't exist
303+
"""
304+
# Create all tables defined in Base's metadata
305+
# Only creates tables that don't already exist (safe to call repeatedly)
34306
Base.metadata.create_all(bind=engine)

0 commit comments

Comments
 (0)