|
| 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 | + |
1 | 106 | 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 |
5 | 109 |
|
| 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. |
6 | 119 | SQLALCHEMY_DATABASE_URL = "sqlite:///./notes.db" |
7 | 120 |
|
| 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 |
8 | 132 | engine = create_engine( |
9 | | - SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} |
| 133 | + SQLALCHEMY_DATABASE_URL, |
| 134 | + connect_args={"check_same_thread": False} |
10 | 135 | ) |
| 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 |
11 | 148 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) |
12 | 149 |
|
| 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 |
13 | 154 | Base = declarative_base() |
14 | 155 |
|
15 | 156 |
|
16 | 157 | 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 | + |
17 | 192 | __tablename__ = "notes" |
18 | | - |
| 193 | + |
| 194 | + # Primary Key |
| 195 | + # ----------- |
| 196 | + # Integer primary key with autoincrement |
| 197 | + # SQLite optimizes primary key lookups via B-tree index |
19 | 198 | 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) |
20 | 205 | 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 |
21 | 212 | 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)) |
23 | 224 |
|
24 | 225 |
|
25 | 226 | 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 | + """ |
26 | 262 | db = SessionLocal() |
27 | 263 | try: |
28 | 264 | yield db |
29 | 265 | finally: |
| 266 | + # Cleanup: Always close session, even if exception occurred |
| 267 | + # This prevents connection leaks and ensures proper resource cleanup |
30 | 268 | db.close() |
31 | 269 |
|
32 | 270 |
|
33 | 271 | 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) |
34 | 306 | Base.metadata.create_all(bind=engine) |
0 commit comments