@libredb/studio 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (572) hide show
  1. package/.claude/settings.local.json +127 -0
  2. package/.cursorrules +426 -0
  3. package/.devin/wiki.json +143 -0
  4. package/.dockerignore +80 -0
  5. package/.env.example +159 -0
  6. package/.github/ISSUE_TEMPLATE/bug_report.md +49 -0
  7. package/.github/ISSUE_TEMPLATE/feature_request.md +29 -0
  8. package/.github/PULL_REQUEST_TEMPLATE.md +57 -0
  9. package/.github/workflows/ci.yml +185 -0
  10. package/.github/workflows/codeql.yml +57 -0
  11. package/.github/workflows/docker-build-push.yml +118 -0
  12. package/.github/workflows/helm-release.yml +113 -0
  13. package/CLAUDE.md +265 -0
  14. package/CODE_OF_CONDUCT.md +124 -0
  15. package/CONTRIBUTING.md +154 -0
  16. package/Dockerfile +73 -0
  17. package/LICENSE +21 -0
  18. package/README.md +614 -0
  19. package/SECURITY.md +107 -0
  20. package/artifacthub-repo.yml +4 -0
  21. package/bun.lock +1714 -0
  22. package/bunfig.toml +3 -0
  23. package/charts/libredb-studio/.helmignore +11 -0
  24. package/charts/libredb-studio/Chart.lock +6 -0
  25. package/charts/libredb-studio/Chart.yaml +50 -0
  26. package/charts/libredb-studio/README.md +206 -0
  27. package/charts/libredb-studio/templates/NOTES.txt +59 -0
  28. package/charts/libredb-studio/templates/_helpers.tpl +135 -0
  29. package/charts/libredb-studio/templates/configmap.yaml +37 -0
  30. package/charts/libredb-studio/templates/deployment.yaml +184 -0
  31. package/charts/libredb-studio/templates/hpa.yaml +32 -0
  32. package/charts/libredb-studio/templates/ingress.yaml +41 -0
  33. package/charts/libredb-studio/templates/networkpolicy.yaml +50 -0
  34. package/charts/libredb-studio/templates/pdb.yaml +18 -0
  35. package/charts/libredb-studio/templates/pvc.yaml +23 -0
  36. package/charts/libredb-studio/templates/secret.yaml +30 -0
  37. package/charts/libredb-studio/templates/seed-configmap.yaml +11 -0
  38. package/charts/libredb-studio/templates/service.yaml +22 -0
  39. package/charts/libredb-studio/templates/serviceaccount.yaml +13 -0
  40. package/charts/libredb-studio/values.schema.json +246 -0
  41. package/charts/libredb-studio/values.yaml +286 -0
  42. package/components.json +22 -0
  43. package/conductor/code_styleguides/typescript.md +43 -0
  44. package/conductor/product-guidelines.md +43 -0
  45. package/conductor/product.md +3 -0
  46. package/conductor/setup_state.json +1 -0
  47. package/conductor/tech-stack.md +39 -0
  48. package/conductor/tracks/enhance_postgres_monitoring_20251227/metadata.json +8 -0
  49. package/conductor/tracks/enhance_postgres_monitoring_20251227/plan.md +44 -0
  50. package/conductor/tracks/enhance_postgres_monitoring_20251227/spec.md +31 -0
  51. package/conductor/tracks.md +8 -0
  52. package/conductor/workflow.md +333 -0
  53. package/database-compose.yml +55 -0
  54. package/docker/postgres-init/01-extensions.sql +10 -0
  55. package/docker/postgres-init/02-sample-data.sql +585 -0
  56. package/docker/postgres.yml +68 -0
  57. package/docker-compose.yml +38 -0
  58. package/docs/AI_PLAN.md +74 -0
  59. package/docs/API_DOCS.md +875 -0
  60. package/docs/ARCHITECTURE.md +218 -0
  61. package/docs/DATABASE_PROVIDERS.md +358 -0
  62. package/docs/FEATURES.md +116 -0
  63. package/docs/HELM_CHART.md +252 -0
  64. package/docs/LOGIN_PAGE.md +178 -0
  65. package/docs/MONACO_EDITOR_PERFORMANCE.md +315 -0
  66. package/docs/OIDC_ARCH.md +681 -0
  67. package/docs/OIDC_SETUP.md +322 -0
  68. package/docs/POSTGRES_METRICS.md +516 -0
  69. package/docs/QUERY_OPTIMIZATION.md +370 -0
  70. package/docs/SEED_CONNECTIONS.md +468 -0
  71. package/docs/SQL_ALIAS_COMPLETION.md +190 -0
  72. package/docs/STORAGE_ARCHITECTURE.md +565 -0
  73. package/docs/STORAGE_QUICK_SETUP.md +419 -0
  74. package/docs/TECHNICAL_PLAN.md +36 -0
  75. package/docs/THEMING.md +345 -0
  76. package/docs/adding-a-new-database-provider.md +642 -0
  77. package/docs/backlogs/000-PLATFORM_DATA_SYNC_DATABASE.md +360 -0
  78. package/docs/backlogs/001-INLINE_DATA_EDITING.md +118 -0
  79. package/docs/backlogs/002-DATA_IMPORT.md +215 -0
  80. package/docs/backlogs/003-QUERY_TIME_MACHINE.md +183 -0
  81. package/docs/backlogs/004-AI_DATA_STORYTELLER.md +292 -0
  82. package/docs/backlogs/005-QUERY_PLAYGROUND.md +352 -0
  83. package/docs/backlogs/006-DATA_MASKING.md +418 -0
  84. package/docs/enterprise-features.md +718 -0
  85. package/docs/kubernetes-helm-chart-artifacthub-plan.md +803 -0
  86. package/docs/medium-koyeb-article-en.md +215 -0
  87. package/docs/plans/test-plans.md +445 -0
  88. package/docs/releases/RELEASE.V0.3.0.md +22 -0
  89. package/docs/releases/RELEASE.V0.4.0.md +154 -0
  90. package/docs/releases/RELEASE.V0.5.0.md +252 -0
  91. package/docs/releases/RELEASE_v0.5.6.md +145 -0
  92. package/docs/releases/RELEASE_v0.6.1.md +303 -0
  93. package/docs/releases/RELEASE_v0.6.7.md +292 -0
  94. package/docs/releases/RELEASE_v0.7.0.md +332 -0
  95. package/docs/releases/RELEASE_v0.8.0.md +521 -0
  96. package/docs/sampledb/titanic.sql +1379 -0
  97. package/docs/superpowers/plans/2026-03-25-seed-connections.md +1362 -0
  98. package/docs/superpowers/specs/2026-03-25-seed-connections-design.md +590 -0
  99. package/e2e/admin-dashboard.spec.ts +64 -0
  100. package/e2e/connection-management.spec.ts +58 -0
  101. package/e2e/export.spec.ts +34 -0
  102. package/e2e/login.spec.ts +85 -0
  103. package/e2e/query-execution.spec.ts +35 -0
  104. package/e2e/tab-management.spec.ts +64 -0
  105. package/eslint.config.mjs +28 -0
  106. package/fly.toml +43 -0
  107. package/next.config.ts +32 -0
  108. package/package.json +130 -0
  109. package/playwright.config.ts +34 -0
  110. package/postcss.config.mjs +7 -0
  111. package/public/favicon-32x32.png +0 -0
  112. package/public/favicon.ico +0 -0
  113. package/public/file.svg +1 -0
  114. package/public/globe.svg +1 -0
  115. package/public/logo.svg +32 -0
  116. package/public/next.svg +1 -0
  117. package/public/screenshots/code-generator.png +0 -0
  118. package/public/screenshots/connection-modal.png +0 -0
  119. package/public/screenshots/data-profiler.png +0 -0
  120. package/public/screenshots/erd-diagram.png +0 -0
  121. package/public/screenshots/hero-editor.png +0 -0
  122. package/public/screenshots/nl2sql.png +0 -0
  123. package/public/vercel.svg +1 -0
  124. package/public/window.svg +1 -0
  125. package/render.yaml +58 -0
  126. package/scripts/merge-lcov.mjs +239 -0
  127. package/sonar-project.properties +16 -0
  128. package/src/app/admin/error.tsx +46 -0
  129. package/src/app/admin/page.tsx +10 -0
  130. package/src/app/api/admin/audit/route.ts +52 -0
  131. package/src/app/api/admin/fleet-health/route.ts +81 -0
  132. package/src/app/api/ai/autopilot/route.ts +105 -0
  133. package/src/app/api/ai/chat/route.ts +132 -0
  134. package/src/app/api/ai/describe-schema/route.ts +52 -0
  135. package/src/app/api/ai/explain/route.ts +86 -0
  136. package/src/app/api/ai/impact/route.ts +97 -0
  137. package/src/app/api/ai/index-advisor/route.ts +98 -0
  138. package/src/app/api/ai/nl2sql/route.ts +87 -0
  139. package/src/app/api/ai/query-safety/route.ts +87 -0
  140. package/src/app/api/auth/login/route.ts +62 -0
  141. package/src/app/api/auth/logout/route.ts +25 -0
  142. package/src/app/api/auth/me/route.ts +10 -0
  143. package/src/app/api/auth/oidc/callback/route.ts +82 -0
  144. package/src/app/api/auth/oidc/login/route.ts +43 -0
  145. package/src/app/api/connections/managed/route.ts +35 -0
  146. package/src/app/api/db/cancel/route.ts +42 -0
  147. package/src/app/api/db/disconnect/route.ts +28 -0
  148. package/src/app/api/db/health/route.ts +49 -0
  149. package/src/app/api/db/maintenance/route.ts +72 -0
  150. package/src/app/api/db/monitoring/route.ts +62 -0
  151. package/src/app/api/db/multi-query/route.ts +116 -0
  152. package/src/app/api/db/pool-stats/route.ts +37 -0
  153. package/src/app/api/db/profile/route.ts +144 -0
  154. package/src/app/api/db/provider-meta/route.ts +49 -0
  155. package/src/app/api/db/query/route.ts +50 -0
  156. package/src/app/api/db/schema/route.ts +47 -0
  157. package/src/app/api/db/schema-snapshot/route.ts +42 -0
  158. package/src/app/api/db/test-connection/route.ts +55 -0
  159. package/src/app/api/db/transaction/route.ts +111 -0
  160. package/src/app/api/storage/[collection]/route.ts +67 -0
  161. package/src/app/api/storage/config/route.ts +17 -0
  162. package/src/app/api/storage/migrate/route.ts +45 -0
  163. package/src/app/api/storage/route.ts +32 -0
  164. package/src/app/error.tsx +49 -0
  165. package/src/app/global-error.tsx +55 -0
  166. package/src/app/globals.css +146 -0
  167. package/src/app/icon.svg +42 -0
  168. package/src/app/layout.tsx +34 -0
  169. package/src/app/login/login-form.tsx +301 -0
  170. package/src/app/login/page.tsx +11 -0
  171. package/src/app/monitoring/page.tsx +8 -0
  172. package/src/app/not-found.tsx +29 -0
  173. package/src/app/page.tsx +5 -0
  174. package/src/components/AIAutopilotPanel.tsx +238 -0
  175. package/src/components/CodeGenerator.tsx +271 -0
  176. package/src/components/CommandPalette.tsx +227 -0
  177. package/src/components/ConnectionModal.tsx +759 -0
  178. package/src/components/CreateTableModal.tsx +281 -0
  179. package/src/components/DataCharts.tsx +962 -0
  180. package/src/components/DataImportModal.tsx +582 -0
  181. package/src/components/DataProfiler.tsx +335 -0
  182. package/src/components/DatabaseDocs.tsx +251 -0
  183. package/src/components/MaskingSettings.tsx +414 -0
  184. package/src/components/MobileNav.tsx +50 -0
  185. package/src/components/NL2SQLPanel.tsx +281 -0
  186. package/src/components/PivotTable.tsx +257 -0
  187. package/src/components/QueryEditor.tsx +760 -0
  188. package/src/components/QueryHistory.tsx +344 -0
  189. package/src/components/QuerySafetyDialog.tsx +290 -0
  190. package/src/components/ResultsGrid.tsx +644 -0
  191. package/src/components/SaveQueryModal.tsx +104 -0
  192. package/src/components/SavedQueries.tsx +128 -0
  193. package/src/components/SchemaDiagram.tsx +473 -0
  194. package/src/components/SchemaDiff.tsx +473 -0
  195. package/src/components/SnapshotTimeline.tsx +116 -0
  196. package/src/components/Studio.tsx +639 -0
  197. package/src/components/TestDataGenerator.tsx +261 -0
  198. package/src/components/VisualExplain.tsx +820 -0
  199. package/src/components/admin/AdminDashboard.tsx +163 -0
  200. package/src/components/admin/tabs/AuditTab.tsx +531 -0
  201. package/src/components/admin/tabs/MonitoringEmbed.tsx +11 -0
  202. package/src/components/admin/tabs/OperationsTab.tsx +646 -0
  203. package/src/components/admin/tabs/OverviewTab.tsx +1328 -0
  204. package/src/components/admin/tabs/SecurityTab.tsx +284 -0
  205. package/src/components/community-section.tsx +92 -0
  206. package/src/components/icons/db-icons.tsx +84 -0
  207. package/src/components/libredb-logo.tsx +61 -0
  208. package/src/components/monitoring/MonitoringDashboard.tsx +345 -0
  209. package/src/components/monitoring/tabs/MetricChart.tsx +82 -0
  210. package/src/components/monitoring/tabs/OverviewTab.tsx +263 -0
  211. package/src/components/monitoring/tabs/PerformanceTab.tsx +254 -0
  212. package/src/components/monitoring/tabs/PoolTab.tsx +174 -0
  213. package/src/components/monitoring/tabs/QueriesTab.tsx +287 -0
  214. package/src/components/monitoring/tabs/SessionsTab.tsx +316 -0
  215. package/src/components/monitoring/tabs/StorageTab.tsx +335 -0
  216. package/src/components/monitoring/tabs/TablesTab.tsx +300 -0
  217. package/src/components/results-grid/ResultCard.tsx +111 -0
  218. package/src/components/results-grid/RowDetailSheet.tsx +178 -0
  219. package/src/components/results-grid/StatsBar.tsx +201 -0
  220. package/src/components/results-grid/index.ts +1 -0
  221. package/src/components/results-grid/utils.ts +23 -0
  222. package/src/components/schema-explorer/ColumnList.tsx +53 -0
  223. package/src/components/schema-explorer/SchemaExplorer.tsx +182 -0
  224. package/src/components/schema-explorer/TableItem.tsx +210 -0
  225. package/src/components/schema-explorer/index.ts +1 -0
  226. package/src/components/sidebar/ConnectionItem.tsx +105 -0
  227. package/src/components/sidebar/ConnectionsList.tsx +62 -0
  228. package/src/components/sidebar/Sidebar.tsx +130 -0
  229. package/src/components/sidebar/index.ts +2 -0
  230. package/src/components/studio/BottomPanel.tsx +286 -0
  231. package/src/components/studio/QueryToolbar.tsx +180 -0
  232. package/src/components/studio/StudioDesktopHeader.tsx +114 -0
  233. package/src/components/studio/StudioMobileHeader.tsx +340 -0
  234. package/src/components/studio/StudioTabBar.tsx +82 -0
  235. package/src/components/studio/index.ts +5 -0
  236. package/src/components/ui/accordion.tsx +66 -0
  237. package/src/components/ui/alert-dialog.tsx +157 -0
  238. package/src/components/ui/alert.tsx +66 -0
  239. package/src/components/ui/aspect-ratio.tsx +11 -0
  240. package/src/components/ui/avatar.tsx +53 -0
  241. package/src/components/ui/badge.tsx +46 -0
  242. package/src/components/ui/breadcrumb.tsx +109 -0
  243. package/src/components/ui/button-group.tsx +83 -0
  244. package/src/components/ui/button.tsx +60 -0
  245. package/src/components/ui/calendar.tsx +216 -0
  246. package/src/components/ui/card.tsx +92 -0
  247. package/src/components/ui/carousel.tsx +241 -0
  248. package/src/components/ui/chart.tsx +357 -0
  249. package/src/components/ui/checkbox.tsx +32 -0
  250. package/src/components/ui/collapsible.tsx +33 -0
  251. package/src/components/ui/command.tsx +184 -0
  252. package/src/components/ui/context-menu.tsx +252 -0
  253. package/src/components/ui/dialog.tsx +143 -0
  254. package/src/components/ui/drawer.tsx +135 -0
  255. package/src/components/ui/dropdown-menu.tsx +257 -0
  256. package/src/components/ui/empty.tsx +104 -0
  257. package/src/components/ui/field.tsx +248 -0
  258. package/src/components/ui/form.tsx +167 -0
  259. package/src/components/ui/hover-card.tsx +44 -0
  260. package/src/components/ui/input-group.tsx +170 -0
  261. package/src/components/ui/input-otp.tsx +77 -0
  262. package/src/components/ui/input.tsx +21 -0
  263. package/src/components/ui/item.tsx +193 -0
  264. package/src/components/ui/kbd.tsx +28 -0
  265. package/src/components/ui/label.tsx +24 -0
  266. package/src/components/ui/menubar.tsx +276 -0
  267. package/src/components/ui/navigation-menu.tsx +168 -0
  268. package/src/components/ui/pagination.tsx +127 -0
  269. package/src/components/ui/popover.tsx +48 -0
  270. package/src/components/ui/progress.tsx +31 -0
  271. package/src/components/ui/radio-group.tsx +45 -0
  272. package/src/components/ui/resizable.tsx +56 -0
  273. package/src/components/ui/scroll-area.tsx +58 -0
  274. package/src/components/ui/select.tsx +187 -0
  275. package/src/components/ui/separator.tsx +28 -0
  276. package/src/components/ui/sheet.tsx +139 -0
  277. package/src/components/ui/sidebar.tsx +726 -0
  278. package/src/components/ui/skeleton.tsx +13 -0
  279. package/src/components/ui/slider.tsx +63 -0
  280. package/src/components/ui/sonner.tsx +40 -0
  281. package/src/components/ui/spinner.tsx +16 -0
  282. package/src/components/ui/switch.tsx +31 -0
  283. package/src/components/ui/table.tsx +116 -0
  284. package/src/components/ui/tabs.tsx +66 -0
  285. package/src/components/ui/textarea.tsx +18 -0
  286. package/src/components/ui/toggle-group.tsx +83 -0
  287. package/src/components/ui/toggle.tsx +47 -0
  288. package/src/components/ui/tooltip.tsx +61 -0
  289. package/src/exports/components.ts +15 -0
  290. package/src/exports/index.ts +4 -0
  291. package/src/exports/providers.ts +4 -0
  292. package/src/exports/types.ts +26 -0
  293. package/src/hooks/use-ai-chat.ts +182 -0
  294. package/src/hooks/use-all-connections.ts +66 -0
  295. package/src/hooks/use-api-call.ts +71 -0
  296. package/src/hooks/use-auth.ts +51 -0
  297. package/src/hooks/use-connection-form.ts +349 -0
  298. package/src/hooks/use-connection-manager.ts +169 -0
  299. package/src/hooks/use-connection-payload.ts +15 -0
  300. package/src/hooks/use-inline-editing.ts +109 -0
  301. package/src/hooks/use-mobile.ts +20 -0
  302. package/src/hooks/use-monitoring-data.ts +270 -0
  303. package/src/hooks/use-provider-metadata.ts +62 -0
  304. package/src/hooks/use-query-execution.ts +478 -0
  305. package/src/hooks/use-storage-sync.ts +259 -0
  306. package/src/hooks/use-tab-manager.ts +231 -0
  307. package/src/hooks/use-toast.ts +20 -0
  308. package/src/hooks/use-transaction-control.ts +64 -0
  309. package/src/lib/api/error-codes.ts +30 -0
  310. package/src/lib/api/errors.ts +236 -0
  311. package/src/lib/api/with-error-handler.ts +41 -0
  312. package/src/lib/audit.ts +105 -0
  313. package/src/lib/auth.ts +87 -0
  314. package/src/lib/connection-string-parser.ts +172 -0
  315. package/src/lib/data-masking.ts +385 -0
  316. package/src/lib/db/base-provider.ts +325 -0
  317. package/src/lib/db/errors.ts +317 -0
  318. package/src/lib/db/factory.ts +324 -0
  319. package/src/lib/db/index.ts +123 -0
  320. package/src/lib/db/providers/document/index.ts +6 -0
  321. package/src/lib/db/providers/document/mongodb.ts +992 -0
  322. package/src/lib/db/providers/keyvalue/redis.ts +554 -0
  323. package/src/lib/db/providers/sql/index.ts +11 -0
  324. package/src/lib/db/providers/sql/mssql.ts +1065 -0
  325. package/src/lib/db/providers/sql/mysql.ts +978 -0
  326. package/src/lib/db/providers/sql/oracle.ts +1044 -0
  327. package/src/lib/db/providers/sql/postgres.ts +1179 -0
  328. package/src/lib/db/providers/sql/sql-base.ts +174 -0
  329. package/src/lib/db/providers/sql/sqlite.ts +721 -0
  330. package/src/lib/db/types.ts +437 -0
  331. package/src/lib/db/utils/pool-manager.ts +287 -0
  332. package/src/lib/db/utils/query-limiter.ts +239 -0
  333. package/src/lib/db-ui-config.ts +86 -0
  334. package/src/lib/editor/mongodb-completions.ts +172 -0
  335. package/src/lib/editor/sql-completions.ts +280 -0
  336. package/src/lib/llm/base-provider.ts +117 -0
  337. package/src/lib/llm/factory.ts +102 -0
  338. package/src/lib/llm/index.ts +90 -0
  339. package/src/lib/llm/providers/custom.ts +181 -0
  340. package/src/lib/llm/providers/gemini.ts +126 -0
  341. package/src/lib/llm/providers/ollama.ts +154 -0
  342. package/src/lib/llm/providers/openai.ts +146 -0
  343. package/src/lib/llm/types.ts +173 -0
  344. package/src/lib/llm/utils/config.ts +187 -0
  345. package/src/lib/llm/utils/retry.ts +119 -0
  346. package/src/lib/llm/utils/streaming.ts +202 -0
  347. package/src/lib/logger.ts +127 -0
  348. package/src/lib/monitoring-thresholds.ts +44 -0
  349. package/src/lib/oidc.ts +262 -0
  350. package/src/lib/query-generators.ts +61 -0
  351. package/src/lib/schema-diff/diff-engine.ts +273 -0
  352. package/src/lib/schema-diff/migration-generator.ts +208 -0
  353. package/src/lib/schema-diff/types.ts +55 -0
  354. package/src/lib/seed/config-loader.ts +79 -0
  355. package/src/lib/seed/connection-filter.ts +49 -0
  356. package/src/lib/seed/credential-resolver.ts +62 -0
  357. package/src/lib/seed/index.ts +40 -0
  358. package/src/lib/seed/resolve-connection.ts +57 -0
  359. package/src/lib/seed/types.ts +69 -0
  360. package/src/lib/sql/alias-extractor.ts +267 -0
  361. package/src/lib/sql/index.ts +8 -0
  362. package/src/lib/sql/statement-splitter.ts +167 -0
  363. package/src/lib/sql/types.ts +40 -0
  364. package/src/lib/ssh/tunnel.ts +142 -0
  365. package/src/lib/storage/factory.ts +84 -0
  366. package/src/lib/storage/index.ts +14 -0
  367. package/src/lib/storage/local-storage.ts +99 -0
  368. package/src/lib/storage/providers/postgres.ts +225 -0
  369. package/src/lib/storage/providers/sqlite.ts +153 -0
  370. package/src/lib/storage/storage-facade.ts +272 -0
  371. package/src/lib/storage/types.ts +75 -0
  372. package/src/lib/time-series-buffer.ts +58 -0
  373. package/src/lib/types.ts +173 -0
  374. package/src/lib/utils.ts +6 -0
  375. package/src/proxy.ts +104 -0
  376. package/src/types/db-drivers.d.ts +23 -0
  377. package/src/types/html2canvas.d.ts +9 -0
  378. package/tests/api/admin/audit.test.ts +178 -0
  379. package/tests/api/admin/fleet-health.test.ts +183 -0
  380. package/tests/api/ai/autopilot.test.ts +174 -0
  381. package/tests/api/ai/chat.test.ts +250 -0
  382. package/tests/api/ai/describe-schema.test.ts +266 -0
  383. package/tests/api/ai/explain.test.ts +199 -0
  384. package/tests/api/ai/impact.test.ts +168 -0
  385. package/tests/api/ai/index-advisor.test.ts +171 -0
  386. package/tests/api/ai/nl2sql.test.ts +202 -0
  387. package/tests/api/ai/query-safety.test.ts +196 -0
  388. package/tests/api/auth/login.test.ts +170 -0
  389. package/tests/api/auth/logout.test.ts +140 -0
  390. package/tests/api/auth/me.test.ts +73 -0
  391. package/tests/api/auth/oidc-callback.test.ts +215 -0
  392. package/tests/api/auth/oidc-login.test.ts +127 -0
  393. package/tests/api/db/cancel.test.ts +198 -0
  394. package/tests/api/db/disconnect.test.ts +124 -0
  395. package/tests/api/db/health.test.ts +222 -0
  396. package/tests/api/db/maintenance.test.ts +263 -0
  397. package/tests/api/db/monitoring.test.ts +221 -0
  398. package/tests/api/db/multi-query.test.ts +316 -0
  399. package/tests/api/db/pool-stats.test.ts +135 -0
  400. package/tests/api/db/profile.test.ts +330 -0
  401. package/tests/api/db/provider-meta.test.ts +193 -0
  402. package/tests/api/db/query.test.ts +314 -0
  403. package/tests/api/db/schema-snapshot.test.ts +170 -0
  404. package/tests/api/db/schema.test.ts +191 -0
  405. package/tests/api/db/test-connection.test.ts +185 -0
  406. package/tests/api/db/transaction.test.ts +314 -0
  407. package/tests/api/proxy.test.ts +191 -0
  408. package/tests/api/seed/managed-route.test.ts +113 -0
  409. package/tests/api/storage/config.test.ts +42 -0
  410. package/tests/api/storage/storage-routes.test.ts +309 -0
  411. package/tests/components/AIAutopilotPanel.test.tsx +756 -0
  412. package/tests/components/AdminPage.test.tsx +33 -0
  413. package/tests/components/CodeGenerator.test.tsx +182 -0
  414. package/tests/components/CommandPalette.test.tsx +428 -0
  415. package/tests/components/CommunitySection.test.tsx +91 -0
  416. package/tests/components/ConnectionModal.mobile.test.tsx +284 -0
  417. package/tests/components/ConnectionModal.test.tsx +570 -0
  418. package/tests/components/CreateTableModal.test.tsx +383 -0
  419. package/tests/components/DataCharts.test.tsx +739 -0
  420. package/tests/components/DataImportModal.test.tsx +751 -0
  421. package/tests/components/DataProfiler.test.tsx +589 -0
  422. package/tests/components/DatabaseDocs.test.tsx +353 -0
  423. package/tests/components/LoginPage.test.tsx +163 -0
  424. package/tests/components/LoginPageOIDC.test.tsx +92 -0
  425. package/tests/components/MaskingSettings.test.tsx +498 -0
  426. package/tests/components/MobileNav.test.tsx +30 -0
  427. package/tests/components/MonitoringPage.test.tsx +32 -0
  428. package/tests/components/NL2SQLPanel.test.tsx +621 -0
  429. package/tests/components/Page.test.tsx +33 -0
  430. package/tests/components/PivotTable.test.tsx +350 -0
  431. package/tests/components/QueryEditor.test.tsx +1730 -0
  432. package/tests/components/QueryHistory.test.tsx +572 -0
  433. package/tests/components/QuerySafetyDialog.test.tsx +586 -0
  434. package/tests/components/ResultsGrid.test.tsx +804 -0
  435. package/tests/components/RootLayout.test.tsx +83 -0
  436. package/tests/components/SaveQueryModal.test.tsx +25 -0
  437. package/tests/components/SavedQueries.test.tsx +43 -0
  438. package/tests/components/SchemaDiagram.test.tsx +1034 -0
  439. package/tests/components/SchemaDiff.test.tsx +906 -0
  440. package/tests/components/SnapshotTimeline.test.tsx +174 -0
  441. package/tests/components/Studio.test.tsx +1030 -0
  442. package/tests/components/TestDataGenerator.test.tsx +291 -0
  443. package/tests/components/VisualExplain.test.tsx +704 -0
  444. package/tests/components/admin/AdminDashboard.test.tsx +205 -0
  445. package/tests/components/admin/AuditTab.test.tsx +220 -0
  446. package/tests/components/admin/MonitoringEmbed.test.tsx +58 -0
  447. package/tests/components/admin/OperationsTab.test.tsx +975 -0
  448. package/tests/components/admin/OverviewTab.test.tsx +254 -0
  449. package/tests/components/admin/SecurityTab.test.tsx +467 -0
  450. package/tests/components/monitoring/MetricChart.test.tsx +111 -0
  451. package/tests/components/monitoring/MonitoringDashboard.test.tsx +259 -0
  452. package/tests/components/monitoring/OverviewTab.test.tsx +78 -0
  453. package/tests/components/monitoring/PerformanceTab.test.tsx +87 -0
  454. package/tests/components/monitoring/PoolTab.test.tsx +42 -0
  455. package/tests/components/monitoring/QueriesTab.test.tsx +80 -0
  456. package/tests/components/monitoring/SessionsTab.test.tsx +154 -0
  457. package/tests/components/monitoring/StorageTab.test.tsx +127 -0
  458. package/tests/components/monitoring/TablesTab.test.tsx +153 -0
  459. package/tests/components/results-grid/ResultCard.test.tsx +105 -0
  460. package/tests/components/results-grid/RowDetailSheet.test.tsx +308 -0
  461. package/tests/components/results-grid/StatsBar.test.tsx +162 -0
  462. package/tests/components/schema-explorer/ColumnList.test.tsx +151 -0
  463. package/tests/components/schema-explorer/SchemaExplorer.test.tsx +461 -0
  464. package/tests/components/schema-explorer/TableItem.test.tsx +415 -0
  465. package/tests/components/sidebar/ConnectionItem.test.tsx +201 -0
  466. package/tests/components/sidebar/ConnectionsList.test.tsx +176 -0
  467. package/tests/components/sidebar/Sidebar.test.tsx +187 -0
  468. package/tests/components/studio/BottomPanel.test.tsx +383 -0
  469. package/tests/components/studio/QueryToolbar.test.tsx +321 -0
  470. package/tests/components/studio/StudioDesktopHeader.test.tsx +377 -0
  471. package/tests/components/studio/StudioMobileHeader.test.tsx +198 -0
  472. package/tests/components/studio/StudioTabBar.test.tsx +331 -0
  473. package/tests/fixtures/connections.ts +96 -0
  474. package/tests/fixtures/masking-configs.ts +86 -0
  475. package/tests/fixtures/query-results.ts +71 -0
  476. package/tests/fixtures/schemas.ts +64 -0
  477. package/tests/fixtures/seed-connections/invalid-config.yaml +7 -0
  478. package/tests/fixtures/seed-connections/minimal-config.yaml +8 -0
  479. package/tests/fixtures/seed-connections/mixed-credentials.yaml +23 -0
  480. package/tests/fixtures/seed-connections/multi-role-config.yaml +30 -0
  481. package/tests/fixtures/seed-connections/valid-config.json +15 -0
  482. package/tests/fixtures/seed-connections/valid-config.yaml +51 -0
  483. package/tests/helpers/mock-fetch.ts +59 -0
  484. package/tests/helpers/mock-monaco.ts +112 -0
  485. package/tests/helpers/mock-navigation.ts +28 -0
  486. package/tests/helpers/mock-next.ts +80 -0
  487. package/tests/helpers/mock-provider.ts +133 -0
  488. package/tests/helpers/mock-sonner.ts +29 -0
  489. package/tests/helpers/render-with-providers.tsx +19 -0
  490. package/tests/hooks/use-ai-chat.test.ts +600 -0
  491. package/tests/hooks/use-auth.test.ts +371 -0
  492. package/tests/hooks/use-connection-form.test.ts +743 -0
  493. package/tests/hooks/use-connection-manager.test.ts +466 -0
  494. package/tests/hooks/use-inline-editing.test.ts +321 -0
  495. package/tests/hooks/use-mobile.test.ts +177 -0
  496. package/tests/hooks/use-monitoring-data.test.ts +819 -0
  497. package/tests/hooks/use-provider-metadata.test.ts +228 -0
  498. package/tests/hooks/use-query-execution.test.ts +1212 -0
  499. package/tests/hooks/use-tab-manager.test.ts +756 -0
  500. package/tests/hooks/use-toast.test.ts +74 -0
  501. package/tests/hooks/use-transaction-control.test.ts +211 -0
  502. package/tests/integration/db/mongodb-provider.test.ts +698 -0
  503. package/tests/integration/db/mssql-provider.test.ts +840 -0
  504. package/tests/integration/db/mysql-provider.test.ts +872 -0
  505. package/tests/integration/db/oracle-provider.test.ts +843 -0
  506. package/tests/integration/db/postgres-provider.test.ts +1382 -0
  507. package/tests/integration/db/redis-provider.test.ts +526 -0
  508. package/tests/integration/db/sqlite-provider.test.ts +480 -0
  509. package/tests/integration/seed/seed-pipeline.test.ts +102 -0
  510. package/tests/isolated/factory-singleton.test.ts +150 -0
  511. package/tests/isolated/use-storage-sync.test.ts +389 -0
  512. package/tests/run-components.sh +196 -0
  513. package/tests/setup-dom.ts +58 -0
  514. package/tests/setup.ts +40 -0
  515. package/tests/unit/api-errors.test.ts +210 -0
  516. package/tests/unit/code-generator-functions.test.ts +271 -0
  517. package/tests/unit/components/column-list.test.tsx +190 -0
  518. package/tests/unit/components/data-import-modal.test.tsx +441 -0
  519. package/tests/unit/components/studio-mobile-header.test.tsx +327 -0
  520. package/tests/unit/data-charts-functions.test.ts +496 -0
  521. package/tests/unit/data-import-functions.test.ts +320 -0
  522. package/tests/unit/data-import-utils.test.ts +125 -0
  523. package/tests/unit/db/base-provider.test.ts +517 -0
  524. package/tests/unit/db/errors.test.ts +403 -0
  525. package/tests/unit/db/factory.test.ts +436 -0
  526. package/tests/unit/db/pool-manager.test.ts +440 -0
  527. package/tests/unit/db/query-limiter.test.ts +387 -0
  528. package/tests/unit/db/sql-base.test.ts +438 -0
  529. package/tests/unit/lib/api/error-codes.test.ts +39 -0
  530. package/tests/unit/lib/audit.test.ts +326 -0
  531. package/tests/unit/lib/auth.test.ts +146 -0
  532. package/tests/unit/lib/connection-string-parser.test.ts +424 -0
  533. package/tests/unit/lib/data-masking.test.ts +583 -0
  534. package/tests/unit/lib/db-icons.test.tsx +41 -0
  535. package/tests/unit/lib/monitoring-thresholds.test.ts +133 -0
  536. package/tests/unit/lib/oidc.test.ts +509 -0
  537. package/tests/unit/lib/query-generators.test.ts +127 -0
  538. package/tests/unit/lib/storage/factory.test.ts +71 -0
  539. package/tests/unit/lib/storage/local-storage.test.ts +114 -0
  540. package/tests/unit/lib/storage/providers/postgres.test.ts +312 -0
  541. package/tests/unit/lib/storage/providers/sqlite.test.ts +232 -0
  542. package/tests/unit/lib/storage/storage-facade-extended.test.ts +331 -0
  543. package/tests/unit/lib/storage/storage-facade.test.ts +184 -0
  544. package/tests/unit/lib/storage.test.ts +317 -0
  545. package/tests/unit/lib/time-series-buffer.test.ts +212 -0
  546. package/tests/unit/lib/utils.test.ts +24 -0
  547. package/tests/unit/llm/base-provider.test.ts +238 -0
  548. package/tests/unit/llm/config.test.ts +262 -0
  549. package/tests/unit/llm/custom-provider.test.ts +281 -0
  550. package/tests/unit/llm/gemini-provider.test.ts +248 -0
  551. package/tests/unit/llm/llm-factory.test.ts +155 -0
  552. package/tests/unit/llm/ollama-provider.test.ts +288 -0
  553. package/tests/unit/llm/openai-provider.test.ts +324 -0
  554. package/tests/unit/llm/retry.test.ts +180 -0
  555. package/tests/unit/llm/streaming.test.ts +355 -0
  556. package/tests/unit/logger.test.ts +198 -0
  557. package/tests/unit/mongodb-completions.test.ts +516 -0
  558. package/tests/unit/pivot-table-functions.test.ts +76 -0
  559. package/tests/unit/query-cancelled-error.test.ts +81 -0
  560. package/tests/unit/schema-diff/diff-engine.test.ts +367 -0
  561. package/tests/unit/schema-diff/migration-generator.test.ts +513 -0
  562. package/tests/unit/seed/config-loader.test.ts +73 -0
  563. package/tests/unit/seed/connection-filter.test.ts +91 -0
  564. package/tests/unit/seed/credential-resolver.test.ts +85 -0
  565. package/tests/unit/seed/index.test.ts +72 -0
  566. package/tests/unit/seed/resolve-connection.test.ts +74 -0
  567. package/tests/unit/seed/types.test.ts +129 -0
  568. package/tests/unit/sql/alias-extractor.test.ts +444 -0
  569. package/tests/unit/sql/statement-splitter.test.ts +348 -0
  570. package/tests/unit/sql-completions.test.ts +463 -0
  571. package/tests/unit/ssh-tunnel.test.ts +465 -0
  572. package/tsconfig.json +42 -0
@@ -0,0 +1,978 @@
1
+ /**
2
+ * MySQL Database Provider
3
+ * Full MySQL support with connection pooling using mysql2
4
+ */
5
+
6
+ import mysql, { type Pool, type PoolConnection, type RowDataPacket, type FieldPacket } from 'mysql2/promise';
7
+ import { SQLBaseProvider } from './sql-base';
8
+ import {
9
+ type DatabaseConnection,
10
+ type TableSchema,
11
+ type QueryResult,
12
+ type HealthInfo,
13
+ type MaintenanceType,
14
+ type MaintenanceResult,
15
+ type ProviderOptions,
16
+ type ProviderCapabilities,
17
+ type SlowQuery,
18
+ type ActiveSession,
19
+ type DatabaseOverview,
20
+ type PerformanceMetrics,
21
+ type SlowQueryStats,
22
+ type ActiveSessionDetails,
23
+ type TableStats,
24
+ type IndexStats,
25
+ type StorageStats,
26
+ } from '../../types';
27
+ import {
28
+ DatabaseConfigError,
29
+ ConnectionError,
30
+ QueryError,
31
+ mapDatabaseError,
32
+ } from '../../errors';
33
+ import { formatBytes } from '../../utils/pool-manager';
34
+
35
+ // ============================================================================
36
+ // MySQL Provider
37
+ // ============================================================================
38
+
39
+ export class MySQLProvider extends SQLBaseProvider {
40
+ private pool: Pool | null = null;
41
+
42
+ // Transaction support: dedicated connection held outside pool
43
+ private txConn: PoolConnection | null = null;
44
+ private txActive = false;
45
+ private txTimeout: ReturnType<typeof setTimeout> | null = null;
46
+ private static readonly TX_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
47
+
48
+ constructor(config: DatabaseConnection, options: ProviderOptions = {}) {
49
+ super(config, options);
50
+ this.validate();
51
+ }
52
+
53
+ // ============================================================================
54
+ // Provider Metadata
55
+ // ============================================================================
56
+
57
+ public override getCapabilities(): ProviderCapabilities {
58
+ return {
59
+ ...super.getCapabilities(),
60
+ defaultPort: 3306,
61
+ supportsExplain: true,
62
+ supportsConnectionString: true,
63
+ maintenanceOperations: ['analyze', 'optimize', 'check', 'kill'],
64
+ };
65
+ }
66
+
67
+ // ============================================================================
68
+ // Validation
69
+ // ============================================================================
70
+
71
+ public validate(): void {
72
+ super.validate();
73
+
74
+ if (!this.config.connectionString) {
75
+ if (!this.config.host) {
76
+ throw new DatabaseConfigError('Host is required for MySQL', 'mysql');
77
+ }
78
+ if (!this.config.database) {
79
+ throw new DatabaseConfigError('Database name is required for MySQL', 'mysql');
80
+ }
81
+ }
82
+ }
83
+
84
+ // ============================================================================
85
+ // Connection Management
86
+ // ============================================================================
87
+
88
+ public async connect(): Promise<void> {
89
+ if (this.pool) {
90
+ return;
91
+ }
92
+
93
+ try {
94
+ this.pool = mysql.createPool(this.buildPoolConfig());
95
+
96
+ const conn = await this.pool.getConnection();
97
+ conn.release();
98
+
99
+ this.setConnected(true);
100
+ } catch (error) {
101
+ this.setError(error instanceof Error ? error : new Error(String(error)));
102
+ throw new ConnectionError(
103
+ `Failed to connect to MySQL: ${error instanceof Error ? error.message : error}`,
104
+ 'mysql',
105
+ this.config.host,
106
+ this.config.port
107
+ );
108
+ }
109
+ }
110
+
111
+ public async disconnect(): Promise<void> {
112
+ if (this.pool) {
113
+ await this.pool.end();
114
+ this.pool = null;
115
+ this.setConnected(false);
116
+ }
117
+ }
118
+
119
+ private buildPoolConfig(): mysql.PoolOptions {
120
+ const baseConfig: mysql.PoolOptions = {
121
+ connectionLimit: this.poolConfig.max,
122
+ waitForConnections: true,
123
+ queueLimit: 0,
124
+ enableKeepAlive: true,
125
+ keepAliveInitialDelay: 10000,
126
+ };
127
+
128
+ if (this.config.connectionString) {
129
+ return {
130
+ ...baseConfig,
131
+ uri: this.config.connectionString,
132
+ };
133
+ }
134
+
135
+ return {
136
+ ...baseConfig,
137
+ host: this.config.host,
138
+ port: this.config.port ?? 3306,
139
+ user: this.config.user,
140
+ password: this.config.password,
141
+ database: this.config.database,
142
+ ssl: this.buildSSLConfig(),
143
+ timezone: this.options.timezone ?? 'Z',
144
+ };
145
+ }
146
+
147
+ private buildSSLConfig(): mysql.SslOptions | undefined {
148
+ const connSSL = this.config.ssl;
149
+
150
+ if (connSSL) {
151
+ if (connSSL.mode === 'disable') return undefined;
152
+
153
+ const ssl: mysql.SslOptions = {
154
+ rejectUnauthorized: connSSL.mode === 'verify-ca' || connSSL.mode === 'verify-full',
155
+ };
156
+
157
+ if (connSSL.caCert) ssl.ca = connSSL.caCert;
158
+ if (connSSL.clientCert) ssl.cert = connSSL.clientCert;
159
+ if (connSSL.clientKey) ssl.key = connSSL.clientKey;
160
+
161
+ return ssl;
162
+ }
163
+
164
+ if (this.shouldEnableSSL()) {
165
+ return { rejectUnauthorized: false };
166
+ }
167
+
168
+ return undefined;
169
+ }
170
+
171
+ // ============================================================================
172
+ // Query Execution
173
+ // ============================================================================
174
+
175
+ private sanitizeRow(row: Record<string, unknown>): Record<string, unknown> {
176
+ const sanitized: Record<string, unknown> = {};
177
+ for (const [key, value] of Object.entries(row)) {
178
+ if (Buffer.isBuffer(value)) {
179
+ sanitized[key] = value.length === 0 ? '' : `0x${value.toString('hex')}`;
180
+ } else {
181
+ sanitized[key] = value;
182
+ }
183
+ }
184
+ return sanitized;
185
+ }
186
+
187
+ // Track running query thread IDs for cancellation
188
+ private runningQueryThreadIds = new Map<string, number>();
189
+
190
+ public async query(sql: string, params?: unknown[], queryId?: string): Promise<QueryResult> {
191
+ this.ensureConnected();
192
+
193
+ return this.trackQuery(async () => {
194
+ const { result, executionTime } = await this.measureExecution(async () => {
195
+ const conn = await this.pool!.getConnection();
196
+ try {
197
+ // Track thread ID for cancellation support
198
+ if (queryId) {
199
+ this.runningQueryThreadIds.set(queryId, conn.threadId);
200
+ }
201
+ const [rows, fields] = await conn.execute<RowDataPacket[]>(sql, params);
202
+ return { rows, fields };
203
+ } catch (error) {
204
+ throw mapDatabaseError(error, 'mysql', sql);
205
+ } finally {
206
+ if (queryId) this.runningQueryThreadIds.delete(queryId);
207
+ conn.release();
208
+ }
209
+ });
210
+
211
+ return {
212
+ rows: (result.rows as unknown[]).map(row => this.sanitizeRow(row as Record<string, unknown>)),
213
+ fields: result.fields?.map((f: FieldPacket) => f.name) ?? [],
214
+ rowCount: Array.isArray(result.rows) ? result.rows.length : 0,
215
+ executionTime,
216
+ };
217
+ });
218
+ }
219
+
220
+ public async cancelQuery(queryId: string): Promise<boolean> {
221
+ const threadId = this.runningQueryThreadIds.get(queryId);
222
+ if (!threadId) return false;
223
+
224
+ try {
225
+ await this.pool!.execute(`KILL QUERY ${threadId}`);
226
+ return true;
227
+ } catch (error) {
228
+ console.error('[MySQL] Failed to cancel query:', error);
229
+ return false;
230
+ }
231
+ }
232
+
233
+ // ============================================================================
234
+ // Transaction Support
235
+ // ============================================================================
236
+
237
+ private clearTxTimeout(): void {
238
+ if (this.txTimeout) {
239
+ clearTimeout(this.txTimeout);
240
+ this.txTimeout = null;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Force-expire an active transaction (auto-rollback).
246
+ * Called by the timeout timer, but also available for testing.
247
+ */
248
+ public async expireTransaction(): Promise<void> {
249
+ if (this.txActive && this.txConn) {
250
+ console.warn('[MySQL] Transaction timed out, auto-rolling back');
251
+ try {
252
+ await this.txConn.rollback();
253
+ } catch { /* ignore */ } finally {
254
+ this.txConn.release();
255
+ this.txConn = null;
256
+ this.txActive = false;
257
+ this.clearTxTimeout();
258
+ }
259
+ }
260
+ }
261
+
262
+ public async beginTransaction(): Promise<void> {
263
+ this.ensureConnected();
264
+ if (this.txActive) throw new QueryError('Transaction already active', 'mysql');
265
+ this.txConn = await this.pool!.getConnection();
266
+ await this.txConn.beginTransaction();
267
+ this.txActive = true;
268
+
269
+ // Auto-rollback after timeout to prevent leaked locks
270
+ this.txTimeout = setTimeout(() => { this.expireTransaction(); }, MySQLProvider.TX_TIMEOUT_MS);
271
+ }
272
+
273
+ public async commitTransaction(): Promise<void> {
274
+ if (!this.txConn || !this.txActive) throw new QueryError('No active transaction', 'mysql');
275
+ this.clearTxTimeout();
276
+ try {
277
+ await this.txConn.commit();
278
+ } finally {
279
+ this.txConn.release();
280
+ this.txConn = null;
281
+ this.txActive = false;
282
+ }
283
+ }
284
+
285
+ public async rollbackTransaction(): Promise<void> {
286
+ if (!this.txConn || !this.txActive) throw new QueryError('No active transaction', 'mysql');
287
+ this.clearTxTimeout();
288
+ try {
289
+ await this.txConn.rollback();
290
+ } finally {
291
+ this.txConn.release();
292
+ this.txConn = null;
293
+ this.txActive = false;
294
+ }
295
+ }
296
+
297
+ public isInTransaction(): boolean {
298
+ return this.txActive;
299
+ }
300
+
301
+ public async queryInTransaction(sql: string, params?: unknown[]): Promise<QueryResult> {
302
+ if (!this.txConn || !this.txActive) throw new QueryError('No active transaction', 'mysql');
303
+
304
+ return this.trackQuery(async () => {
305
+ const { result, executionTime } = await this.measureExecution(async () => {
306
+ try {
307
+ const [rows, fields] = await this.txConn!.execute<RowDataPacket[]>(sql, params);
308
+ return { rows, fields };
309
+ } catch (error) {
310
+ throw mapDatabaseError(error, 'mysql', sql);
311
+ }
312
+ });
313
+
314
+ return {
315
+ rows: (result.rows as unknown[]).map(row => this.sanitizeRow(row as Record<string, unknown>)),
316
+ fields: result.fields?.map((f: FieldPacket) => f.name) ?? [],
317
+ rowCount: Array.isArray(result.rows) ? result.rows.length : 0,
318
+ executionTime,
319
+ };
320
+ });
321
+ }
322
+
323
+ // ============================================================================
324
+ // Schema Operations
325
+ // ============================================================================
326
+
327
+ public async getSchema(): Promise<TableSchema[]> {
328
+ this.ensureConnected();
329
+
330
+ const conn = await this.pool!.getConnection();
331
+ try {
332
+ const [tablesRows] = await conn.execute<RowDataPacket[]>(`
333
+ SELECT
334
+ TABLE_NAME as table_name,
335
+ TABLE_ROWS as row_count,
336
+ DATA_LENGTH + INDEX_LENGTH as total_size
337
+ FROM information_schema.TABLES
338
+ WHERE TABLE_SCHEMA = ?
339
+ AND TABLE_TYPE = 'BASE TABLE'
340
+ ORDER BY TABLE_NAME ASC;
341
+ `, [this.config.database]);
342
+
343
+ const schemas: TableSchema[] = [];
344
+
345
+ for (const row of tablesRows) {
346
+ const tableName = row.table_name;
347
+ const rowCount = parseInt(row.row_count || '0');
348
+ const sizeBytes = parseInt(row.total_size || '0');
349
+
350
+ const [columnsRows] = await conn.execute<RowDataPacket[]>(`
351
+ SELECT
352
+ COLUMN_NAME as column_name,
353
+ DATA_TYPE as data_type,
354
+ IS_NULLABLE as is_nullable,
355
+ COLUMN_DEFAULT as column_default,
356
+ COLUMN_KEY as column_key
357
+ FROM information_schema.COLUMNS
358
+ WHERE TABLE_SCHEMA = ?
359
+ AND TABLE_NAME = ?
360
+ ORDER BY ORDINAL_POSITION
361
+ LIMIT 100;
362
+ `, [this.config.database, tableName]);
363
+
364
+ const [fkRows] = await conn.execute<RowDataPacket[]>(`
365
+ SELECT
366
+ COLUMN_NAME as column_name,
367
+ REFERENCED_TABLE_NAME as referenced_table,
368
+ REFERENCED_COLUMN_NAME as referenced_column
369
+ FROM information_schema.KEY_COLUMN_USAGE
370
+ WHERE TABLE_SCHEMA = ?
371
+ AND TABLE_NAME = ?
372
+ AND REFERENCED_TABLE_NAME IS NOT NULL;
373
+ `, [this.config.database, tableName]);
374
+
375
+ const [indexRows] = await conn.execute<RowDataPacket[]>(`
376
+ SELECT
377
+ INDEX_NAME as index_name,
378
+ GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) as columns,
379
+ NOT NON_UNIQUE as is_unique
380
+ FROM information_schema.STATISTICS
381
+ WHERE TABLE_SCHEMA = ?
382
+ AND TABLE_NAME = ?
383
+ GROUP BY INDEX_NAME, NON_UNIQUE;
384
+ `, [this.config.database, tableName]);
385
+
386
+ schemas.push({
387
+ name: tableName,
388
+ rowCount,
389
+ size: formatBytes(sizeBytes),
390
+ columns: columnsRows.map((col) => ({
391
+ name: col.column_name,
392
+ type: col.data_type,
393
+ nullable: col.is_nullable === 'YES',
394
+ isPrimary: col.column_key === 'PRI',
395
+ defaultValue: col.column_default ?? undefined,
396
+ })),
397
+ indexes: indexRows.map((idx) => ({
398
+ name: idx.index_name,
399
+ columns: idx.columns?.split(',') ?? [],
400
+ unique: Boolean(idx.is_unique),
401
+ })),
402
+ foreignKeys: fkRows.map((fk) => ({
403
+ columnName: fk.column_name,
404
+ referencedTable: fk.referenced_table,
405
+ referencedColumn: fk.referenced_column,
406
+ })),
407
+ });
408
+ }
409
+
410
+ return schemas;
411
+ } finally {
412
+ conn.release();
413
+ }
414
+ }
415
+
416
+ // ============================================================================
417
+ // Health & Monitoring
418
+ // ============================================================================
419
+
420
+ public async getHealth(): Promise<HealthInfo> {
421
+ this.ensureConnected();
422
+
423
+ const conn = await this.pool!.getConnection();
424
+ try {
425
+ const [connRows] = await conn.execute<RowDataPacket[]>(
426
+ "SHOW STATUS LIKE 'Threads_connected'"
427
+ );
428
+ const activeConnections = parseInt(connRows[0]?.Value || '0');
429
+
430
+ const [sizeRows] = await conn.execute<RowDataPacket[]>(`
431
+ SELECT
432
+ ROUND(SUM(DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) as size_mb
433
+ FROM information_schema.TABLES
434
+ WHERE TABLE_SCHEMA = ?;
435
+ `, [this.config.database]);
436
+ const databaseSize = `${sizeRows[0]?.size_mb || 0} MB`;
437
+
438
+ const [hitRows] = await conn.execute<RowDataPacket[]>(`
439
+ SELECT
440
+ (1 - (
441
+ (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads') /
442
+ NULLIF((SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_read_requests'), 0)
443
+ )) * 100 as hit_ratio;
444
+ `);
445
+ const cacheHitRatio = `${(hitRows[0]?.hit_ratio || 99).toFixed(1)}%`;
446
+
447
+ let slowQueries: SlowQuery[] = [];
448
+ try {
449
+ const [slowRows] = await conn.execute<RowDataPacket[]>(`
450
+ SELECT
451
+ LEFT(sql_text, 100) as query,
452
+ count_star as calls,
453
+ CONCAT(ROUND(avg_timer_wait / 1000000000, 2), 'ms') as avgTime
454
+ FROM performance_schema.events_statements_summary_by_digest
455
+ WHERE schema_name = ?
456
+ ORDER BY sum_timer_wait DESC
457
+ LIMIT 5;
458
+ `, [this.config.database]);
459
+ slowQueries = slowRows.map((r) => ({
460
+ query: r.query || '',
461
+ calls: parseInt(r.calls || '0'),
462
+ avgTime: r.avgTime || 'N/A',
463
+ }));
464
+ } catch {
465
+ slowQueries = [{ query: 'Performance schema not available', calls: 0, avgTime: 'N/A' }];
466
+ }
467
+
468
+ const [sessionRows] = await conn.execute<RowDataPacket[]>(`
469
+ SELECT
470
+ ID as pid,
471
+ USER as user,
472
+ DB as \`database\`,
473
+ COMMAND as state,
474
+ LEFT(COALESCE(INFO, ''), 100) as query,
475
+ CONCAT(TIME, 's') as duration
476
+ FROM information_schema.PROCESSLIST
477
+ WHERE DB = ?
478
+ ORDER BY TIME DESC
479
+ LIMIT 10;
480
+ `, [this.config.database]);
481
+
482
+ const activeSessions: ActiveSession[] = sessionRows.map((r) => ({
483
+ pid: r.pid,
484
+ user: r.user || 'unknown',
485
+ database: r.database || '',
486
+ state: r.state || 'unknown',
487
+ query: r.query || '',
488
+ duration: r.duration || 'N/A',
489
+ }));
490
+
491
+ return {
492
+ activeConnections,
493
+ databaseSize,
494
+ cacheHitRatio,
495
+ slowQueries,
496
+ activeSessions,
497
+ };
498
+ } finally {
499
+ conn.release();
500
+ }
501
+ }
502
+
503
+ // ============================================================================
504
+ // Maintenance Operations
505
+ // ============================================================================
506
+
507
+ public async runMaintenance(
508
+ type: MaintenanceType,
509
+ target?: string
510
+ ): Promise<MaintenanceResult> {
511
+ this.ensureConnected();
512
+
513
+ const { result, executionTime } = await this.measureExecution(async () => {
514
+ const conn = await this.pool!.getConnection();
515
+ try {
516
+ let sql = '';
517
+
518
+ switch (type) {
519
+ case 'analyze':
520
+ sql = target
521
+ ? `ANALYZE TABLE ${this.escapeIdentifier(target)}`
522
+ : `ANALYZE TABLE ${await this.getAllTablesForMaintenance(conn)}`;
523
+ break;
524
+ case 'optimize':
525
+ sql = target
526
+ ? `OPTIMIZE TABLE ${this.escapeIdentifier(target)}`
527
+ : `OPTIMIZE TABLE ${await this.getAllTablesForMaintenance(conn)}`;
528
+ break;
529
+ case 'check':
530
+ sql = target
531
+ ? `CHECK TABLE ${this.escapeIdentifier(target)}`
532
+ : `CHECK TABLE ${await this.getAllTablesForMaintenance(conn)}`;
533
+ break;
534
+ case 'kill':
535
+ if (!target) {
536
+ throw new QueryError('Target connection ID is required for kill operation', 'mysql');
537
+ }
538
+ const connId = parseInt(target, 10);
539
+ if (isNaN(connId)) {
540
+ throw new QueryError('Invalid connection ID for kill operation', 'mysql');
541
+ }
542
+ sql = `KILL ${connId}`;
543
+ break;
544
+ default:
545
+ throw new QueryError(`Unsupported maintenance type for MySQL: ${type}`, 'mysql');
546
+ }
547
+
548
+ await conn.execute(sql);
549
+ return { success: true };
550
+ } finally {
551
+ conn.release();
552
+ }
553
+ });
554
+
555
+ return {
556
+ success: result.success,
557
+ executionTime,
558
+ message: `${type.toUpperCase()} completed successfully`,
559
+ };
560
+ }
561
+
562
+ private async getAllTablesForMaintenance(conn: PoolConnection): Promise<string> {
563
+ const [rows] = await conn.execute<RowDataPacket[]>(`
564
+ SELECT TABLE_NAME
565
+ FROM information_schema.TABLES
566
+ WHERE TABLE_SCHEMA = ?
567
+ AND TABLE_TYPE = 'BASE TABLE'
568
+ LIMIT 50;
569
+ `, [this.config.database]);
570
+
571
+ return rows.map((r) => this.escapeIdentifier(r.TABLE_NAME)).join(', ');
572
+ }
573
+
574
+ // ============================================================================
575
+ // Monitoring Operations
576
+ // ============================================================================
577
+
578
+ public async getOverview(): Promise<DatabaseOverview> {
579
+ this.ensureConnected();
580
+
581
+ const conn = await this.pool!.getConnection();
582
+ try {
583
+ // Get version
584
+ const [versionRows] = await conn.execute<RowDataPacket[]>('SELECT VERSION() as version');
585
+ const version = versionRows[0]?.version || 'Unknown';
586
+
587
+ // Get uptime
588
+ const [uptimeRows] = await conn.execute<RowDataPacket[]>(
589
+ "SHOW STATUS LIKE 'Uptime'"
590
+ );
591
+ const uptimeSeconds = parseInt(uptimeRows[0]?.Value || '0');
592
+ const uptime = this.formatUptimeString(uptimeSeconds);
593
+
594
+ // Get active connections
595
+ const [connRows] = await conn.execute<RowDataPacket[]>(
596
+ "SHOW STATUS LIKE 'Threads_connected'"
597
+ );
598
+ const activeConnections = parseInt(connRows[0]?.Value || '0');
599
+
600
+ // Get max connections
601
+ const [maxConnRows] = await conn.execute<RowDataPacket[]>(
602
+ "SHOW VARIABLES LIKE 'max_connections'"
603
+ );
604
+ const maxConnections = parseInt(maxConnRows[0]?.Value || '151');
605
+
606
+ // Get database size
607
+ const [sizeRows] = await conn.execute<RowDataPacket[]>(`
608
+ SELECT SUM(DATA_LENGTH + INDEX_LENGTH) as size_bytes
609
+ FROM information_schema.TABLES
610
+ WHERE TABLE_SCHEMA = ?;
611
+ `, [this.config.database]);
612
+ const databaseSizeBytes = parseInt(sizeRows[0]?.size_bytes || '0');
613
+
614
+ // Get table and index count
615
+ const [countRows] = await conn.execute<RowDataPacket[]>(`
616
+ SELECT
617
+ COUNT(DISTINCT TABLE_NAME) as table_count,
618
+ COUNT(DISTINCT INDEX_NAME) as index_count
619
+ FROM information_schema.STATISTICS
620
+ WHERE TABLE_SCHEMA = ?;
621
+ `, [this.config.database]);
622
+
623
+ const [tableCountRows] = await conn.execute<RowDataPacket[]>(`
624
+ SELECT COUNT(*) as cnt FROM information_schema.TABLES
625
+ WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'BASE TABLE';
626
+ `, [this.config.database]);
627
+
628
+ return {
629
+ version: `MySQL ${version}`,
630
+ uptime,
631
+ startTime: new Date(Date.now() - uptimeSeconds * 1000),
632
+ activeConnections,
633
+ maxConnections,
634
+ databaseSize: formatBytes(databaseSizeBytes),
635
+ databaseSizeBytes,
636
+ tableCount: parseInt(tableCountRows[0]?.cnt || '0'),
637
+ indexCount: parseInt(countRows[0]?.index_count || '0'),
638
+ };
639
+ } finally {
640
+ conn.release();
641
+ }
642
+ }
643
+
644
+ public async getPerformanceMetrics(): Promise<PerformanceMetrics> {
645
+ this.ensureConnected();
646
+
647
+ const conn = await this.pool!.getConnection();
648
+ try {
649
+ // Calculate cache hit ratio from InnoDB buffer pool
650
+ const [hitRows] = await conn.execute<RowDataPacket[]>(`
651
+ SELECT
652
+ (1 - (
653
+ (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads') /
654
+ NULLIF((SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_read_requests'), 0)
655
+ )) * 100 as hit_ratio;
656
+ `);
657
+ const cacheHitRatio = parseFloat(hitRows[0]?.hit_ratio || '99');
658
+
659
+ // Get buffer pool usage
660
+ const [poolRows] = await conn.execute<RowDataPacket[]>(`
661
+ SELECT
662
+ (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_pages_data') as data_pages,
663
+ (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_pages_total') as total_pages;
664
+ `);
665
+ const dataPages = parseInt(poolRows[0]?.data_pages || '0');
666
+ const totalPages = parseInt(poolRows[0]?.total_pages || '1');
667
+ const bufferPoolUsage = (dataPages / totalPages) * 100;
668
+
669
+ // Get queries per second
670
+ const [qpsRows] = await conn.execute<RowDataPacket[]>(`
671
+ SELECT
672
+ (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Queries') as queries,
673
+ (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Uptime') as uptime;
674
+ `);
675
+ const queries = parseInt(qpsRows[0]?.queries || '0');
676
+ const uptime = parseInt(qpsRows[0]?.uptime || '1');
677
+ const queriesPerSecond = queries / uptime;
678
+
679
+ // Get deadlocks
680
+ const [deadlockRows] = await conn.execute<RowDataPacket[]>(
681
+ "SHOW STATUS LIKE 'Innodb_deadlocks'"
682
+ );
683
+ const deadlocks = parseInt(deadlockRows[0]?.Value || '0');
684
+
685
+ return {
686
+ cacheHitRatio: Math.min(100, Math.max(0, cacheHitRatio)),
687
+ queriesPerSecond: Math.round(queriesPerSecond * 100) / 100,
688
+ bufferPoolUsage: Math.round(bufferPoolUsage * 100) / 100,
689
+ deadlocks,
690
+ };
691
+ } catch {
692
+ // Fallback if performance_schema is not available
693
+ return {
694
+ cacheHitRatio: 99,
695
+ queriesPerSecond: 0,
696
+ bufferPoolUsage: 0,
697
+ deadlocks: 0,
698
+ };
699
+ } finally {
700
+ conn.release();
701
+ }
702
+ }
703
+
704
+ public async getSlowQueries(options?: { limit?: number }): Promise<SlowQueryStats[]> {
705
+ this.ensureConnected();
706
+ const limit = options?.limit ?? 10;
707
+
708
+ const conn = await this.pool!.getConnection();
709
+ try {
710
+ const [rows] = await conn.execute<RowDataPacket[]>(`
711
+ SELECT
712
+ DIGEST as query_id,
713
+ LEFT(DIGEST_TEXT, 500) as query,
714
+ COUNT_STAR as calls,
715
+ SUM_TIMER_WAIT / 1000000000 as total_time_ms,
716
+ AVG_TIMER_WAIT / 1000000000 as avg_time_ms,
717
+ MIN_TIMER_WAIT / 1000000000 as min_time_ms,
718
+ MAX_TIMER_WAIT / 1000000000 as max_time_ms,
719
+ SUM_ROWS_EXAMINED as rows_examined
720
+ FROM performance_schema.events_statements_summary_by_digest
721
+ WHERE SCHEMA_NAME = ?
722
+ ORDER BY SUM_TIMER_WAIT DESC
723
+ LIMIT ${Number(limit)};
724
+ `, [this.config.database]);
725
+
726
+ return rows.map((r) => ({
727
+ queryId: r.query_id || undefined,
728
+ query: r.query || '',
729
+ calls: parseInt(r.calls || '0'),
730
+ totalTime: parseFloat(r.total_time_ms || '0'),
731
+ avgTime: parseFloat(r.avg_time_ms || '0'),
732
+ minTime: parseFloat(r.min_time_ms || '0'),
733
+ maxTime: parseFloat(r.max_time_ms || '0'),
734
+ rows: parseInt(r.rows_examined || '0'),
735
+ }));
736
+ } catch {
737
+ // Performance schema not available
738
+ return [];
739
+ } finally {
740
+ conn.release();
741
+ }
742
+ }
743
+
744
+ public async getActiveSessions(options?: { limit?: number }): Promise<ActiveSessionDetails[]> {
745
+ this.ensureConnected();
746
+ const limit = options?.limit ?? 50;
747
+
748
+ const conn = await this.pool!.getConnection();
749
+ try {
750
+ const [rows] = await conn.execute<RowDataPacket[]>(`
751
+ SELECT
752
+ ID as pid,
753
+ USER as user,
754
+ DB as database_name,
755
+ HOST as client_addr,
756
+ COMMAND as state,
757
+ LEFT(COALESCE(INFO, ''), 500) as query,
758
+ TIME as duration_seconds
759
+ FROM information_schema.PROCESSLIST
760
+ WHERE DB = ? OR DB IS NULL
761
+ ORDER BY TIME DESC
762
+ LIMIT ${Number(limit)};
763
+ `, [this.config.database]);
764
+
765
+ return rows.map((r) => {
766
+ const durationSeconds = parseInt(r.duration_seconds || '0');
767
+ return {
768
+ pid: r.pid,
769
+ user: r.user || 'unknown',
770
+ database: r.database_name || '',
771
+ clientAddr: r.client_addr?.split(':')[0] || undefined,
772
+ state: r.state || 'unknown',
773
+ query: r.query || '',
774
+ duration: this.formatDurationString(durationSeconds * 1000),
775
+ durationMs: durationSeconds * 1000,
776
+ };
777
+ });
778
+ } finally {
779
+ conn.release();
780
+ }
781
+ }
782
+
783
+ public async getTableStats(options?: { schema?: string }): Promise<TableStats[]> {
784
+ this.ensureConnected();
785
+ const schema = options?.schema ?? this.config.database;
786
+
787
+ const conn = await this.pool!.getConnection();
788
+ try {
789
+ const [rows] = await conn.execute<RowDataPacket[]>(`
790
+ SELECT
791
+ TABLE_SCHEMA as schema_name,
792
+ TABLE_NAME as table_name,
793
+ TABLE_ROWS as row_count,
794
+ DATA_LENGTH as table_size_bytes,
795
+ INDEX_LENGTH as index_size_bytes,
796
+ DATA_LENGTH + INDEX_LENGTH as total_size_bytes,
797
+ DATA_FREE as free_space_bytes
798
+ FROM information_schema.TABLES
799
+ WHERE TABLE_SCHEMA = ?
800
+ AND TABLE_TYPE = 'BASE TABLE'
801
+ ORDER BY DATA_LENGTH + INDEX_LENGTH DESC
802
+ LIMIT 100;
803
+ `, [schema]);
804
+
805
+ return rows.map((r) => {
806
+ const tableSizeBytes = parseInt(r.table_size_bytes || '0');
807
+ const indexSizeBytes = parseInt(r.index_size_bytes || '0');
808
+ const totalSizeBytes = parseInt(r.total_size_bytes || '0');
809
+ const freeSpaceBytes = parseInt(r.free_space_bytes || '0');
810
+
811
+ // Estimate bloat ratio from free space
812
+ const bloatRatio = totalSizeBytes > 0 ? (freeSpaceBytes / totalSizeBytes) * 100 : 0;
813
+
814
+ return {
815
+ schemaName: r.schema_name || schema || '',
816
+ tableName: r.table_name || '',
817
+ rowCount: parseInt(r.row_count || '0'),
818
+ tableSize: formatBytes(tableSizeBytes),
819
+ tableSizeBytes,
820
+ indexSize: formatBytes(indexSizeBytes),
821
+ totalSize: formatBytes(totalSizeBytes),
822
+ totalSizeBytes,
823
+ bloatRatio: Math.round(bloatRatio * 10) / 10,
824
+ };
825
+ });
826
+ } finally {
827
+ conn.release();
828
+ }
829
+ }
830
+
831
+ public async getIndexStats(options?: { schema?: string }): Promise<IndexStats[]> {
832
+ this.ensureConnected();
833
+ const schema = options?.schema ?? this.config.database;
834
+
835
+ const conn = await this.pool!.getConnection();
836
+ try {
837
+ const [rows] = await conn.execute<RowDataPacket[]>(`
838
+ SELECT
839
+ TABLE_SCHEMA as schema_name,
840
+ TABLE_NAME as table_name,
841
+ INDEX_NAME as index_name,
842
+ INDEX_TYPE as index_type,
843
+ GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) as columns,
844
+ NOT NON_UNIQUE as is_unique,
845
+ INDEX_NAME = 'PRIMARY' as is_primary,
846
+ MAX(CARDINALITY) as cardinality
847
+ FROM information_schema.STATISTICS
848
+ WHERE TABLE_SCHEMA = ?
849
+ GROUP BY TABLE_SCHEMA, TABLE_NAME, INDEX_NAME, INDEX_TYPE, NON_UNIQUE
850
+ ORDER BY TABLE_NAME, INDEX_NAME
851
+ LIMIT 200;
852
+ `, [schema]);
853
+
854
+ // Get index sizes from INNODB_SYS_INDEXES if available
855
+ const indexSizes: Record<string, number> = {};
856
+ try {
857
+ const [sizeRows] = await conn.execute<RowDataPacket[]>(`
858
+ SELECT
859
+ CONCAT(t.NAME) as full_name,
860
+ SUM(s.INDEX_SIZE * @@innodb_page_size) as size_bytes
861
+ FROM information_schema.INNODB_INDEXES i
862
+ JOIN information_schema.INNODB_TABLES t ON i.TABLE_ID = t.TABLE_ID
863
+ JOIN information_schema.INNODB_TABLESPACES s ON t.SPACE = s.SPACE
864
+ WHERE t.NAME LIKE ?
865
+ GROUP BY t.NAME, i.NAME;
866
+ `, [`${schema}/%`]);
867
+
868
+ for (const row of sizeRows) {
869
+ indexSizes[row.full_name] = parseInt(row.size_bytes || '0');
870
+ }
871
+ } catch {
872
+ // INNODB_SYS tables not available
873
+ }
874
+
875
+ return rows.map((r) => {
876
+ const indexKey = `${r.schema_name}/${r.table_name}`;
877
+ const indexSizeBytes = indexSizes[indexKey] || 0;
878
+
879
+ return {
880
+ schemaName: r.schema_name || schema || '',
881
+ tableName: r.table_name || '',
882
+ indexName: r.index_name || '',
883
+ indexType: r.index_type || 'BTREE',
884
+ columns: r.columns?.split(',') || [],
885
+ isUnique: Boolean(r.is_unique),
886
+ isPrimary: Boolean(r.is_primary),
887
+ indexSize: formatBytes(indexSizeBytes),
888
+ indexSizeBytes,
889
+ scans: parseInt(r.cardinality || '0'),
890
+ };
891
+ });
892
+ } finally {
893
+ conn.release();
894
+ }
895
+ }
896
+
897
+ public async getStorageStats(): Promise<StorageStats[]> {
898
+ this.ensureConnected();
899
+
900
+ const conn = await this.pool!.getConnection();
901
+ try {
902
+ const stats: StorageStats[] = [];
903
+
904
+ // Get database size
905
+ const [dbRows] = await conn.execute<RowDataPacket[]>(`
906
+ SELECT
907
+ TABLE_SCHEMA as name,
908
+ SUM(DATA_LENGTH + INDEX_LENGTH) as size_bytes
909
+ FROM information_schema.TABLES
910
+ WHERE TABLE_SCHEMA = ?
911
+ GROUP BY TABLE_SCHEMA;
912
+ `, [this.config.database]);
913
+
914
+ if (dbRows.length > 0) {
915
+ const sizeBytes = parseInt(dbRows[0].size_bytes || '0');
916
+ stats.push({
917
+ name: 'Data',
918
+ location: this.config.database || 'default',
919
+ size: formatBytes(sizeBytes),
920
+ sizeBytes,
921
+ });
922
+ }
923
+
924
+ // Get binary log size if available
925
+ try {
926
+ const [binlogRows] = await conn.execute<RowDataPacket[]>('SHOW BINARY LOGS');
927
+ const binlogSize = binlogRows.reduce((sum, r) => sum + parseInt(r.File_size || '0'), 0);
928
+ if (binlogSize > 0) {
929
+ stats.push({
930
+ name: 'Binary Logs',
931
+ size: formatBytes(binlogSize),
932
+ sizeBytes: binlogSize,
933
+ });
934
+ }
935
+ } catch {
936
+ // Binary logging not enabled
937
+ }
938
+
939
+ // Get InnoDB data file size
940
+ try {
941
+ const [innodbRows] = await conn.execute<RowDataPacket[]>(
942
+ "SHOW VARIABLES LIKE 'innodb_data_file_path'"
943
+ );
944
+ if (innodbRows.length > 0) {
945
+ stats.push({
946
+ name: 'InnoDB',
947
+ location: innodbRows[0].Value || 'ibdata1',
948
+ size: 'N/A',
949
+ sizeBytes: 0,
950
+ });
951
+ }
952
+ } catch {
953
+ // Could not get InnoDB info
954
+ }
955
+
956
+ return stats;
957
+ } finally {
958
+ conn.release();
959
+ }
960
+ }
961
+
962
+ private formatUptimeString(seconds: number): string {
963
+ const days = Math.floor(seconds / 86400);
964
+ const hours = Math.floor((seconds % 86400) / 3600);
965
+ const minutes = Math.floor((seconds % 3600) / 60);
966
+
967
+ if (days > 0) return `${days}d ${hours}h`;
968
+ if (hours > 0) return `${hours}h ${minutes}m`;
969
+ return `${minutes}m`;
970
+ }
971
+
972
+ private formatDurationString(ms: number): string {
973
+ if (ms < 1000) return `${ms}ms`;
974
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
975
+ if (ms < 3600000) return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
976
+ return `${Math.floor(ms / 3600000)}h ${Math.floor((ms % 3600000) / 60000)}m`;
977
+ }
978
+ }