Database¶
LightningROD uses PostgreSQL 16 with SQLAlchemy 2.0 in async mode. The schema covers the full ha-fordpass data model plus reference tables for networks, locations, stalls, and settings.
Schema Overview¶
Core Tables¶
| Table | Purpose |
|---|---|
ev_charging_session |
Charging events: timing, energy, cost, location, SOC, EVSE data |
ev_battery_status |
HV/12V battery snapshots: SOC, voltage, current, temperature |
ev_vehicle_status |
Drivetrain, temperatures, tire pressure, door locks |
ev_trip_metrics |
Per-trip energy, distance, efficiency, driving scores |
ev_location |
GPS snapshots with optional reverse geocoding |
Reference Tables¶
| Table | Purpose |
|---|---|
ev_charging_networks |
Network definitions with cost_per_kwh and color |
ev_location_lookup |
Known locations with network FK and optional cost override |
ev_charger_stalls |
Charger configurations per location (type, rated kW, connector) |
app_settings |
Key-value store for user preferences and toggles |
ev_statistics |
Aggregate statistics summary (single row, recomputed) |
Key Relationships¶
ev_charging_networks
├── ev_location_lookup (network_id FK)
│ └── ev_charger_stalls (location_id FK)
└── ev_charging_session (network_id FK, stall_id FK)
Charging Session Fields¶
The ev_charging_session table includes:
- Identity:
id,session_id(UUID),device_id - Type/Location:
charge_type,location_name,location_type,network_id,location_id,is_free - Power:
charging_voltage,charging_amperage,charging_kw,max_power,min_power - Timestamps:
session_start_utc,session_end_utc,estimated_end_utc,recorded_at - Duration:
charge_duration_seconds,plugged_in_duration_seconds - Energy/SOC:
start_soc,end_soc,energy_kwh,miles_added - Cost:
cost,cost_source,estimated_cost,cost_without_overrides - EVSE:
evse_voltage,evse_amperage,evse_kw,evse_energy_kwh,evse_max_power_kw,charger_rated_kw,evse_source,stall_id - Location data:
address,latitude,longitude - Metadata:
source_system,ingested_at,original_timestamp,ingest_schema_version
Connection Management¶
db/engine.py
"""Async SQLAlchemy engine and session factory."""
from urllib.parse import urlparse
from sqlalchemy import event
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.pool import NullPool
from config import settings
def _is_sqlite(url: str) -> bool:
return urlparse(url).scheme.startswith("sqlite")
_kwargs: dict = {"echo": settings.debug}
if _is_sqlite(settings.database_url):
_kwargs["poolclass"] = NullPool
else:
_kwargs["pool_pre_ping"] = True
_kwargs["pool_recycle"] = 3600
engine = create_async_engine(settings.database_url, **_kwargs)
# Dialect captured at engine construction so the listener can no-op cleanly
# on PostgreSQL without inspecting the engine on every connection.
_DIALECT_IS_SQLITE = engine.sync_engine.dialect.name == "sqlite"
@event.listens_for(engine.sync_engine, "connect")
def _set_sqlite_pragmas(dbapi_connection, connection_record):
"""Enable SQLite safety/performance PRAGMAs on each DBAPI connection."""
if not _DIALECT_IS_SQLITE:
return
cursor = dbapi_connection.cursor()
try:
cursor.execute("PRAGMA foreign_keys = ON")
cursor.execute("PRAGMA journal_mode = WAL")
cursor.execute("PRAGMA synchronous = NORMAL")
cursor.execute("PRAGMA busy_timeout = 5000")
finally:
cursor.close()
AsyncSessionLocal = async_sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
Key settings:
pool_pre_ping=True-- validates connections before usepool_recycle=3600-- refreshes connections every hourasyncpgdriver for native async PostgreSQL access
Models¶
Models live in db/models/ with one file per domain area. The __init__.py imports all model classes so Alembic's autogenerate discovers every table:
db/models/__init__.py
"""SQLAlchemy model package."""
# Import Base and ALL model classes so Alembic's autogenerate sees all tables.
# CRITICAL: Every model module must be imported here. If a module is missing,
# alembic revision --autogenerate will produce an empty migration.
from db.models.base import Base
from db.models.battery_status import EVBatteryStatus
from db.models.charging_session import EVChargingSession
from db.models.data_source_config import DataSourceConfig
from db.models.ice_vehicle import IceVehicle
from db.models.location import EVLocation
from db.models.reference import (
AppSettings,
EVChargerStall,
EVChargingNetwork,
EVLocationLookup,
EVStatistics,
GasPriceHistory,
GasPriceReading,
)
from db.models.trip_metrics import EVTripMetrics
from db.models.vehicle import EVVehicle
from db.models.vehicle_status import EVVehicleStatus
__all__ = [
"Base",
"DataSourceConfig",
"EVChargingSession",
"EVBatteryStatus",
"EVTripMetrics",
"EVLocation",
"EVVehicleStatus",
"EVChargingNetwork",
"EVChargerStall",
"EVLocationLookup",
"EVStatistics",
"EVVehicle",
"IceVehicle",
"AppSettings",
"GasPriceHistory",
"GasPriceReading",
]
Migrations¶
Alembic manages schema versioning with an async-compatible env.py.
Creating a New Migration¶
Dependency Injection¶
Each request gets its own database session via FastAPI's dependency system:
web/dependencies.py
"""FastAPI dependency providers."""
from collections.abc import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession
from db.engine import AsyncSessionLocal
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
try:
yield session
finally:
await session.close()
Route handlers declare the dependency:
@router.get("/sessions")
async def sessions(request: Request, db: AsyncSession = Depends(get_db)):
...
The session is automatically committed on success and closed after the request.