@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,1065 @@
1
+ /**
2
+ * Microsoft SQL Server Database Provider
3
+ * Full MSSQL support with connection pooling (SQL Authentication)
4
+ */
5
+
6
+ import mssql from 'mssql';
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 ProviderLabels,
18
+ type SlowQuery,
19
+ type ActiveSession,
20
+ type DatabaseOverview,
21
+ type PerformanceMetrics,
22
+ type SlowQueryStats,
23
+ type ActiveSessionDetails,
24
+ type TableStats,
25
+ type IndexStats,
26
+ type StorageStats,
27
+ type PreparedQuery,
28
+ type QueryPrepareOptions,
29
+ } from '../../types';
30
+ import {
31
+ DatabaseConfigError,
32
+ ConnectionError,
33
+ QueryError,
34
+ mapDatabaseError,
35
+ } from '../../errors';
36
+ import { formatBytes } from '../../utils/pool-manager';
37
+ import {
38
+ analyzeQuery,
39
+ DEFAULT_QUERY_LIMIT,
40
+ MAX_UNLIMITED_ROWS,
41
+ } from '../../utils/query-limiter';
42
+
43
+ // ============================================================================
44
+ // MSSQL Provider
45
+ // ============================================================================
46
+
47
+ export class MSSQLProvider extends SQLBaseProvider {
48
+ private pool: mssql.ConnectionPool | null = null;
49
+
50
+ // Transaction support
51
+ private txTransaction: mssql.Transaction | null = null;
52
+ private txActive = false;
53
+
54
+ // Track running requests for cancellation
55
+ private runningRequests = new Map<string, mssql.Request>();
56
+
57
+ constructor(config: DatabaseConnection, options: ProviderOptions = {}) {
58
+ super(config, options);
59
+ this.validate();
60
+ }
61
+
62
+ // ============================================================================
63
+ // Provider Metadata
64
+ // ============================================================================
65
+
66
+ public override getCapabilities(): ProviderCapabilities {
67
+ return {
68
+ ...super.getCapabilities(),
69
+ defaultPort: 1433,
70
+ supportsExplain: true,
71
+ supportsConnectionString: true,
72
+ maintenanceOperations: ['analyze', 'check', 'optimize', 'kill'],
73
+ };
74
+ }
75
+
76
+ public override getLabels(): ProviderLabels {
77
+ return {
78
+ ...super.getLabels(),
79
+ analyzeAction: 'Update Statistics',
80
+ vacuumAction: 'Rebuild Indexes',
81
+ analyzeGlobalLabel: 'Update Stats',
82
+ analyzeGlobalTitle: 'Update Statistics',
83
+ analyzeGlobalDesc: 'Updates query optimizer statistics for all tables to improve query performance.',
84
+ vacuumGlobalLabel: 'Rebuild Indexes',
85
+ vacuumGlobalTitle: 'Rebuild All Indexes',
86
+ vacuumGlobalDesc: 'Rebuilds all indexes to reclaim space and reduce fragmentation.',
87
+ };
88
+ }
89
+
90
+ // ============================================================================
91
+ // SQL Dialect Overrides
92
+ // ============================================================================
93
+
94
+ protected override escapeIdentifier(identifier: string): string {
95
+ const escaped = identifier.replace(/\]/g, ']]');
96
+ return `[${escaped}]`;
97
+ }
98
+
99
+ // ============================================================================
100
+ // Validation
101
+ // ============================================================================
102
+
103
+ public validate(): void {
104
+ super.validate();
105
+
106
+ if (!this.config.connectionString) {
107
+ if (!this.config.host) {
108
+ throw new DatabaseConfigError('Host is required for SQL Server', 'mssql');
109
+ }
110
+ if (!this.config.database) {
111
+ throw new DatabaseConfigError('Database name is required for SQL Server', 'mssql');
112
+ }
113
+ }
114
+ }
115
+
116
+ // ============================================================================
117
+ // Connection Management
118
+ // ============================================================================
119
+
120
+ private buildConfig(): mssql.config {
121
+ const host = this.config.host || 'localhost';
122
+ const port = this.config.port || 1433;
123
+ const isAzure = host.endsWith('.database.windows.net');
124
+
125
+ const sslConfig = this.config.ssl;
126
+ // SQL Server 2022+ enforces encryption by default; always encrypt and trust self-signed certs for non-Azure
127
+ let encrypt = true;
128
+ let trustServerCertificate = !isAzure;
129
+
130
+ if (sslConfig) {
131
+ if (sslConfig.mode === 'disable') {
132
+ encrypt = false;
133
+ } else {
134
+ encrypt = true;
135
+ trustServerCertificate = sslConfig.mode === 'require';
136
+ }
137
+ }
138
+
139
+ const config: mssql.config = {
140
+ user: this.config.user,
141
+ password: this.config.password,
142
+ server: host,
143
+ port,
144
+ database: this.config.database,
145
+ pool: {
146
+ min: this.poolConfig.min,
147
+ max: this.poolConfig.max,
148
+ idleTimeoutMillis: this.poolConfig.idleTimeout,
149
+ },
150
+ options: {
151
+ encrypt,
152
+ trustServerCertificate,
153
+ connectTimeout: this.poolConfig.acquireTimeout,
154
+ requestTimeout: this.queryTimeout,
155
+ },
156
+ };
157
+
158
+ // Named instance support
159
+ if (this.config.instanceName) {
160
+ config.options = {
161
+ ...config.options,
162
+ instanceName: this.config.instanceName,
163
+ };
164
+ // When using instance name, port is auto-negotiated via SQL Server Browser
165
+ delete (config as Record<string, unknown>).port;
166
+ }
167
+
168
+ return config;
169
+ }
170
+
171
+ public async connect(): Promise<void> {
172
+ if (this.pool) {
173
+ return;
174
+ }
175
+
176
+ try {
177
+ const config = this.buildConfig();
178
+ this.pool = new mssql.ConnectionPool(config);
179
+ await this.pool.connect();
180
+
181
+ // Test the connection
182
+ await this.pool.request().query('SELECT 1 AS test');
183
+
184
+ this.setConnected(true);
185
+ } catch (error) {
186
+ this.setError(error instanceof Error ? error : new Error(String(error)));
187
+ throw new ConnectionError(
188
+ `Failed to connect to SQL Server: ${error instanceof Error ? error.message : error}`,
189
+ 'mssql',
190
+ this.config.host,
191
+ this.config.port
192
+ );
193
+ }
194
+ }
195
+
196
+ public async disconnect(): Promise<void> {
197
+ if (this.pool) {
198
+ try {
199
+ await this.pool.close();
200
+ } catch {
201
+ // Force close on error
202
+ }
203
+ this.pool = null;
204
+ this.setConnected(false);
205
+ }
206
+ }
207
+
208
+ // ============================================================================
209
+ // Query Execution
210
+ // ============================================================================
211
+
212
+ public async query(sql: string, params?: unknown[], queryId?: string): Promise<QueryResult> {
213
+ this.ensureConnected();
214
+
215
+ return this.trackQuery(async () => {
216
+ const { result, executionTime } = await this.measureExecution(async () => {
217
+ try {
218
+ const request = this.pool!.request();
219
+
220
+ if (queryId) {
221
+ this.runningRequests.set(queryId, request);
222
+ }
223
+
224
+ // Add parameters
225
+ if (params && params.length > 0) {
226
+ params.forEach((p, i) => {
227
+ request.input(`p${i + 1}`, p);
228
+ });
229
+ }
230
+
231
+ const res = await request.query(sql);
232
+ return res;
233
+ } catch (error) {
234
+ throw mapDatabaseError(error, 'mssql', sql);
235
+ } finally {
236
+ if (queryId) this.runningRequests.delete(queryId);
237
+ }
238
+ });
239
+
240
+ const recordset = result.recordset || [];
241
+ const fields = recordset.columns
242
+ ? Object.keys(recordset.columns)
243
+ : recordset.length > 0
244
+ ? Object.keys(recordset[0])
245
+ : [];
246
+
247
+ return {
248
+ rows: recordset as Record<string, unknown>[],
249
+ fields,
250
+ rowCount: result.rowsAffected?.[0] ?? recordset.length,
251
+ executionTime,
252
+ };
253
+ });
254
+ }
255
+
256
+ public async cancelQuery(queryId: string): Promise<boolean> {
257
+ const request = this.runningRequests.get(queryId);
258
+ if (!request) return false;
259
+
260
+ try {
261
+ request.cancel();
262
+ return true;
263
+ } catch (error) {
264
+ console.error('[MSSQL] Failed to cancel query:', error);
265
+ return false;
266
+ }
267
+ }
268
+
269
+ // ============================================================================
270
+ // Query Preparation (MSSQL TOP / OFFSET FETCH)
271
+ // ============================================================================
272
+
273
+ public override prepareQuery(query: string, options: QueryPrepareOptions = {}): PreparedQuery {
274
+ const { limit = DEFAULT_QUERY_LIMIT, offset = 0, unlimited = false } = options;
275
+ const effectiveLimit = unlimited ? MAX_UNLIMITED_ROWS : limit;
276
+ const queryInfo = analyzeQuery(query);
277
+
278
+ if (queryInfo.type === 'SELECT' && !queryInfo.hasLimit) {
279
+ let modifiedSql = query.trim();
280
+ const hasSemicolon = modifiedSql.endsWith(';');
281
+ if (hasSemicolon) modifiedSql = modifiedSql.slice(0, -1).trim();
282
+
283
+ if (offset > 0) {
284
+ // OFFSET FETCH requires ORDER BY
285
+ const hasOrderBy = /\bORDER\s+BY\b/i.test(modifiedSql);
286
+ if (!hasOrderBy) {
287
+ modifiedSql = `${modifiedSql} ORDER BY (SELECT NULL)`;
288
+ }
289
+ modifiedSql = `${modifiedSql} OFFSET ${offset} ROWS FETCH NEXT ${effectiveLimit} ROWS ONLY`;
290
+ } else {
291
+ // Inject TOP N after SELECT
292
+ modifiedSql = modifiedSql.replace(
293
+ /^(\s*SELECT\s+)(DISTINCT\s+)?/i,
294
+ `$1$2TOP ${effectiveLimit} `
295
+ );
296
+ }
297
+
298
+ if (hasSemicolon) modifiedSql += ';';
299
+
300
+ return {
301
+ query: modifiedSql,
302
+ wasLimited: true,
303
+ limit: effectiveLimit,
304
+ offset,
305
+ };
306
+ }
307
+
308
+ return { query, wasLimited: false, limit: effectiveLimit, offset };
309
+ }
310
+
311
+ // ============================================================================
312
+ // Transaction Support
313
+ // ============================================================================
314
+
315
+ public async beginTransaction(): Promise<void> {
316
+ this.ensureConnected();
317
+ if (this.txActive) throw new QueryError('Transaction already active', 'mssql');
318
+ this.txTransaction = new mssql.Transaction(this.pool!);
319
+ await this.txTransaction.begin();
320
+ this.txActive = true;
321
+ }
322
+
323
+ public async commitTransaction(): Promise<void> {
324
+ if (!this.txTransaction || !this.txActive) throw new QueryError('No active transaction', 'mssql');
325
+ try {
326
+ await this.txTransaction.commit();
327
+ } finally {
328
+ this.txTransaction = null;
329
+ this.txActive = false;
330
+ }
331
+ }
332
+
333
+ public async rollbackTransaction(): Promise<void> {
334
+ if (!this.txTransaction || !this.txActive) throw new QueryError('No active transaction', 'mssql');
335
+ try {
336
+ await this.txTransaction.rollback();
337
+ } finally {
338
+ this.txTransaction = null;
339
+ this.txActive = false;
340
+ }
341
+ }
342
+
343
+ public isInTransaction(): boolean {
344
+ return this.txActive;
345
+ }
346
+
347
+ public async queryInTransaction(sql: string, params?: unknown[]): Promise<QueryResult> {
348
+ if (!this.txTransaction || !this.txActive) throw new QueryError('No active transaction', 'mssql');
349
+
350
+ return this.trackQuery(async () => {
351
+ const { result, executionTime } = await this.measureExecution(async () => {
352
+ try {
353
+ const request = new mssql.Request(this.txTransaction!);
354
+ if (params && params.length > 0) {
355
+ params.forEach((p, i) => {
356
+ request.input(`p${i + 1}`, p);
357
+ });
358
+ }
359
+ return await request.query(sql);
360
+ } catch (error) {
361
+ throw mapDatabaseError(error, 'mssql', sql);
362
+ }
363
+ });
364
+
365
+ const recordset = result.recordset || [];
366
+ const fields = recordset.length > 0 ? Object.keys(recordset[0]) : [];
367
+
368
+ return {
369
+ rows: recordset as Record<string, unknown>[],
370
+ fields,
371
+ rowCount: result.rowsAffected?.[0] ?? recordset.length,
372
+ executionTime,
373
+ };
374
+ });
375
+ }
376
+
377
+ // ============================================================================
378
+ // Schema Operations
379
+ // ============================================================================
380
+
381
+ public async getSchema(): Promise<TableSchema[]> {
382
+ this.ensureConnected();
383
+
384
+ try {
385
+ // Get tables
386
+ const tablesRes = await this.pool!.request().query(`
387
+ SELECT
388
+ s.name AS schema_name,
389
+ t.name AS table_name,
390
+ SUM(p.rows) AS row_count
391
+ FROM sys.tables t
392
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
393
+ LEFT JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
394
+ WHERE t.type = 'U'
395
+ GROUP BY s.name, t.name
396
+ ORDER BY s.name, t.name
397
+ `);
398
+ const tables = tablesRes.recordset || [];
399
+
400
+ // Get columns
401
+ const colsRes = await this.pool!.request().query(`
402
+ SELECT
403
+ TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE,
404
+ IS_NULLABLE, COLUMN_DEFAULT, ORDINAL_POSITION
405
+ FROM INFORMATION_SCHEMA.COLUMNS
406
+ ORDER BY TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION
407
+ `);
408
+ const allCols = colsRes.recordset || [];
409
+
410
+ // Get primary keys
411
+ const pkRes = await this.pool!.request().query(`
412
+ SELECT
413
+ s.name AS schema_name,
414
+ t.name AS table_name,
415
+ c.name AS column_name
416
+ FROM sys.indexes i
417
+ JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
418
+ JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
419
+ JOIN sys.tables t ON i.object_id = t.object_id
420
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
421
+ WHERE i.is_primary_key = 1
422
+ `);
423
+ const pkMap = new Map<string, Set<string>>();
424
+ for (const row of pkRes.recordset || []) {
425
+ const key = `${row.schema_name}.${row.table_name}`;
426
+ if (!pkMap.has(key)) pkMap.set(key, new Set());
427
+ pkMap.get(key)!.add(row.column_name);
428
+ }
429
+
430
+ // Get foreign keys
431
+ const fkRes = await this.pool!.request().query(`
432
+ SELECT
433
+ OBJECT_SCHEMA_NAME(fk.parent_object_id) AS schema_name,
434
+ OBJECT_NAME(fk.parent_object_id) AS table_name,
435
+ COL_NAME(fkc.parent_object_id, fkc.parent_column_id) AS column_name,
436
+ OBJECT_NAME(fk.referenced_object_id) AS ref_table,
437
+ COL_NAME(fkc.referenced_object_id, fkc.referenced_column_id) AS ref_column
438
+ FROM sys.foreign_keys fk
439
+ JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id
440
+ `);
441
+ const fksByTable = new Map<string, Array<{ columnName: string; referencedTable: string; referencedColumn: string }>>();
442
+ for (const row of fkRes.recordset || []) {
443
+ const key = `${row.schema_name}.${row.table_name}`;
444
+ if (!fksByTable.has(key)) fksByTable.set(key, []);
445
+ fksByTable.get(key)!.push({
446
+ columnName: row.column_name,
447
+ referencedTable: row.ref_table,
448
+ referencedColumn: row.ref_column,
449
+ });
450
+ }
451
+
452
+ // Get indexes
453
+ const idxRes = await this.pool!.request().query(`
454
+ SELECT
455
+ s.name AS schema_name,
456
+ t.name AS table_name,
457
+ i.name AS index_name,
458
+ i.is_unique,
459
+ c.name AS column_name,
460
+ ic.key_ordinal
461
+ FROM sys.indexes i
462
+ JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
463
+ JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
464
+ JOIN sys.tables t ON i.object_id = t.object_id
465
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
466
+ WHERE i.name IS NOT NULL AND i.is_primary_key = 0
467
+ ORDER BY s.name, t.name, i.name, ic.key_ordinal
468
+ `);
469
+
470
+ const idxByTable = new Map<string, Map<string, { unique: boolean; columns: string[] }>>();
471
+ for (const row of idxRes.recordset || []) {
472
+ const key = `${row.schema_name}.${row.table_name}`;
473
+ if (!idxByTable.has(key)) idxByTable.set(key, new Map());
474
+ const tableIdxs = idxByTable.get(key)!;
475
+ if (!tableIdxs.has(row.index_name)) {
476
+ tableIdxs.set(row.index_name, { unique: row.is_unique, columns: [] });
477
+ }
478
+ tableIdxs.get(row.index_name)!.columns.push(row.column_name);
479
+ }
480
+
481
+ // Group columns by table
482
+ const colsByTable = new Map<string, typeof allCols>();
483
+ for (const c of allCols) {
484
+ const key = `${c.TABLE_SCHEMA}.${c.TABLE_NAME}`;
485
+ if (!colsByTable.has(key)) colsByTable.set(key, []);
486
+ colsByTable.get(key)!.push(c);
487
+ }
488
+
489
+ return tables.map((t: Record<string, unknown>) => {
490
+ const schemaName = String(t.schema_name || 'dbo');
491
+ const tableName = String(t.table_name || '');
492
+ const key = `${schemaName}.${tableName}`;
493
+ const displayName = schemaName === 'dbo' ? tableName : `${schemaName}.${tableName}`;
494
+ const pks = pkMap.get(key) || new Set();
495
+
496
+ const columns = (colsByTable.get(key) || []).map((c: Record<string, unknown>) => ({
497
+ name: String(c.COLUMN_NAME || ''),
498
+ type: String(c.DATA_TYPE || ''),
499
+ nullable: String(c.IS_NULLABLE || '') === 'YES',
500
+ isPrimary: pks.has(String(c.COLUMN_NAME || '')),
501
+ defaultValue: c.COLUMN_DEFAULT ? String(c.COLUMN_DEFAULT) : undefined,
502
+ }));
503
+
504
+ const foreignKeys = fksByTable.get(key) || [];
505
+
506
+ const tableIdxs = idxByTable.get(key) || new Map();
507
+ const indexes = Array.from(tableIdxs.entries()).map(([name, info]) => ({
508
+ name,
509
+ columns: info.columns,
510
+ unique: info.unique,
511
+ }));
512
+
513
+ return {
514
+ name: displayName,
515
+ rowCount: Number(t.row_count || 0),
516
+ columns,
517
+ indexes,
518
+ foreignKeys,
519
+ };
520
+ });
521
+ } catch (error) {
522
+ throw mapDatabaseError(error, 'mssql');
523
+ }
524
+ }
525
+
526
+ // ============================================================================
527
+ // Health & Monitoring
528
+ // ============================================================================
529
+
530
+ public async getHealth(): Promise<HealthInfo> {
531
+ this.ensureConnected();
532
+
533
+ try {
534
+ let activeConnections = 0;
535
+ let databaseSize = 'N/A';
536
+ let cacheHitRatio = 'N/A';
537
+ const slowQueries: SlowQuery[] = [];
538
+ const activeSessions: ActiveSession[] = [];
539
+
540
+ // Active connections
541
+ try {
542
+ const connRes = await this.pool!.request().query(
543
+ `SELECT COUNT(*) AS cnt FROM sys.dm_exec_sessions WHERE is_user_process = 1`
544
+ );
545
+ activeConnections = connRes.recordset[0]?.cnt || 0;
546
+ } catch { /* DMV may require permissions */ }
547
+
548
+ // Database size
549
+ try {
550
+ const sizeRes = await this.pool!.request().query(`
551
+ SELECT
552
+ CAST(SUM(size) * 8.0 / 1024 AS DECIMAL(10,2)) AS size_mb
553
+ FROM sys.database_files
554
+ `);
555
+ const mb = Number(sizeRes.recordset[0]?.size_mb || 0);
556
+ databaseSize = mb > 1024 ? `${(mb / 1024).toFixed(2)} GB` : `${mb} MB`;
557
+ } catch { /* ignore */ }
558
+
559
+ // Cache hit ratio
560
+ try {
561
+ const cacheRes = await this.pool!.request().query(`
562
+ SELECT
563
+ CAST(
564
+ (a.cntr_value * 1.0 / NULLIF(b.cntr_value, 0)) * 100
565
+ AS DECIMAL(5,2)
566
+ ) AS hit_ratio
567
+ FROM sys.dm_os_performance_counters a
568
+ CROSS JOIN sys.dm_os_performance_counters b
569
+ WHERE a.counter_name = 'Buffer cache hit ratio'
570
+ AND a.object_name LIKE '%Buffer Manager%'
571
+ AND b.counter_name = 'Buffer cache hit ratio base'
572
+ AND b.object_name LIKE '%Buffer Manager%'
573
+ `);
574
+ cacheHitRatio = `${cacheRes.recordset[0]?.hit_ratio || 0}%`;
575
+ } catch { /* ignore */ }
576
+
577
+ // Slow queries
578
+ try {
579
+ const slowRes = await this.pool!.request().query(`
580
+ SELECT TOP 5
581
+ SUBSTRING(qt.text, 1, 100) AS query,
582
+ qs.execution_count AS calls,
583
+ CAST(qs.total_elapsed_time / NULLIF(qs.execution_count, 0) / 1000.0 AS DECIMAL(10,2)) AS avg_time_ms
584
+ FROM sys.dm_exec_query_stats qs
585
+ CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
586
+ WHERE qs.execution_count > 0
587
+ ORDER BY qs.total_elapsed_time DESC
588
+ `);
589
+ for (const row of slowRes.recordset || []) {
590
+ slowQueries.push({
591
+ query: String(row.query || ''),
592
+ calls: Number(row.calls || 0),
593
+ avgTime: `${row.avg_time_ms}ms`,
594
+ });
595
+ }
596
+ } catch { /* DMV permissions */ }
597
+
598
+ // Active sessions
599
+ try {
600
+ const sessRes = await this.pool!.request().query(`
601
+ SELECT TOP 10
602
+ s.session_id AS pid,
603
+ s.login_name AS [user],
604
+ DB_NAME(s.database_id) AS [database],
605
+ s.status AS state,
606
+ ISNULL(SUBSTRING(t.text, 1, 100), '') AS query,
607
+ ISNULL(CAST(DATEDIFF(SECOND, s.last_request_start_time, GETDATE()) AS VARCHAR) + 's', 'N/A') AS duration
608
+ FROM sys.dm_exec_sessions s
609
+ LEFT JOIN sys.dm_exec_requests r ON s.session_id = r.session_id
610
+ OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) t
611
+ WHERE s.is_user_process = 1
612
+ ORDER BY s.last_request_start_time DESC
613
+ `);
614
+ for (const row of sessRes.recordset || []) {
615
+ activeSessions.push({
616
+ pid: Number(row.pid || 0),
617
+ user: String(row.user || 'unknown'),
618
+ database: String(row.database || ''),
619
+ state: String(row.state || 'unknown'),
620
+ query: String(row.query || ''),
621
+ duration: String(row.duration || 'N/A'),
622
+ });
623
+ }
624
+ } catch { /* ignore */ }
625
+
626
+ return { activeConnections, databaseSize, cacheHitRatio, slowQueries, activeSessions };
627
+ } catch (error) {
628
+ throw mapDatabaseError(error, 'mssql');
629
+ }
630
+ }
631
+
632
+ // ============================================================================
633
+ // Maintenance Operations
634
+ // ============================================================================
635
+
636
+ public async runMaintenance(type: MaintenanceType, target?: string): Promise<MaintenanceResult> {
637
+ this.ensureConnected();
638
+
639
+ const { result, executionTime } = await this.measureExecution(async () => {
640
+ try {
641
+ let sql = '';
642
+
643
+ switch (type) {
644
+ case 'analyze':
645
+ if (target) {
646
+ sql = `UPDATE STATISTICS [${target.replace(/\]/g, ']]')}]`;
647
+ } else {
648
+ sql = `EXEC sp_updatestats`;
649
+ }
650
+ break;
651
+ case 'check':
652
+ sql = `DBCC CHECKDB WITH NO_INFOMSGS`;
653
+ break;
654
+ case 'optimize':
655
+ if (target) {
656
+ sql = `ALTER INDEX ALL ON [${target.replace(/\]/g, ']]')}] REBUILD`;
657
+ } else {
658
+ // Rebuild all indexes on all tables
659
+ sql = `
660
+ DECLARE @sql NVARCHAR(MAX) = '';
661
+ SELECT @sql = @sql + 'ALTER INDEX ALL ON [' + s.name + '].[' + t.name + '] REBUILD;'
662
+ FROM sys.tables t
663
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
664
+ WHERE t.type = 'U';
665
+ EXEC sp_executesql @sql;
666
+ `;
667
+ }
668
+ break;
669
+ case 'kill':
670
+ if (!target) {
671
+ throw new QueryError('Target SPID is required for kill operation', 'mssql');
672
+ }
673
+ const spid = parseInt(target, 10);
674
+ if (isNaN(spid)) {
675
+ throw new QueryError('Invalid SPID for kill operation', 'mssql');
676
+ }
677
+ sql = `KILL ${spid}`;
678
+ break;
679
+ default:
680
+ throw new QueryError(`Unsupported maintenance type: ${type}`, 'mssql');
681
+ }
682
+
683
+ await this.pool!.request().query(sql);
684
+ return { success: true };
685
+ } catch (error) {
686
+ throw mapDatabaseError(error, 'mssql');
687
+ }
688
+ });
689
+
690
+ return {
691
+ success: result.success,
692
+ executionTime,
693
+ message: `${type.toUpperCase()} completed successfully`,
694
+ };
695
+ }
696
+
697
+ // ============================================================================
698
+ // Pool Statistics
699
+ // ============================================================================
700
+
701
+ public getPoolStats() {
702
+ if (!this.pool) {
703
+ return { total: 0, idle: 0, active: 0, waiting: 0 };
704
+ }
705
+
706
+ return {
707
+ total: this.pool.size,
708
+ idle: this.pool.available,
709
+ active: this.pool.size - this.pool.available,
710
+ waiting: this.pool.pending,
711
+ };
712
+ }
713
+
714
+ // ============================================================================
715
+ // Extended Monitoring Methods
716
+ // ============================================================================
717
+
718
+ public async getOverview(): Promise<DatabaseOverview> {
719
+ this.ensureConnected();
720
+
721
+ try {
722
+ let version = 'SQL Server';
723
+ let uptime = 'N/A';
724
+ let startTime: Date | undefined;
725
+ let activeConnections = 0;
726
+ let maxConnections = 0;
727
+ let databaseSize = '0 bytes';
728
+ let databaseSizeBytes = 0;
729
+ let tableCount = 0;
730
+ let indexCount = 0;
731
+
732
+ // Version
733
+ try {
734
+ const vRes = await this.pool!.request().query(`SELECT @@VERSION AS version`);
735
+ version = String(vRes.recordset[0]?.version || '').split('\n')[0];
736
+ } catch { /* ignore */ }
737
+
738
+ // Uptime
739
+ try {
740
+ const upRes = await this.pool!.request().query(`
741
+ SELECT sqlserver_start_time,
742
+ DATEDIFF(SECOND, sqlserver_start_time, GETDATE()) AS uptime_seconds
743
+ FROM sys.dm_os_sys_info
744
+ `);
745
+ if (upRes.recordset[0]) {
746
+ const secs = Number(upRes.recordset[0].uptime_seconds || 0);
747
+ const days = Math.floor(secs / 86400);
748
+ const hours = Math.floor((secs % 86400) / 3600);
749
+ const minutes = Math.floor((secs % 3600) / 60);
750
+ uptime = days > 0 ? `${days}d ${hours}h ${minutes}m` : hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
751
+ startTime = new Date(upRes.recordset[0].sqlserver_start_time);
752
+ }
753
+ } catch { /* ignore */ }
754
+
755
+ // Connections
756
+ try {
757
+ const connRes = await this.pool!.request().query(`
758
+ SELECT
759
+ COUNT(*) AS active_connections,
760
+ (SELECT CAST(value_in_use AS INT) FROM sys.configurations WHERE name = 'user connections') AS max_connections
761
+ FROM sys.dm_exec_sessions
762
+ WHERE is_user_process = 1
763
+ `);
764
+ activeConnections = Number(connRes.recordset[0]?.active_connections || 0);
765
+ maxConnections = Number(connRes.recordset[0]?.max_connections || 32767);
766
+ if (maxConnections === 0) maxConnections = 32767; // 0 means unlimited
767
+ } catch { /* ignore */ }
768
+
769
+ // Database size
770
+ try {
771
+ const sizeRes = await this.pool!.request().query(`
772
+ SELECT SUM(CAST(size AS BIGINT)) * 8 * 1024 AS size_bytes FROM sys.database_files
773
+ `);
774
+ databaseSizeBytes = Number(sizeRes.recordset[0]?.size_bytes || 0);
775
+ databaseSize = formatBytes(databaseSizeBytes);
776
+ } catch { /* ignore */ }
777
+
778
+ // Table/index counts
779
+ try {
780
+ const cntRes = await this.pool!.request().query(`
781
+ SELECT
782
+ (SELECT COUNT(*) FROM sys.tables WHERE type = 'U') AS table_count,
783
+ (SELECT COUNT(*) FROM sys.indexes WHERE object_id IN (SELECT object_id FROM sys.tables WHERE type = 'U') AND name IS NOT NULL) AS index_count
784
+ `);
785
+ tableCount = Number(cntRes.recordset[0]?.table_count || 0);
786
+ indexCount = Number(cntRes.recordset[0]?.index_count || 0);
787
+ } catch { /* ignore */ }
788
+
789
+ return {
790
+ version, uptime, startTime, activeConnections, maxConnections,
791
+ databaseSize, databaseSizeBytes, tableCount, indexCount,
792
+ };
793
+ } catch (error) {
794
+ throw mapDatabaseError(error, 'mssql');
795
+ }
796
+ }
797
+
798
+ public async getPerformanceMetrics(): Promise<PerformanceMetrics> {
799
+ this.ensureConnected();
800
+
801
+ try {
802
+ let cacheHitRatio = 100;
803
+ let bufferPoolUsage: number | undefined;
804
+
805
+ try {
806
+ const cacheRes = await this.pool!.request().query(`
807
+ SELECT
808
+ CAST(
809
+ (a.cntr_value * 1.0 / NULLIF(b.cntr_value, 0)) * 100
810
+ AS DECIMAL(5,2)
811
+ ) AS hit_ratio
812
+ FROM sys.dm_os_performance_counters a
813
+ CROSS JOIN sys.dm_os_performance_counters b
814
+ WHERE a.counter_name = 'Buffer cache hit ratio'
815
+ AND a.object_name LIKE '%Buffer Manager%'
816
+ AND b.counter_name = 'Buffer cache hit ratio base'
817
+ AND b.object_name LIKE '%Buffer Manager%'
818
+ `);
819
+ cacheHitRatio = Number(cacheRes.recordset[0]?.hit_ratio || 100);
820
+ bufferPoolUsage = cacheHitRatio;
821
+ } catch { /* ignore */ }
822
+
823
+ return {
824
+ cacheHitRatio,
825
+ bufferPoolUsage,
826
+ };
827
+ } catch (error) {
828
+ throw mapDatabaseError(error, 'mssql');
829
+ }
830
+ }
831
+
832
+ public async getSlowQueries(options?: { limit?: number }): Promise<SlowQueryStats[]> {
833
+ this.ensureConnected();
834
+ const limit = options?.limit ?? 10;
835
+
836
+ try {
837
+ const res = await this.pool!.request().query(`
838
+ SELECT TOP ${limit}
839
+ CAST(qs.query_hash AS VARCHAR(50)) AS query_id,
840
+ SUBSTRING(qt.text, 1, 500) AS query,
841
+ qs.execution_count AS calls,
842
+ CAST(qs.total_elapsed_time / 1000.0 AS DECIMAL(18,2)) AS total_time,
843
+ CAST(qs.total_elapsed_time / NULLIF(qs.execution_count, 0) / 1000.0 AS DECIMAL(18,2)) AS avg_time,
844
+ CAST(qs.min_elapsed_time / 1000.0 AS DECIMAL(18,2)) AS min_time,
845
+ CAST(qs.max_elapsed_time / 1000.0 AS DECIMAL(18,2)) AS max_time,
846
+ qs.total_rows AS row_cnt,
847
+ qs.total_logical_reads AS logical_reads,
848
+ qs.total_physical_reads AS physical_reads
849
+ FROM sys.dm_exec_query_stats qs
850
+ CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
851
+ WHERE qs.execution_count > 0
852
+ ORDER BY qs.total_elapsed_time DESC
853
+ `);
854
+
855
+ return (res.recordset || []).map((r: Record<string, unknown>) => ({
856
+ queryId: String(r.query_id || ''),
857
+ query: String(r.query || ''),
858
+ calls: Number(r.calls || 0),
859
+ totalTime: Number(r.total_time || 0),
860
+ avgTime: Number(r.avg_time || 0),
861
+ minTime: Number(r.min_time || 0),
862
+ maxTime: Number(r.max_time || 0),
863
+ rows: Number(r.row_cnt || 0),
864
+ sharedBlksHit: Number(r.logical_reads || 0),
865
+ sharedBlksRead: Number(r.physical_reads || 0),
866
+ }));
867
+ } catch {
868
+ return [];
869
+ }
870
+ }
871
+
872
+ public async getActiveSessions(options?: { limit?: number }): Promise<ActiveSessionDetails[]> {
873
+ this.ensureConnected();
874
+ const limit = options?.limit ?? 50;
875
+
876
+ try {
877
+ const res = await this.pool!.request().query(`
878
+ SELECT TOP ${limit}
879
+ s.session_id AS pid,
880
+ s.login_name AS [user],
881
+ DB_NAME(s.database_id) AS [database],
882
+ s.program_name AS application_name,
883
+ s.host_name AS client_addr,
884
+ s.status AS state,
885
+ ISNULL(SUBSTRING(t.text, 1, 500), '') AS query,
886
+ s.last_request_start_time AS query_start,
887
+ ISNULL(CAST(DATEDIFF(SECOND, s.last_request_start_time, GETDATE()) AS VARCHAR) + 's', 'N/A') AS duration,
888
+ ISNULL(DATEDIFF(MILLISECOND, s.last_request_start_time, GETDATE()), 0) AS duration_ms,
889
+ r.wait_type,
890
+ r.last_wait_type,
891
+ CASE WHEN r.blocking_session_id > 0 THEN 1 ELSE 0 END AS is_blocked
892
+ FROM sys.dm_exec_sessions s
893
+ LEFT JOIN sys.dm_exec_requests r ON s.session_id = r.session_id
894
+ OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) t
895
+ WHERE s.is_user_process = 1
896
+ ORDER BY
897
+ CASE s.status WHEN 'running' THEN 0 WHEN 'sleeping' THEN 1 ELSE 2 END,
898
+ s.last_request_start_time DESC
899
+ `);
900
+
901
+ return (res.recordset || []).map((r: Record<string, unknown>) => ({
902
+ pid: Number(r.pid || 0),
903
+ user: String(r.user || 'unknown'),
904
+ database: String(r.database || ''),
905
+ applicationName: r.application_name ? String(r.application_name) : undefined,
906
+ clientAddr: r.client_addr ? String(r.client_addr) : undefined,
907
+ state: String(r.state || 'unknown'),
908
+ query: String(r.query || ''),
909
+ queryStart: r.query_start ? new Date(String(r.query_start)) : undefined,
910
+ duration: String(r.duration || 'N/A'),
911
+ durationMs: Number(r.duration_ms || 0),
912
+ waitEventType: r.wait_type ? String(r.wait_type) : undefined,
913
+ waitEvent: r.last_wait_type ? String(r.last_wait_type) : undefined,
914
+ blocked: Boolean(r.is_blocked),
915
+ }));
916
+ } catch {
917
+ return [];
918
+ }
919
+ }
920
+
921
+ public async getTableStats(): Promise<TableStats[]> {
922
+ this.ensureConnected();
923
+
924
+ try {
925
+ const res = await this.pool!.request().query(`
926
+ SELECT
927
+ s.name AS schema_name,
928
+ t.name AS table_name,
929
+ SUM(p.rows) AS row_count,
930
+ SUM(a.total_pages) * 8 * 1024 AS total_size_bytes,
931
+ SUM(a.used_pages) * 8 * 1024 AS used_size_bytes,
932
+ SUM(CASE WHEN i.type IN (0, 1) THEN a.total_pages ELSE 0 END) * 8 * 1024 AS table_size_bytes,
933
+ SUM(CASE WHEN i.type > 1 THEN a.total_pages ELSE 0 END) * 8 * 1024 AS index_size_bytes,
934
+ STATS_DATE(t.object_id, 1) AS last_stats_update
935
+ FROM sys.tables t
936
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
937
+ JOIN sys.indexes i ON t.object_id = i.object_id
938
+ JOIN sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id
939
+ JOIN sys.allocation_units a ON p.partition_id = a.container_id
940
+ WHERE t.type = 'U'
941
+ GROUP BY s.name, t.name, t.object_id
942
+ ORDER BY SUM(a.total_pages) DESC
943
+ `);
944
+
945
+ return (res.recordset || []).map((r: Record<string, unknown>) => {
946
+ const tableSizeBytes = Number(r.table_size_bytes || 0);
947
+ const indexSizeBytes = Number(r.index_size_bytes || 0);
948
+ const totalSizeBytes = Number(r.total_size_bytes || 0);
949
+ return {
950
+ schemaName: String(r.schema_name || 'dbo'),
951
+ tableName: String(r.table_name || ''),
952
+ rowCount: Number(r.row_count || 0),
953
+ tableSize: formatBytes(tableSizeBytes),
954
+ tableSizeBytes,
955
+ indexSize: formatBytes(indexSizeBytes),
956
+ indexSizeBytes,
957
+ totalSize: formatBytes(totalSizeBytes),
958
+ totalSizeBytes,
959
+ lastAnalyze: r.last_stats_update ? new Date(String(r.last_stats_update)) : undefined,
960
+ };
961
+ });
962
+ } catch {
963
+ return [];
964
+ }
965
+ }
966
+
967
+ public async getIndexStats(): Promise<IndexStats[]> {
968
+ this.ensureConnected();
969
+
970
+ try {
971
+ const res = await this.pool!.request().query(`
972
+ SELECT
973
+ s.name AS schema_name,
974
+ t.name AS table_name,
975
+ i.name AS index_name,
976
+ i.type_desc AS index_type,
977
+ i.is_unique,
978
+ i.is_primary_key,
979
+ SUM(a.total_pages) * 8 * 1024 AS index_size_bytes,
980
+ ISNULL(u.user_seeks + u.user_scans + u.user_lookups, 0) AS scans
981
+ FROM sys.indexes i
982
+ JOIN sys.tables t ON i.object_id = t.object_id
983
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
984
+ LEFT JOIN sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id
985
+ LEFT JOIN sys.allocation_units a ON p.partition_id = a.container_id
986
+ LEFT JOIN sys.dm_db_index_usage_stats u ON i.object_id = u.object_id AND i.index_id = u.index_id AND u.database_id = DB_ID()
987
+ WHERE i.name IS NOT NULL AND t.type = 'U'
988
+ GROUP BY s.name, t.name, i.name, i.type_desc, i.is_unique, i.is_primary_key,
989
+ i.object_id, i.index_id, u.user_seeks, u.user_scans, u.user_lookups
990
+ ORDER BY SUM(a.total_pages) DESC
991
+ `);
992
+
993
+ // Get columns for each index
994
+ const colRes = await this.pool!.request().query(`
995
+ SELECT
996
+ s.name AS schema_name,
997
+ t.name AS table_name,
998
+ i.name AS index_name,
999
+ c.name AS column_name,
1000
+ ic.key_ordinal
1001
+ FROM sys.index_columns ic
1002
+ JOIN sys.indexes i ON ic.object_id = i.object_id AND ic.index_id = i.index_id
1003
+ JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
1004
+ JOIN sys.tables t ON i.object_id = t.object_id
1005
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
1006
+ WHERE i.name IS NOT NULL AND t.type = 'U'
1007
+ ORDER BY s.name, t.name, i.name, ic.key_ordinal
1008
+ `);
1009
+
1010
+ const colMap = new Map<string, string[]>();
1011
+ for (const c of colRes.recordset || []) {
1012
+ const key = `${c.schema_name}.${c.table_name}.${c.index_name}`;
1013
+ if (!colMap.has(key)) colMap.set(key, []);
1014
+ colMap.get(key)!.push(String(c.column_name));
1015
+ }
1016
+
1017
+ return (res.recordset || []).map((r: Record<string, unknown>) => {
1018
+ const key = `${r.schema_name}.${r.table_name}.${r.index_name}`;
1019
+ const idxSizeBytes = Number(r.index_size_bytes || 0);
1020
+ return {
1021
+ schemaName: String(r.schema_name || 'dbo'),
1022
+ tableName: String(r.table_name || ''),
1023
+ indexName: String(r.index_name || ''),
1024
+ indexType: String(r.index_type || ''),
1025
+ columns: colMap.get(key) || [],
1026
+ isUnique: Boolean(r.is_unique),
1027
+ isPrimary: Boolean(r.is_primary_key),
1028
+ indexSize: formatBytes(idxSizeBytes),
1029
+ indexSizeBytes: idxSizeBytes,
1030
+ scans: Number(r.scans || 0),
1031
+ };
1032
+ });
1033
+ } catch {
1034
+ return [];
1035
+ }
1036
+ }
1037
+
1038
+ public async getStorageStats(): Promise<StorageStats[]> {
1039
+ this.ensureConnected();
1040
+
1041
+ try {
1042
+ const res = await this.pool!.request().query(`
1043
+ SELECT
1044
+ name,
1045
+ physical_name AS location,
1046
+ CAST(size AS BIGINT) * 8 * 1024 AS size_bytes,
1047
+ type_desc
1048
+ FROM sys.database_files
1049
+ ORDER BY size DESC
1050
+ `);
1051
+
1052
+ return (res.recordset || []).map((r: Record<string, unknown>) => {
1053
+ const sizeBytes = Number(r.size_bytes || 0);
1054
+ return {
1055
+ name: String(r.name || ''),
1056
+ location: String(r.location || ''),
1057
+ size: formatBytes(sizeBytes),
1058
+ sizeBytes,
1059
+ };
1060
+ });
1061
+ } catch {
1062
+ return [];
1063
+ }
1064
+ }
1065
+ }