db.py 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. from __future__ import annotations
  2. import os
  3. from typing import Any
  4. from sqlalchemy import Integer, JSON, String, create_engine, select, text
  5. from sqlalchemy.engine import Engine
  6. from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column
  7. class Base(DeclarativeBase):
  8. pass
  9. class SysConfig(Base):
  10. __tablename__ = "sys_config"
  11. id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
  12. key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False)
  13. value: Mapped[Any] = mapped_column(JSON, nullable=False)
  14. _ENGINE: Engine | None = None
  15. _ENGINE_URL: str | None = None
  16. def database_url() -> str:
  17. raw_value = str(os.getenv("DATABASE_URL") or "").strip()
  18. if not raw_value:
  19. raise ValueError("DATABASE_URL is required and must point to PostgreSQL")
  20. if raw_value.startswith("sqlite:"):
  21. raise ValueError("SQLite is not supported; use PostgreSQL DATABASE_URL")
  22. return raw_value
  23. def sql_engine() -> Engine:
  24. global _ENGINE, _ENGINE_URL
  25. effective_url = database_url()
  26. if _ENGINE is not None and _ENGINE_URL == effective_url:
  27. return _ENGINE
  28. _ENGINE = create_engine(
  29. effective_url,
  30. future=True,
  31. pool_pre_ping=True,
  32. pool_recycle=1800,
  33. )
  34. _ENGINE_URL = effective_url
  35. return _ENGINE
  36. def check_database_connection() -> None:
  37. engine = sql_engine()
  38. with engine.connect() as connection:
  39. connection.execute(text("SELECT 1"))
  40. def read_sys_config_value(config_key: str) -> Any | None:
  41. try:
  42. engine = sql_engine()
  43. Base.metadata.create_all(engine, checkfirst=True)
  44. with Session(engine) as session:
  45. return session.scalar(select(SysConfig.value).where(SysConfig.key == config_key))
  46. except Exception as exc:
  47. raise ValueError(f"failed to read sys_config key={config_key}: {exc}") from exc
  48. def write_sys_config_value(config_key: str, value: Any) -> None:
  49. try:
  50. engine = sql_engine()
  51. Base.metadata.create_all(engine, checkfirst=True)
  52. with Session(engine) as session:
  53. row = session.scalar(select(SysConfig).where(SysConfig.key == config_key))
  54. if row is None:
  55. session.add(SysConfig(key=config_key, value=value))
  56. else:
  57. row.value = value
  58. session.commit()
  59. except Exception as exc:
  60. raise ValueError(f"failed to write sys_config key={config_key}: {exc}") from exc