Files
mi-fitness-health/src/mi_fitness_mcp/storage/__init__.py
T

879 lines
34 KiB
Python

"""SQLite storage layer for Mi Fitness MCP."""
import json
import sqlite3
from contextlib import contextmanager
from datetime import datetime
from pathlib import Path
from typing import Any
from mi_fitness_mcp.models import (
AbnormalHeartBeatEvent,
BodyMeasurement,
DailyActivity,
HeartRateSample,
SleepSession,
SpO2Sample,
StressSample,
Workout,
)
class Database:
"""SQLite database manager."""
def __init__(self, db_path: Path | str):
"""Initialize database.
Args:
db_path: Path to SQLite database file
"""
self.db_path = Path(db_path)
self._init_db()
def _init_db(self) -> None:
"""Initialize database schema."""
self.db_path.parent.mkdir(parents=True, exist_ok=True)
with self._get_connection() as conn:
# 日常活动表
conn.execute("""
CREATE TABLE IF NOT EXISTS daily_activity (
id TEXT PRIMARY KEY,
provider TEXT NOT NULL,
source_type TEXT NOT NULL,
source_record_id TEXT,
user_id TEXT NOT NULL,
device_id TEXT,
timezone TEXT DEFAULT 'UTC',
collected_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
date TEXT NOT NULL,
steps INTEGER NOT NULL DEFAULT 0,
distance_m REAL NOT NULL DEFAULT 0,
active_kcal REAL NOT NULL DEFAULT 0,
total_kcal REAL,
floors INTEGER,
active_minutes INTEGER,
UNIQUE(user_id, date, device_id)
)
""")
# 睡眠记录表
conn.execute("""
CREATE TABLE IF NOT EXISTS sleep_sessions (
id TEXT PRIMARY KEY,
provider TEXT NOT NULL,
source_type TEXT NOT NULL,
source_record_id TEXT,
user_id TEXT NOT NULL,
device_id TEXT,
timezone TEXT DEFAULT 'UTC',
collected_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
sleep_id TEXT NOT NULL,
start_at TIMESTAMP NOT NULL,
end_at TIMESTAMP NOT NULL,
duration_minutes INTEGER NOT NULL,
time_asleep_minutes INTEGER NOT NULL,
time_awake_minutes INTEGER NOT NULL,
sleep_score INTEGER,
is_nap BOOLEAN DEFAULT FALSE,
stages TEXT, -- JSON array of sleep stages
UNIQUE(user_id, sleep_id)
)
""")
# 运动记录表
conn.execute("""
CREATE TABLE IF NOT EXISTS workouts (
id TEXT PRIMARY KEY,
provider TEXT NOT NULL,
source_type TEXT NOT NULL,
source_record_id TEXT,
user_id TEXT NOT NULL,
device_id TEXT,
timezone TEXT DEFAULT 'UTC',
collected_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
workout_id TEXT NOT NULL,
activity_type TEXT NOT NULL,
start_at TIMESTAMP NOT NULL,
end_at TIMESTAMP NOT NULL,
duration_minutes INTEGER NOT NULL,
distance_m REAL,
calories_kcal REAL,
avg_heart_rate_bpm INTEGER,
max_heart_rate_bpm INTEGER,
avg_pace_sec_per_km REAL,
max_pace_sec_per_km REAL,
total_steps INTEGER,
UNIQUE(user_id, workout_id)
)
""")
# 身体测量表
conn.execute("""
CREATE TABLE IF NOT EXISTS body_measurements (
id TEXT PRIMARY KEY,
provider TEXT NOT NULL,
source_type TEXT NOT NULL,
source_record_id TEXT,
user_id TEXT NOT NULL,
device_id TEXT,
timezone TEXT DEFAULT 'UTC',
collected_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
timestamp TIMESTAMP NOT NULL,
weight_kg REAL NOT NULL,
bmi REAL,
body_fat_pct REAL,
muscle_mass_kg REAL,
water_pct REAL,
bone_mass_kg REAL,
visceral_fat_score INTEGER,
basal_metabolism_kcal INTEGER,
metabolic_age INTEGER,
UNIQUE(user_id, timestamp, device_id)
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS heart_rate_samples (
id TEXT PRIMARY KEY,
provider TEXT NOT NULL,
source_type TEXT NOT NULL,
source_record_id TEXT,
user_id TEXT NOT NULL,
device_id TEXT,
timezone TEXT DEFAULT 'UTC',
collected_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
timestamp TIMESTAMP NOT NULL,
bpm INTEGER NOT NULL,
sample_type TEXT NOT NULL,
UNIQUE(user_id, timestamp, sample_type)
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS spo2_samples (
id TEXT PRIMARY KEY,
provider TEXT NOT NULL,
source_type TEXT NOT NULL,
source_record_id TEXT,
user_id TEXT NOT NULL,
device_id TEXT,
timezone TEXT DEFAULT 'UTC',
collected_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
timestamp TIMESTAMP NOT NULL,
spo2_pct INTEGER NOT NULL,
UNIQUE(user_id, timestamp)
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS stress_samples (
id TEXT PRIMARY KEY,
provider TEXT NOT NULL,
source_type TEXT NOT NULL,
source_record_id TEXT,
user_id TEXT NOT NULL,
device_id TEXT,
timezone TEXT DEFAULT 'UTC',
collected_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
timestamp TIMESTAMP NOT NULL,
stress_score INTEGER NOT NULL,
level TEXT NOT NULL,
UNIQUE(user_id, timestamp)
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS abnormal_heart_beat_events (
id TEXT PRIMARY KEY,
provider TEXT NOT NULL,
source_type TEXT NOT NULL,
source_record_id TEXT,
user_id TEXT NOT NULL,
device_id TEXT,
timezone TEXT DEFAULT 'UTC',
collected_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
event_id TEXT NOT NULL,
start_at TIMESTAMP NOT NULL,
end_at TIMESTAMP NOT NULL,
duration_seconds INTEGER NOT NULL,
UNIQUE(user_id, event_id)
)
""")
# 同步状态表
conn.execute("""
CREATE TABLE IF NOT EXISTS sync_state (
id INTEGER PRIMARY KEY AUTOINCREMENT,
data_type TEXT NOT NULL UNIQUE,
last_sync_at TIMESTAMP,
last_record_timestamp TIMESTAMP,
records_count INTEGER DEFAULT 0
)
""")
# 创建索引以提升查询性能
conn.execute("""
CREATE INDEX IF NOT EXISTS idx_activity_user_date
ON daily_activity(user_id, date)
""")
conn.execute("""
CREATE INDEX IF NOT EXISTS idx_sleep_user_start
ON sleep_sessions(user_id, start_at)
""")
conn.execute("""
CREATE INDEX IF NOT EXISTS idx_workouts_user_start
ON workouts(user_id, start_at)
""")
conn.execute("""
CREATE INDEX IF NOT EXISTS idx_measurements_user_ts
ON body_measurements(user_id, timestamp)
""")
conn.execute("""
CREATE INDEX IF NOT EXISTS idx_heart_rate_user_ts
ON heart_rate_samples(user_id, timestamp)
""")
conn.execute("""
CREATE INDEX IF NOT EXISTS idx_spo2_user_ts
ON spo2_samples(user_id, timestamp)
""")
conn.execute("""
CREATE INDEX IF NOT EXISTS idx_stress_user_ts
ON stress_samples(user_id, timestamp)
""")
conn.execute("""
CREATE INDEX IF NOT EXISTS idx_abnormal_hr_user_start
ON abnormal_heart_beat_events(user_id, start_at)
""")
conn.commit()
@contextmanager
def _get_connection(self):
"""Get database connection with row factory."""
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
try:
yield conn
finally:
conn.close()
def insert_daily_activity(self, activity: DailyActivity) -> bool:
"""Insert or update daily activity record.
Returns:
True if record was inserted, False if updated
"""
with self._get_connection() as conn:
cursor = conn.execute(
"""
INSERT INTO daily_activity (
id, provider, source_type, source_record_id, user_id, device_id,
timezone, collected_at, date, steps, distance_m, active_kcal,
total_kcal, floors, active_minutes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
timezone = excluded.timezone,
collected_at = excluded.collected_at,
steps = excluded.steps,
distance_m = excluded.distance_m,
active_kcal = excluded.active_kcal,
total_kcal = excluded.total_kcal,
floors = excluded.floors,
active_minutes = excluded.active_minutes,
updated_at = CURRENT_TIMESTAMP
""",
(
activity.id,
activity.provider,
activity.source_type,
activity.source_record_id,
activity.user_id,
activity.device_id,
activity.timezone,
activity.collected_at.isoformat() if activity.collected_at else None,
activity.date,
activity.steps,
activity.distance_m,
activity.active_kcal,
activity.total_kcal,
activity.floors,
activity.active_minutes,
),
)
conn.commit()
return cursor.rowcount > 0
def insert_sleep_session(self, sleep: SleepSession) -> bool:
"""Insert or update sleep session record."""
with self._get_connection() as conn:
cursor = conn.execute(
"""
INSERT INTO sleep_sessions (
id, provider, source_type, source_record_id, user_id, device_id,
timezone, collected_at, sleep_id, start_at, end_at, duration_minutes,
time_asleep_minutes, time_awake_minutes, sleep_score, is_nap, stages
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(user_id, sleep_id) DO UPDATE SET
duration_minutes = excluded.duration_minutes,
time_asleep_minutes = excluded.time_asleep_minutes,
time_awake_minutes = excluded.time_awake_minutes,
sleep_score = excluded.sleep_score,
stages = excluded.stages,
updated_at = CURRENT_TIMESTAMP
""",
(
sleep.id,
sleep.provider,
sleep.source_type,
sleep.source_record_id,
sleep.user_id,
sleep.device_id,
sleep.timezone,
sleep.collected_at.isoformat() if sleep.collected_at else None,
sleep.sleep_id,
sleep.start_at.isoformat(),
sleep.end_at.isoformat(),
sleep.duration_minutes,
sleep.time_asleep_minutes,
sleep.time_awake_minutes,
sleep.sleep_score,
sleep.is_nap,
json.dumps([s.model_dump() for s in sleep.stages]),
),
)
conn.commit()
return cursor.rowcount > 0
def insert_workout(self, workout: Workout) -> bool:
"""Insert or update workout record."""
with self._get_connection() as conn:
cursor = conn.execute(
"""
INSERT INTO workouts (
id, provider, source_type, source_record_id, user_id, device_id,
timezone, collected_at, workout_id, activity_type, start_at, end_at,
duration_minutes, distance_m, calories_kcal, avg_heart_rate_bpm,
max_heart_rate_bpm, avg_pace_sec_per_km, max_pace_sec_per_km, total_steps
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(user_id, workout_id) DO UPDATE SET
duration_minutes = excluded.duration_minutes,
distance_m = excluded.distance_m,
calories_kcal = excluded.calories_kcal,
avg_heart_rate_bpm = excluded.avg_heart_rate_bpm,
max_heart_rate_bpm = excluded.max_heart_rate_bpm,
updated_at = CURRENT_TIMESTAMP
""",
(
workout.id,
workout.provider,
workout.source_type,
workout.source_record_id,
workout.user_id,
workout.device_id,
workout.timezone,
workout.collected_at.isoformat() if workout.collected_at else None,
workout.workout_id,
workout.activity_type,
workout.start_at.isoformat(),
workout.end_at.isoformat(),
workout.duration_minutes,
workout.distance_m,
workout.calories_kcal,
workout.avg_heart_rate_bpm,
workout.max_heart_rate_bpm,
workout.avg_pace_sec_per_km,
workout.max_pace_sec_per_km,
workout.total_steps,
),
)
conn.commit()
return cursor.rowcount > 0
def insert_body_measurement(self, measurement: BodyMeasurement) -> bool:
"""Insert or update body measurement record."""
with self._get_connection() as conn:
cursor = conn.execute(
"""
INSERT INTO body_measurements (
id, provider, source_type, source_record_id, user_id, device_id,
timezone, collected_at, timestamp, weight_kg, bmi, body_fat_pct,
muscle_mass_kg, water_pct, bone_mass_kg, visceral_fat_score,
basal_metabolism_kcal, metabolic_age
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
weight_kg = excluded.weight_kg,
bmi = excluded.bmi,
body_fat_pct = excluded.body_fat_pct,
muscle_mass_kg = excluded.muscle_mass_kg,
water_pct = excluded.water_pct,
updated_at = CURRENT_TIMESTAMP
""",
(
measurement.id,
measurement.provider,
measurement.source_type,
measurement.source_record_id,
measurement.user_id,
measurement.device_id,
measurement.timezone,
measurement.collected_at.isoformat() if measurement.collected_at else None,
measurement.timestamp.isoformat(),
measurement.weight_kg,
measurement.bmi,
measurement.body_fat_pct,
measurement.muscle_mass_kg,
measurement.water_pct,
measurement.bone_mass_kg,
measurement.visceral_fat_score,
measurement.basal_metabolism_kcal,
measurement.metabolic_age,
),
)
conn.commit()
return cursor.rowcount > 0
def insert_heart_rate_sample(self, sample: HeartRateSample) -> bool:
with self._get_connection() as conn:
cursor = conn.execute(
"""
INSERT INTO heart_rate_samples (
id, provider, source_type, source_record_id, user_id, device_id,
timezone, collected_at, timestamp, bpm, sample_type
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
timezone = excluded.timezone,
collected_at = excluded.collected_at,
timestamp = excluded.timestamp,
bpm = excluded.bpm,
sample_type = excluded.sample_type,
updated_at = CURRENT_TIMESTAMP
""",
(
sample.id,
sample.provider,
sample.source_type,
sample.source_record_id,
sample.user_id,
sample.device_id,
sample.timezone,
sample.collected_at.isoformat() if sample.collected_at else None,
sample.timestamp.isoformat(),
sample.bpm,
sample.sample_type,
),
)
conn.commit()
return cursor.rowcount > 0
def insert_spo2_sample(self, sample: SpO2Sample) -> bool:
with self._get_connection() as conn:
cursor = conn.execute(
"""
INSERT INTO spo2_samples (
id, provider, source_type, source_record_id, user_id, device_id,
timezone, collected_at, timestamp, spo2_pct
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
timezone = excluded.timezone,
collected_at = excluded.collected_at,
timestamp = excluded.timestamp,
spo2_pct = excluded.spo2_pct,
updated_at = CURRENT_TIMESTAMP
""",
(
sample.id,
sample.provider,
sample.source_type,
sample.source_record_id,
sample.user_id,
sample.device_id,
sample.timezone,
sample.collected_at.isoformat() if sample.collected_at else None,
sample.timestamp.isoformat(),
sample.spo2_pct,
),
)
conn.commit()
return cursor.rowcount > 0
def insert_stress_sample(self, sample: StressSample) -> bool:
with self._get_connection() as conn:
cursor = conn.execute(
"""
INSERT INTO stress_samples (
id, provider, source_type, source_record_id, user_id, device_id,
timezone, collected_at, timestamp, stress_score, level
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
timezone = excluded.timezone,
collected_at = excluded.collected_at,
timestamp = excluded.timestamp,
stress_score = excluded.stress_score,
level = excluded.level,
updated_at = CURRENT_TIMESTAMP
""",
(
sample.id,
sample.provider,
sample.source_type,
sample.source_record_id,
sample.user_id,
sample.device_id,
sample.timezone,
sample.collected_at.isoformat() if sample.collected_at else None,
sample.timestamp.isoformat(),
sample.stress_score,
sample.level,
),
)
conn.commit()
return cursor.rowcount > 0
def insert_abnormal_heart_beat_event(self, event: AbnormalHeartBeatEvent) -> bool:
with self._get_connection() as conn:
cursor = conn.execute(
"""
INSERT INTO abnormal_heart_beat_events (
id, provider, source_type, source_record_id, user_id, device_id,
timezone, collected_at, event_id, start_at, end_at, duration_seconds
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(user_id, event_id) DO UPDATE SET
timezone = excluded.timezone,
collected_at = excluded.collected_at,
start_at = excluded.start_at,
end_at = excluded.end_at,
duration_seconds = excluded.duration_seconds,
updated_at = CURRENT_TIMESTAMP
""",
(
event.id,
event.provider,
event.source_type,
event.source_record_id,
event.user_id,
event.device_id,
event.timezone,
event.collected_at.isoformat() if event.collected_at else None,
event.event_id,
event.start_at.isoformat(),
event.end_at.isoformat(),
event.duration_seconds,
),
)
conn.commit()
return cursor.rowcount > 0
def update_sync_state(self, data_type: str, last_record_ts: datetime | None = None) -> None:
"""Update sync state for a data type."""
with self._get_connection() as conn:
conn.execute(
"""
INSERT INTO sync_state (data_type, last_sync_at, last_record_timestamp)
VALUES (?, CURRENT_TIMESTAMP, ?)
ON CONFLICT(data_type) DO UPDATE SET
last_sync_at = CURRENT_TIMESTAMP,
last_record_timestamp = excluded.last_record_timestamp
""",
(data_type, last_record_ts.isoformat() if last_record_ts else None),
)
conn.commit()
def get_sync_state(self, data_type: str) -> dict[str, Any] | None:
"""Get sync state for a data type."""
with self._get_connection() as conn:
row = conn.execute(
"SELECT * FROM sync_state WHERE data_type = ?",
(data_type,),
).fetchone()
return dict(row) if row else None
def query_daily_activity(
self,
user_id: str,
start_date: str,
end_date: str,
) -> list[dict[str, Any]]:
"""Query daily activity records."""
with self._get_connection() as conn:
rows = conn.execute(
"""
SELECT * FROM daily_activity
WHERE user_id = ? AND date >= ? AND date <= ?
ORDER BY date
""",
(user_id, start_date, end_date),
).fetchall()
return [dict(row) for row in rows]
def query_sleep_sessions(
self,
user_id: str,
start_date: str,
end_date: str,
) -> list[dict[str, Any]]:
"""Query sleep session records."""
with self._get_connection() as conn:
rows = conn.execute(
"""
SELECT * FROM sleep_sessions
WHERE user_id = ?
AND substr(start_at, 1, 10) >= ? AND substr(start_at, 1, 10) <= ?
ORDER BY start_at
""",
(user_id, start_date, end_date),
).fetchall()
return [dict(row) for row in rows]
def query_workouts(
self,
user_id: str,
start_date: str,
end_date: str,
) -> list[dict[str, Any]]:
"""Query workout records."""
with self._get_connection() as conn:
rows = conn.execute(
"""
SELECT * FROM workouts
WHERE user_id = ?
AND substr(start_at, 1, 10) >= ? AND substr(start_at, 1, 10) <= ?
ORDER BY start_at
""",
(user_id, start_date, end_date),
).fetchall()
return [dict(row) for row in rows]
def query_body_measurements(
self,
user_id: str,
start_date: str,
end_date: str,
) -> list[dict[str, Any]]:
"""Query body measurement records."""
with self._get_connection() as conn:
rows = conn.execute(
"""
SELECT * FROM body_measurements
WHERE user_id = ?
AND substr(timestamp, 1, 10) >= ? AND substr(timestamp, 1, 10) <= ?
ORDER BY timestamp
""",
(user_id, start_date, end_date),
).fetchall()
return [dict(row) for row in rows]
def query_heart_rate_samples(
self,
user_id: str,
start_date: str,
end_date: str,
) -> list[dict[str, Any]]:
with self._get_connection() as conn:
rows = conn.execute(
"""
SELECT * FROM heart_rate_samples
WHERE user_id = ?
AND substr(timestamp, 1, 10) >= ? AND substr(timestamp, 1, 10) <= ?
ORDER BY timestamp
""",
(user_id, start_date, end_date),
).fetchall()
return [dict(row) for row in rows]
def query_spo2_samples(
self,
user_id: str,
start_date: str,
end_date: str,
) -> list[dict[str, Any]]:
with self._get_connection() as conn:
rows = conn.execute(
"""
SELECT * FROM spo2_samples
WHERE user_id = ?
AND substr(timestamp, 1, 10) >= ? AND substr(timestamp, 1, 10) <= ?
ORDER BY timestamp
""",
(user_id, start_date, end_date),
).fetchall()
return [dict(row) for row in rows]
def query_stress_samples(
self,
user_id: str,
start_date: str,
end_date: str,
) -> list[dict[str, Any]]:
with self._get_connection() as conn:
rows = conn.execute(
"""
SELECT * FROM stress_samples
WHERE user_id = ?
AND substr(timestamp, 1, 10) >= ? AND substr(timestamp, 1, 10) <= ?
ORDER BY timestamp
""",
(user_id, start_date, end_date),
).fetchall()
return [dict(row) for row in rows]
def query_abnormal_heart_beat_events(
self,
user_id: str,
start_date: str,
end_date: str,
) -> list[dict[str, Any]]:
with self._get_connection() as conn:
rows = conn.execute(
"""
SELECT * FROM abnormal_heart_beat_events
WHERE user_id = ?
AND substr(start_at, 1, 10) >= ? AND substr(start_at, 1, 10) <= ?
ORDER BY start_at
""",
(user_id, start_date, end_date),
).fetchall()
return [dict(row) for row in rows]
def get_data_coverage(self, user_id: str) -> list[dict[str, Any]]:
"""Get data coverage statistics."""
with self._get_connection() as conn:
results = []
# 日常活动覆盖范围
row = conn.execute(
"""
SELECT
MIN(date) as first_date,
MAX(date) as last_date,
COUNT(DISTINCT date) as days_with_data
FROM daily_activity
WHERE user_id = ?
""",
(user_id,),
).fetchone()
if row and row["first_date"]:
results.append(
{
"data_type": "daily_activity",
**dict(row),
}
)
# 睡眠覆盖范围
row = conn.execute(
"""
SELECT
MIN(substr(start_at, 1, 10)) as first_date,
MAX(substr(start_at, 1, 10)) as last_date,
COUNT(DISTINCT substr(start_at, 1, 10)) as days_with_data
FROM sleep_sessions
WHERE user_id = ?
""",
(user_id,),
).fetchone()
if row and row["first_date"]:
results.append(
{
"data_type": "sleep",
**dict(row),
}
)
# 运动记录覆盖范围
row = conn.execute(
"""
SELECT
MIN(substr(start_at, 1, 10)) as first_date,
MAX(substr(start_at, 1, 10)) as last_date,
COUNT(DISTINCT substr(start_at, 1, 10)) as days_with_data
FROM workouts
WHERE user_id = ?
""",
(user_id,),
).fetchone()
if row and row["first_date"]:
results.append(
{
"data_type": "workouts",
**dict(row),
}
)
# 身体测量覆盖范围
row = conn.execute(
"""
SELECT
MIN(substr(timestamp, 1, 10)) as first_date,
MAX(substr(timestamp, 1, 10)) as last_date,
COUNT(DISTINCT substr(timestamp, 1, 10)) as days_with_data
FROM body_measurements
WHERE user_id = ?
""",
(user_id,),
).fetchone()
if row and row["first_date"]:
results.append(
{
"data_type": "body_measurements",
**dict(row),
}
)
row = conn.execute(
"""
SELECT
MIN(substr(timestamp, 1, 10)) as first_date,
MAX(substr(timestamp, 1, 10)) as last_date,
COUNT(DISTINCT substr(timestamp, 1, 10)) as days_with_data
FROM heart_rate_samples
WHERE user_id = ?
""",
(user_id,),
).fetchone()
if row and row["first_date"]:
results.append(
{
"data_type": "heart_rate",
**dict(row),
}
)
for data_type, table_name, date_expr in [
("spo2", "spo2_samples", "timestamp"),
("stress", "stress_samples", "timestamp"),
("abnormal_heart_beat", "abnormal_heart_beat_events", "start_at"),
]:
row = conn.execute(
f"""
SELECT
MIN(date({date_expr})) as first_date,
MAX(date({date_expr})) as last_date,
COUNT(DISTINCT date({date_expr})) as days_with_data
FROM {table_name}
WHERE user_id = ?
""",
(user_id,),
).fetchone()
if row and row["first_date"]:
results.append({"data_type": data_type, **dict(row)})
return results