11. Data Models & Database Schema

11.1 Database Technology

SQLite 3.x

Type: Embedded, serverless database

ORM: SQLAlchemy 2.x

Location: ./currency_calculator.db (local file)

Advantages

  • ? Zero configuration
  • ? No server required
  • ? Fully offline
  • ? File-based (easy backup)
  • ? ACID compliant
  • ? Cross-platform
  • ? Fast for local operations

11.2 Complete Database Schema

11.2.1 Calculations Table

CREATE TABLE calculations (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    amount DECIMAL(20, 2) NOT NULL,
    currency VARCHAR(3) NOT NULL,
    mode VARCHAR(20) NOT NULL,
    breakdown TEXT NOT NULL,  -- JSON string
    summary TEXT NOT NULL,     -- JSON string
    timestamp DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
    
    -- Indexes for performance
    INDEX idx_currency (currency),
    INDEX idx_timestamp (timestamp DESC),
    INDEX idx_amount (amount)
);

SQLAlchemy Model

from sqlalchemy import Column, Integer, String, DateTime, Text, Numeric, Index
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime

Base = declarative_base()

class Calculation(Base):
    __tablename__ = "calculations"
    
    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    amount = Column(Numeric(precision=20, scale=2), nullable=False, index=True)
    currency = Column(String(3), nullable=False, index=True)
    mode = Column(String(20), nullable=False)
    breakdown = Column(Text, nullable=False)  # JSON: List[{denomination, type, count, total_value}]
    summary = Column(Text, nullable=False)     # JSON: {total_notes, total_coins, total_denominations}
    timestamp = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
    
    # Composite indexes
    __table_args__ = (
        Index('idx_currency_timestamp', 'currency', 'timestamp'),
    )
    
    def __repr__(self):
        return f""

Column Details

Column Type Description Example
id INTEGER Auto-incrementing primary key 1, 2, 3, ...
amount DECIMAL(20,2) Original amount calculated 1234.56
currency VARCHAR(3) Currency code (indexed) INR, USD, EUR, GBP
mode VARCHAR(20) Optimization mode used greedy, balanced, minimize_large, minimize_small
breakdown TEXT (JSON) Denomination breakdown details [{"denomination": 500, "count": 2, ...}]
summary TEXT (JSON) Summary statistics {"total_notes": 5, "total_coins": 3}
timestamp DATETIME Creation time (indexed, descending) 2025-11-27 10:30:00

11.2.2 Settings Table

CREATE TABLE settings (
    id INTEGER PRIMARY KEY DEFAULT 1,  -- Single row table
    theme VARCHAR(10) DEFAULT 'light' NOT NULL,
    language VARCHAR(2) DEFAULT 'en' NOT NULL,
    default_currency VARCHAR(3) DEFAULT 'INR' NOT NULL,
    default_mode VARCHAR(20) DEFAULT 'greedy' NOT NULL,
    auto_save_history BOOLEAN DEFAULT 1 NOT NULL,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
    
    -- Ensure only one settings row
    CHECK (id = 1)
);

SQLAlchemy Model

class Settings(Base):
    __tablename__ = "settings"
    
    id = Column(Integer, primary_key=True, default=1)
    theme = Column(String(10), default="light", nullable=False)
    language = Column(String(2), default="en", nullable=False)
    default_currency = Column(String(3), default="INR", nullable=False)
    default_mode = Column(String(20), default="greedy", nullable=False)
    auto_save_history = Column(Boolean, default=True, nullable=False)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
    
    def __repr__(self):
        return f""

Column Details

Column Type Default Options
id INTEGER 1 Always 1 (single row)
theme VARCHAR(10) light light, dark, system
language VARCHAR(2) en en, hi, es, fr, de
default_currency VARCHAR(3) INR INR, USD, EUR, GBP
default_mode VARCHAR(20) greedy greedy, balanced, minimize_large, minimize_small
auto_save_history BOOLEAN TRUE TRUE, FALSE

11.3 Sample Data & Queries

Common Queries

1. Get Latest Calculations

# Get last 10 calculations
recent = db.query(Calculation)\
    .order_by(Calculation.timestamp.desc())\
    .limit(10)\
    .all()

2. Filter by Currency

# Get all INR calculations
inr_calcs = db.query(Calculation)\
    .filter(Calculation.currency == 'INR')\
    .order_by(Calculation.timestamp.desc())\
    .all()

3. Paginated History

def get_paginated_history(page: int = 1, per_page: int = 50):
    offset = (page - 1) * per_page
    
    results = db.query(Calculation)\
        .order_by(Calculation.timestamp.desc())\
        .offset(offset)\
        .limit(per_page)\
        .all()
    
    total = db.query(Calculation).count()
    
    return {
        'items': results,
        'total': total,
        'page': page,
        'pages': (total + per_page - 1) // per_page
    }

4. Statistics by Currency

from sqlalchemy import func

# Total calculations by currency
stats = db.query(
    Calculation.currency,
    func.count(Calculation.id).label('count'),
    func.sum(Calculation.amount).label('total_amount')
)\
    .group_by(Calculation.currency)\
    .all()

# Output:
# [('INR', 150, Decimal('450000.00')),
#  ('USD', 50, Decimal('125000.00')),
#  ...]

5. Date Range Filter

from datetime import datetime, timedelta

# Last 7 days
start_date = datetime.utcnow() - timedelta(days=7)

recent_week = db.query(Calculation)\
    .filter(Calculation.timestamp >= start_date)\
    .order_by(Calculation.timestamp.desc())\
    .all()

6. Delete Calculation

# Delete by ID
calc = db.query(Calculation).filter(Calculation.id == 42).first()
if calc:
    db.delete(calc)
    db.commit()

7. Clear All History

# Delete all calculations
db.query(Calculation).delete()
db.commit()

8. Get Settings

# Always returns single row
settings = db.query(Settings).first()

if not settings:
    # Initialize with defaults
    settings = Settings(
        id=1,
        theme="light",
        language="en",
        default_currency="INR",
        default_mode="greedy",
        auto_save_history=True
    )
    db.add(settings)
    db.commit()

9. Update Settings

settings = db.query(Settings).first()
settings.theme = "dark"
settings.language = "hi"
db.commit()

11.4 Database Initialization

File: packages/local-backend/app/db/database.py

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.db.models import Base, Settings

# Create engine
DATABASE_URL = "sqlite:///./currency_calculator.db"
engine = create_engine(
    DATABASE_URL,
    connect_args={"check_same_thread": False},  # SQLite specific
    echo=False  # Set True for SQL logging
)

# Create all tables
Base.metadata.create_all(bind=engine)

# Session factory
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# Dependency for FastAPI
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

# Initialize default settings
def init_database():
    """Initialize database with default settings."""
    db = SessionLocal()
    
    try:
        # Check if settings exist
        if not db.query(Settings).first():
            default_settings = Settings(
                id=1,
                theme="light",
                language="en",
                default_currency="INR",
                default_mode="greedy",
                auto_save_history=True
            )
            db.add(default_settings)
            db.commit()
            print("? Default settings initialized")
    finally:
        db.close()

# Call on startup
init_database()

11.5 JSON Field Structures

Breakdown JSON Format

[
  {
    "denomination": 500,
    "type": "note",
    "count": 3,
    "total_value": 1500
  },
  {
    "denomination": 200,
    "type": "note",
    "count": 1,
    "total_value": 200
  },
  {
    "denomination": 50,
    "type": "note",
    "count": 1,
    "total_value": 50
  },
  {
    "denomination": 5,
    "type": "coin",
    "count": 2,
    "total_value": 10
  }
]

Summary JSON Format

{
  "total_notes": 5,
  "total_coins": 2,
  "total_denominations": 4
}

11.6 Backup & Restore

Manual Backup

# Simple file copy
Copy-Item currency_calculator.db -Destination backup_$(Get-Date -Format 'yyyyMMdd_HHmmss').db

Automated Backup Script

import shutil
from datetime import datetime
from pathlib import Path

def backup_database():
    """Create timestamped database backup."""
    db_path = Path("currency_calculator.db")
    
    if not db_path.exists():
        print("Database not found")
        return
    
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    backup_path = Path(f"backups/currency_calculator_{timestamp}.db")
    backup_path.parent.mkdir(exist_ok=True)
    
    shutil.copy2(db_path, backup_path)
    print(f"? Backup created: {backup_path}")

# Schedule daily backups
if __name__ == "__main__":
    backup_database()

Export to CSV

import csv
from app.db.database import SessionLocal
from app.db.models import Calculation

def export_to_csv(output_file: str):
    """Export all calculations to CSV."""
    db = SessionLocal()
    
    try:
        calcs = db.query(Calculation).order_by(Calculation.timestamp.desc()).all()
        
        with open(output_file, 'w', newline='', encoding='utf-8') as f:
            writer = csv.writer(f)
            
            # Header
            writer.writerow(['ID', 'Amount', 'Currency', 'Mode', 'Timestamp', 'Breakdown', 'Summary'])
            
            # Data
            for calc in calcs:
                writer.writerow([
                    calc.id,
                    calc.amount,
                    calc.currency,
                    calc.mode,
                    calc.timestamp,
                    calc.breakdown,
                    calc.summary
                ])
        
        print(f"? Exported {len(calcs)} calculations to {output_file}")
    finally:
        db.close()
? Section Complete

This section covers complete database schema including SQLite setup, 2 tables (Calculations, Settings), SQLAlchemy models, 9 common query examples, initialization scripts, JSON structures, and backup/restore utilities.