database.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
  2. from sqlalchemy.orm import DeclarativeBase
  3. from backend.app.core.config import settings
  4. engine = create_async_engine(
  5. settings.database_url,
  6. echo=settings.debug,
  7. )
  8. async_session = async_sessionmaker(
  9. engine,
  10. class_=AsyncSession,
  11. expire_on_commit=False,
  12. )
  13. class Base(DeclarativeBase):
  14. pass
  15. async def get_db() -> AsyncSession:
  16. async with async_session() as session:
  17. try:
  18. yield session
  19. await session.commit()
  20. except Exception:
  21. await session.rollback()
  22. raise
  23. finally:
  24. await session.close()
  25. async def init_db():
  26. # Import models to register them with SQLAlchemy
  27. from backend.app.models import printer, archive, filament, settings, smart_plug, print_queue, notification, maintenance, kprofile_note, notification_template, external_link, project, api_key # noqa: F401
  28. async with engine.begin() as conn:
  29. await conn.run_sync(Base.metadata.create_all)
  30. # Run migrations for new columns (SQLite doesn't auto-add columns)
  31. await run_migrations(conn)
  32. # Seed default notification templates
  33. await seed_notification_templates()
  34. async def run_migrations(conn):
  35. """Add new columns to existing tables if they don't exist."""
  36. from sqlalchemy import text
  37. # Migration: Add is_favorite column to print_archives
  38. try:
  39. await conn.execute(text(
  40. "ALTER TABLE print_archives ADD COLUMN is_favorite BOOLEAN DEFAULT 0"
  41. ))
  42. except Exception:
  43. # Column already exists
  44. pass
  45. # Migration: Add content_hash column to print_archives for duplicate detection
  46. try:
  47. await conn.execute(text(
  48. "ALTER TABLE print_archives ADD COLUMN content_hash VARCHAR(64)"
  49. ))
  50. except Exception:
  51. # Column already exists
  52. pass
  53. # Migration: Add auto_off_executed column to smart_plugs
  54. try:
  55. await conn.execute(text(
  56. "ALTER TABLE smart_plugs ADD COLUMN auto_off_executed BOOLEAN DEFAULT 0"
  57. ))
  58. except Exception:
  59. # Column already exists
  60. pass
  61. # Migration: Add on_print_stopped column to notification_providers
  62. try:
  63. await conn.execute(text(
  64. "ALTER TABLE notification_providers ADD COLUMN on_print_stopped BOOLEAN DEFAULT 1"
  65. ))
  66. except Exception:
  67. # Column already exists
  68. pass
  69. # Migration: Add source_3mf_path column to print_archives
  70. try:
  71. await conn.execute(text(
  72. "ALTER TABLE print_archives ADD COLUMN source_3mf_path VARCHAR(500)"
  73. ))
  74. except Exception:
  75. # Column already exists
  76. pass
  77. # Migration: Add on_maintenance_due column to notification_providers
  78. try:
  79. await conn.execute(text(
  80. "ALTER TABLE notification_providers ADD COLUMN on_maintenance_due BOOLEAN DEFAULT 0"
  81. ))
  82. except Exception:
  83. # Column already exists
  84. pass
  85. # Migration: Add location column to printers for grouping
  86. try:
  87. await conn.execute(text(
  88. "ALTER TABLE printers ADD COLUMN location VARCHAR(100)"
  89. ))
  90. except Exception:
  91. # Column already exists
  92. pass
  93. # Migration: Add interval_type column to maintenance_types
  94. try:
  95. await conn.execute(text(
  96. "ALTER TABLE maintenance_types ADD COLUMN interval_type VARCHAR(20) DEFAULT 'hours'"
  97. ))
  98. except Exception:
  99. # Column already exists
  100. pass
  101. # Migration: Add custom_interval_type column to printer_maintenance
  102. try:
  103. await conn.execute(text(
  104. "ALTER TABLE printer_maintenance ADD COLUMN custom_interval_type VARCHAR(20)"
  105. ))
  106. except Exception:
  107. # Column already exists
  108. pass
  109. # Migration: Add power alert columns to smart_plugs
  110. try:
  111. await conn.execute(text(
  112. "ALTER TABLE smart_plugs ADD COLUMN power_alert_enabled BOOLEAN DEFAULT 0"
  113. ))
  114. except Exception:
  115. pass
  116. try:
  117. await conn.execute(text(
  118. "ALTER TABLE smart_plugs ADD COLUMN power_alert_high REAL"
  119. ))
  120. except Exception:
  121. pass
  122. try:
  123. await conn.execute(text(
  124. "ALTER TABLE smart_plugs ADD COLUMN power_alert_low REAL"
  125. ))
  126. except Exception:
  127. pass
  128. try:
  129. await conn.execute(text(
  130. "ALTER TABLE smart_plugs ADD COLUMN power_alert_last_triggered DATETIME"
  131. ))
  132. except Exception:
  133. pass
  134. # Migration: Add schedule columns to smart_plugs
  135. try:
  136. await conn.execute(text(
  137. "ALTER TABLE smart_plugs ADD COLUMN schedule_enabled BOOLEAN DEFAULT 0"
  138. ))
  139. except Exception:
  140. pass
  141. try:
  142. await conn.execute(text(
  143. "ALTER TABLE smart_plugs ADD COLUMN schedule_on_time VARCHAR(5)"
  144. ))
  145. except Exception:
  146. pass
  147. try:
  148. await conn.execute(text(
  149. "ALTER TABLE smart_plugs ADD COLUMN schedule_off_time VARCHAR(5)"
  150. ))
  151. except Exception:
  152. pass
  153. # Migration: Add daily digest columns to notification_providers
  154. try:
  155. await conn.execute(text(
  156. "ALTER TABLE notification_providers ADD COLUMN daily_digest_enabled BOOLEAN DEFAULT 0"
  157. ))
  158. except Exception:
  159. pass
  160. try:
  161. await conn.execute(text(
  162. "ALTER TABLE notification_providers ADD COLUMN daily_digest_time VARCHAR(5)"
  163. ))
  164. except Exception:
  165. pass
  166. # Migration: Add project_id column to print_archives
  167. try:
  168. await conn.execute(text(
  169. "ALTER TABLE print_archives ADD COLUMN project_id INTEGER REFERENCES projects(id) ON DELETE SET NULL"
  170. ))
  171. except Exception:
  172. pass
  173. # Migration: Add project_id column to print_queue
  174. try:
  175. await conn.execute(text(
  176. "ALTER TABLE print_queue ADD COLUMN project_id INTEGER REFERENCES projects(id) ON DELETE SET NULL"
  177. ))
  178. except Exception:
  179. pass
  180. # Migration: Create FTS5 virtual table for archive full-text search
  181. try:
  182. await conn.execute(text("""
  183. CREATE VIRTUAL TABLE IF NOT EXISTS archive_fts USING fts5(
  184. print_name,
  185. filename,
  186. tags,
  187. notes,
  188. designer,
  189. filament_type,
  190. content='print_archives',
  191. content_rowid='id'
  192. )
  193. """))
  194. except Exception:
  195. pass
  196. # Migration: Create triggers to keep FTS index in sync
  197. try:
  198. await conn.execute(text("""
  199. CREATE TRIGGER IF NOT EXISTS archive_fts_insert AFTER INSERT ON print_archives BEGIN
  200. INSERT INTO archive_fts(rowid, print_name, filename, tags, notes, designer, filament_type)
  201. VALUES (new.id, new.print_name, new.filename, new.tags, new.notes, new.designer, new.filament_type);
  202. END
  203. """))
  204. except Exception:
  205. pass
  206. try:
  207. await conn.execute(text("""
  208. CREATE TRIGGER IF NOT EXISTS archive_fts_delete AFTER DELETE ON print_archives BEGIN
  209. INSERT INTO archive_fts(archive_fts, rowid, print_name, filename, tags, notes, designer, filament_type)
  210. VALUES ('delete', old.id, old.print_name, old.filename, old.tags, old.notes, old.designer, old.filament_type);
  211. END
  212. """))
  213. except Exception:
  214. pass
  215. try:
  216. await conn.execute(text("""
  217. CREATE TRIGGER IF NOT EXISTS archive_fts_update AFTER UPDATE ON print_archives BEGIN
  218. INSERT INTO archive_fts(archive_fts, rowid, print_name, filename, tags, notes, designer, filament_type)
  219. VALUES ('delete', old.id, old.print_name, old.filename, old.tags, old.notes, old.designer, old.filament_type);
  220. INSERT INTO archive_fts(rowid, print_name, filename, tags, notes, designer, filament_type)
  221. VALUES (new.id, new.print_name, new.filename, new.tags, new.notes, new.designer, new.filament_type);
  222. END
  223. """))
  224. except Exception:
  225. pass
  226. # Migration: Add auto_off_pending columns to smart_plugs (for restart recovery)
  227. try:
  228. await conn.execute(text(
  229. "ALTER TABLE smart_plugs ADD COLUMN auto_off_pending BOOLEAN DEFAULT 0"
  230. ))
  231. except Exception:
  232. pass
  233. try:
  234. await conn.execute(text(
  235. "ALTER TABLE smart_plugs ADD COLUMN auto_off_pending_since DATETIME"
  236. ))
  237. except Exception:
  238. pass
  239. # Migration: Add AMS alarm notification columns to notification_providers
  240. try:
  241. await conn.execute(text(
  242. "ALTER TABLE notification_providers ADD COLUMN on_ams_humidity_high BOOLEAN DEFAULT 0"
  243. ))
  244. except Exception:
  245. pass
  246. try:
  247. await conn.execute(text(
  248. "ALTER TABLE notification_providers ADD COLUMN on_ams_temperature_high BOOLEAN DEFAULT 0"
  249. ))
  250. except Exception:
  251. pass
  252. # Migration: Add AMS-HT alarm notification columns to notification_providers
  253. try:
  254. await conn.execute(text(
  255. "ALTER TABLE notification_providers ADD COLUMN on_ams_ht_humidity_high BOOLEAN DEFAULT 0"
  256. ))
  257. except Exception:
  258. pass
  259. try:
  260. await conn.execute(text(
  261. "ALTER TABLE notification_providers ADD COLUMN on_ams_ht_temperature_high BOOLEAN DEFAULT 0"
  262. ))
  263. except Exception:
  264. pass
  265. async def seed_notification_templates():
  266. """Seed default notification templates if they don't exist."""
  267. from sqlalchemy import select
  268. from backend.app.models.notification_template import NotificationTemplate, DEFAULT_TEMPLATES
  269. async with async_session() as session:
  270. # Check if templates already exist
  271. result = await session.execute(select(NotificationTemplate).limit(1))
  272. if result.scalar_one_or_none() is not None:
  273. # Templates already seeded
  274. return
  275. # Insert default templates
  276. for template_data in DEFAULT_TEMPLATES:
  277. template = NotificationTemplate(
  278. event_type=template_data["event_type"],
  279. name=template_data["name"],
  280. title_template=template_data["title_template"],
  281. body_template=template_data["body_template"],
  282. is_default=True,
  283. )
  284. session.add(template)
  285. await session.commit()