database.py 12 KB

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