@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,1044 @@
1
+ /**
2
+ * Oracle Database Provider
3
+ * Full Oracle support with connection pooling (Thin mode - no Instant Client needed)
4
+ */
5
+
6
+ import oracledb from 'oracledb';
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
+ // Oracle Provider
45
+ // ============================================================================
46
+
47
+ export class OracleProvider extends SQLBaseProvider {
48
+ private pool: oracledb.Pool | null = null;
49
+
50
+ // Transaction support: dedicated connection held outside pool
51
+ private txConn: oracledb.Connection | null = null;
52
+ private txActive = false;
53
+
54
+ // Track running connections for cancellation
55
+ private runningConns = new Map<string, oracledb.Connection>();
56
+
57
+ constructor(config: DatabaseConnection, options: ProviderOptions = {}) {
58
+ super(config, options);
59
+ // Use thin mode (pure JS, no Oracle Instant Client)
60
+ oracledb.initOracleClient = undefined as unknown as typeof oracledb.initOracleClient;
61
+ oracledb.outFormat = oracledb.OUT_FORMAT_OBJECT;
62
+ oracledb.autoCommit = true;
63
+ this.validate();
64
+ }
65
+
66
+ // ============================================================================
67
+ // Provider Metadata
68
+ // ============================================================================
69
+
70
+ public override getCapabilities(): ProviderCapabilities {
71
+ return {
72
+ ...super.getCapabilities(),
73
+ defaultPort: 1521,
74
+ supportsExplain: true,
75
+ supportsConnectionString: true,
76
+ maintenanceOperations: ['analyze', 'optimize', 'kill'],
77
+ };
78
+ }
79
+
80
+ public override getLabels(): ProviderLabels {
81
+ return {
82
+ ...super.getLabels(),
83
+ analyzeAction: 'Gather Statistics',
84
+ vacuumAction: 'Rebuild Indexes',
85
+ analyzeGlobalLabel: 'Gather Stats',
86
+ analyzeGlobalTitle: 'Gather Statistics',
87
+ analyzeGlobalDesc: 'Collects optimizer statistics for all tables to improve query performance.',
88
+ vacuumGlobalLabel: 'Rebuild Indexes',
89
+ vacuumGlobalTitle: 'Rebuild All Indexes',
90
+ vacuumGlobalDesc: 'Rebuilds all indexes to reclaim space and improve performance.',
91
+ };
92
+ }
93
+
94
+ // ============================================================================
95
+ // Validation
96
+ // ============================================================================
97
+
98
+ public validate(): void {
99
+ super.validate();
100
+
101
+ if (!this.config.connectionString) {
102
+ if (!this.config.host) {
103
+ throw new DatabaseConfigError('Host is required for Oracle', 'oracle');
104
+ }
105
+ }
106
+ }
107
+
108
+ // ============================================================================
109
+ // Connection Management
110
+ // ============================================================================
111
+
112
+ private getConnectString(): string {
113
+ if (this.config.connectionString) {
114
+ return this.config.connectionString;
115
+ }
116
+
117
+ const host = this.config.host || 'localhost';
118
+ const port = this.config.port || 1521;
119
+ const serviceName = this.config.serviceName || this.config.database || 'ORCL';
120
+
121
+ return `${host}:${port}/${serviceName}`;
122
+ }
123
+
124
+ public async connect(): Promise<void> {
125
+ if (this.pool) {
126
+ return;
127
+ }
128
+
129
+ try {
130
+ this.pool = await oracledb.createPool({
131
+ user: this.config.user,
132
+ password: this.config.password,
133
+ connectString: this.getConnectString(),
134
+ poolMin: this.poolConfig.min,
135
+ poolMax: this.poolConfig.max,
136
+ poolTimeout: Math.floor(this.poolConfig.idleTimeout / 1000),
137
+ });
138
+
139
+ // Test the connection
140
+ const conn = await this.pool.getConnection();
141
+ await conn.close();
142
+
143
+ this.setConnected(true);
144
+ } catch (error) {
145
+ this.setError(error instanceof Error ? error : new Error(String(error)));
146
+ throw new ConnectionError(
147
+ `Failed to connect to Oracle: ${error instanceof Error ? error.message : error}`,
148
+ 'oracle',
149
+ this.config.host,
150
+ this.config.port
151
+ );
152
+ }
153
+ }
154
+
155
+ public async disconnect(): Promise<void> {
156
+ if (this.pool) {
157
+ try {
158
+ await this.pool.close(0);
159
+ } catch {
160
+ // Force close on error
161
+ }
162
+ this.pool = null;
163
+ this.setConnected(false);
164
+ }
165
+ }
166
+
167
+ // ============================================================================
168
+ // Query Execution
169
+ // ============================================================================
170
+
171
+ public async query(sql: string, params?: unknown[], queryId?: string): Promise<QueryResult> {
172
+ this.ensureConnected();
173
+
174
+ return this.trackQuery(async () => {
175
+ const { result, executionTime } = await this.measureExecution(async () => {
176
+ let conn: oracledb.Connection | undefined;
177
+ try {
178
+ conn = await this.pool!.getConnection();
179
+
180
+ if (queryId) {
181
+ this.runningConns.set(queryId, conn);
182
+ }
183
+
184
+ const bindParams = params || [];
185
+ const res = await conn.execute(sql, bindParams, {
186
+ outFormat: oracledb.OUT_FORMAT_OBJECT,
187
+ autoCommit: true,
188
+ });
189
+
190
+ return res;
191
+ } catch (error) {
192
+ throw mapDatabaseError(error, 'oracle', sql);
193
+ } finally {
194
+ if (queryId) this.runningConns.delete(queryId);
195
+ if (conn) {
196
+ try { await conn.close(); } catch { /* ignore */ }
197
+ }
198
+ }
199
+ });
200
+
201
+ const rows = (result.rows || []) as Record<string, unknown>[];
202
+ const fields = result.metaData?.map((m: { name: string }) => m.name) ?? [];
203
+
204
+ return {
205
+ rows,
206
+ fields,
207
+ rowCount: rows.length,
208
+ executionTime,
209
+ };
210
+ });
211
+ }
212
+
213
+ public async cancelQuery(queryId: string): Promise<boolean> {
214
+ const conn = this.runningConns.get(queryId);
215
+ if (!conn) return false;
216
+
217
+ try {
218
+ await conn.break();
219
+ return true;
220
+ } catch (error) {
221
+ console.error('[Oracle] Failed to cancel query:', error);
222
+ return false;
223
+ }
224
+ }
225
+
226
+ // ============================================================================
227
+ // Query Preparation (Oracle FETCH FIRST instead of LIMIT)
228
+ // ============================================================================
229
+
230
+ public override prepareQuery(query: string, options: QueryPrepareOptions = {}): PreparedQuery {
231
+ const { limit = DEFAULT_QUERY_LIMIT, offset = 0, unlimited = false } = options;
232
+ const effectiveLimit = unlimited ? MAX_UNLIMITED_ROWS : limit;
233
+ const queryInfo = analyzeQuery(query);
234
+
235
+ if (queryInfo.type === 'SELECT' && !queryInfo.hasLimit) {
236
+ let modifiedSql = query.trim();
237
+ const hasSemicolon = modifiedSql.endsWith(';');
238
+ if (hasSemicolon) modifiedSql = modifiedSql.slice(0, -1).trim();
239
+
240
+ if (offset > 0) {
241
+ modifiedSql = `${modifiedSql} OFFSET ${offset} ROWS FETCH NEXT ${effectiveLimit} ROWS ONLY`;
242
+ } else {
243
+ modifiedSql = `${modifiedSql} FETCH FIRST ${effectiveLimit} ROWS ONLY`;
244
+ }
245
+
246
+ if (hasSemicolon) modifiedSql += ';';
247
+
248
+ return {
249
+ query: modifiedSql,
250
+ wasLimited: true,
251
+ limit: effectiveLimit,
252
+ offset,
253
+ };
254
+ }
255
+
256
+ return { query, wasLimited: false, limit: effectiveLimit, offset };
257
+ }
258
+
259
+ // ============================================================================
260
+ // Transaction Support
261
+ // ============================================================================
262
+
263
+ public async beginTransaction(): Promise<void> {
264
+ this.ensureConnected();
265
+ if (this.txActive) throw new QueryError('Transaction already active', 'oracle');
266
+ this.txConn = await this.pool!.getConnection();
267
+ // Oracle auto-starts a transaction; we just hold the connection
268
+ this.txActive = true;
269
+ }
270
+
271
+ public async commitTransaction(): Promise<void> {
272
+ if (!this.txConn || !this.txActive) throw new QueryError('No active transaction', 'oracle');
273
+ try {
274
+ await this.txConn.commit();
275
+ } finally {
276
+ await this.txConn.close();
277
+ this.txConn = null;
278
+ this.txActive = false;
279
+ }
280
+ }
281
+
282
+ public async rollbackTransaction(): Promise<void> {
283
+ if (!this.txConn || !this.txActive) throw new QueryError('No active transaction', 'oracle');
284
+ try {
285
+ await this.txConn.rollback();
286
+ } finally {
287
+ await this.txConn.close();
288
+ this.txConn = null;
289
+ this.txActive = false;
290
+ }
291
+ }
292
+
293
+ public isInTransaction(): boolean {
294
+ return this.txActive;
295
+ }
296
+
297
+ public async queryInTransaction(sql: string, params?: unknown[]): Promise<QueryResult> {
298
+ if (!this.txConn || !this.txActive) throw new QueryError('No active transaction', 'oracle');
299
+
300
+ return this.trackQuery(async () => {
301
+ const { result, executionTime } = await this.measureExecution(async () => {
302
+ try {
303
+ return await this.txConn!.execute(sql, params || [], {
304
+ outFormat: oracledb.OUT_FORMAT_OBJECT,
305
+ autoCommit: false,
306
+ });
307
+ } catch (error) {
308
+ throw mapDatabaseError(error, 'oracle', sql);
309
+ }
310
+ });
311
+
312
+ const rows = (result.rows || []) as Record<string, unknown>[];
313
+ const fields = result.metaData?.map((m: { name: string }) => m.name) ?? [];
314
+
315
+ return {
316
+ rows,
317
+ fields,
318
+ rowCount: rows.length,
319
+ executionTime,
320
+ };
321
+ });
322
+ }
323
+
324
+ // ============================================================================
325
+ // Schema Operations
326
+ // ============================================================================
327
+
328
+ public async getSchema(): Promise<TableSchema[]> {
329
+ this.ensureConnected();
330
+
331
+ let conn: oracledb.Connection | undefined;
332
+ try {
333
+ conn = await this.pool!.getConnection();
334
+ const owner = this.config.user?.toUpperCase() || '';
335
+
336
+ // Get tables
337
+ const tablesRes = await conn.execute(
338
+ `SELECT TABLE_NAME, NUM_ROWS FROM ALL_TABLES WHERE OWNER = :1 ORDER BY TABLE_NAME`,
339
+ [owner],
340
+ { outFormat: oracledb.OUT_FORMAT_OBJECT }
341
+ );
342
+ const tables = (tablesRes.rows || []) as Record<string, unknown>[];
343
+
344
+ // Get columns
345
+ const colsRes = await conn.execute(
346
+ `SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, NULLABLE, DATA_DEFAULT, COLUMN_ID
347
+ FROM ALL_TAB_COLUMNS WHERE OWNER = :1
348
+ ORDER BY TABLE_NAME, COLUMN_ID`,
349
+ [owner],
350
+ { outFormat: oracledb.OUT_FORMAT_OBJECT }
351
+ );
352
+ const allCols = (colsRes.rows || []) as Record<string, unknown>[];
353
+
354
+ // Get primary keys
355
+ const pkRes = await conn.execute(
356
+ `SELECT ac.TABLE_NAME, acc.COLUMN_NAME
357
+ FROM ALL_CONSTRAINTS ac
358
+ JOIN ALL_CONS_COLUMNS acc ON ac.CONSTRAINT_NAME = acc.CONSTRAINT_NAME AND ac.OWNER = acc.OWNER
359
+ WHERE ac.OWNER = :1 AND ac.CONSTRAINT_TYPE = 'P'`,
360
+ [owner],
361
+ { outFormat: oracledb.OUT_FORMAT_OBJECT }
362
+ );
363
+ const pkRows = (pkRes.rows || []) as Record<string, unknown>[];
364
+ const pkMap = new Map<string, Set<string>>();
365
+ for (const row of pkRows) {
366
+ const tbl = String(row.TABLE_NAME || '');
367
+ const col = String(row.COLUMN_NAME || '');
368
+ if (!pkMap.has(tbl)) pkMap.set(tbl, new Set());
369
+ pkMap.get(tbl)!.add(col);
370
+ }
371
+
372
+ // Get foreign keys
373
+ const fkRes = await conn.execute(
374
+ `SELECT ac.TABLE_NAME,
375
+ acc.COLUMN_NAME,
376
+ rc.TABLE_NAME AS REF_TABLE,
377
+ rcc.COLUMN_NAME AS REF_COLUMN
378
+ FROM ALL_CONSTRAINTS ac
379
+ JOIN ALL_CONS_COLUMNS acc ON ac.CONSTRAINT_NAME = acc.CONSTRAINT_NAME AND ac.OWNER = acc.OWNER
380
+ JOIN ALL_CONSTRAINTS rc ON ac.R_CONSTRAINT_NAME = rc.CONSTRAINT_NAME AND ac.R_OWNER = rc.OWNER
381
+ JOIN ALL_CONS_COLUMNS rcc ON rc.CONSTRAINT_NAME = rcc.CONSTRAINT_NAME AND rc.OWNER = rcc.OWNER
382
+ WHERE ac.OWNER = :1 AND ac.CONSTRAINT_TYPE = 'R'`,
383
+ [owner],
384
+ { outFormat: oracledb.OUT_FORMAT_OBJECT }
385
+ );
386
+ const fkRows = (fkRes.rows || []) as Record<string, unknown>[];
387
+
388
+ // Get indexes
389
+ const idxRes = await conn.execute(
390
+ `SELECT ai.TABLE_NAME, ai.INDEX_NAME, ai.UNIQUENESS, aic.COLUMN_NAME, aic.COLUMN_POSITION
391
+ FROM ALL_INDEXES ai
392
+ JOIN ALL_IND_COLUMNS aic ON ai.INDEX_NAME = aic.INDEX_NAME AND ai.OWNER = aic.INDEX_OWNER
393
+ WHERE ai.OWNER = :1
394
+ ORDER BY ai.TABLE_NAME, ai.INDEX_NAME, aic.COLUMN_POSITION`,
395
+ [owner],
396
+ { outFormat: oracledb.OUT_FORMAT_OBJECT }
397
+ );
398
+ const idxRows = (idxRes.rows || []) as Record<string, unknown>[];
399
+
400
+ // Group columns, indexes, foreign keys by table
401
+ const colsByTable = new Map<string, Record<string, unknown>[]>();
402
+ for (const c of allCols) {
403
+ const tbl = String(c.TABLE_NAME || '');
404
+ if (!colsByTable.has(tbl)) colsByTable.set(tbl, []);
405
+ colsByTable.get(tbl)!.push(c);
406
+ }
407
+
408
+ const fksByTable = new Map<string, Record<string, unknown>[]>();
409
+ for (const fk of fkRows) {
410
+ const tbl = String(fk.TABLE_NAME || '');
411
+ if (!fksByTable.has(tbl)) fksByTable.set(tbl, []);
412
+ fksByTable.get(tbl)!.push(fk);
413
+ }
414
+
415
+ const idxByTable = new Map<string, Map<string, { unique: boolean; columns: string[] }>>();
416
+ for (const idx of idxRows) {
417
+ const tbl = String(idx.TABLE_NAME || '');
418
+ const idxName = String(idx.INDEX_NAME || '');
419
+ if (!idxByTable.has(tbl)) idxByTable.set(tbl, new Map());
420
+ const tableIdxs = idxByTable.get(tbl)!;
421
+ if (!tableIdxs.has(idxName)) {
422
+ tableIdxs.set(idxName, {
423
+ unique: String(idx.UNIQUENESS || '') === 'UNIQUE',
424
+ columns: [],
425
+ });
426
+ }
427
+ tableIdxs.get(idxName)!.columns.push(String(idx.COLUMN_NAME || ''));
428
+ }
429
+
430
+ return tables.map((t) => {
431
+ const tableName = String(t.TABLE_NAME || '');
432
+ const pks = pkMap.get(tableName) || new Set();
433
+
434
+ const columns = (colsByTable.get(tableName) || []).map((c) => ({
435
+ name: String(c.COLUMN_NAME || ''),
436
+ type: String(c.DATA_TYPE || ''),
437
+ nullable: String(c.NULLABLE || '') === 'Y',
438
+ isPrimary: pks.has(String(c.COLUMN_NAME || '')),
439
+ defaultValue: c.DATA_DEFAULT ? String(c.DATA_DEFAULT).trim() : undefined,
440
+ }));
441
+
442
+ const foreignKeys = (fksByTable.get(tableName) || []).map((fk) => ({
443
+ columnName: String(fk.COLUMN_NAME || ''),
444
+ referencedTable: String(fk.REF_TABLE || ''),
445
+ referencedColumn: String(fk.REF_COLUMN || ''),
446
+ }));
447
+
448
+ const tableIdxs = idxByTable.get(tableName) || new Map();
449
+ const indexes = Array.from(tableIdxs.entries()).map(([name, info]) => ({
450
+ name,
451
+ columns: info.columns,
452
+ unique: info.unique,
453
+ }));
454
+
455
+ return {
456
+ name: tableName,
457
+ rowCount: Number(t.NUM_ROWS || 0),
458
+ columns,
459
+ indexes,
460
+ foreignKeys,
461
+ };
462
+ });
463
+ } finally {
464
+ if (conn) await conn.close();
465
+ }
466
+ }
467
+
468
+ // ============================================================================
469
+ // Health & Monitoring
470
+ // ============================================================================
471
+
472
+ public async getHealth(): Promise<HealthInfo> {
473
+ this.ensureConnected();
474
+
475
+ let conn: oracledb.Connection | undefined;
476
+ try {
477
+ conn = await this.pool!.getConnection();
478
+
479
+ let activeConnections = 0;
480
+ let databaseSize = 'N/A';
481
+ let cacheHitRatio = 'N/A';
482
+ const slowQueries: SlowQuery[] = [];
483
+ const activeSessions: ActiveSession[] = [];
484
+
485
+ // Active connections
486
+ try {
487
+ const connRes = await conn.execute(
488
+ `SELECT COUNT(*) AS CNT FROM V$SESSION WHERE STATUS = 'ACTIVE'`,
489
+ [], { outFormat: oracledb.OUT_FORMAT_OBJECT }
490
+ );
491
+ const rows = (connRes.rows || []) as Record<string, unknown>[];
492
+ activeConnections = Number(rows[0]?.CNT || 0);
493
+ } catch { /* V$ requires privileges */ }
494
+
495
+ // Database size
496
+ try {
497
+ const sizeRes = await conn.execute(
498
+ `SELECT ROUND(SUM(BYTES) / 1024 / 1024, 2) AS SIZE_MB FROM USER_SEGMENTS`,
499
+ [], { outFormat: oracledb.OUT_FORMAT_OBJECT }
500
+ );
501
+ const sizeRows = (sizeRes.rows || []) as Record<string, unknown>[];
502
+ const mb = Number(sizeRows[0]?.SIZE_MB || 0);
503
+ databaseSize = mb > 1024 ? `${(mb / 1024).toFixed(2)} GB` : `${mb} MB`;
504
+ } catch { /* ignore */ }
505
+
506
+ // Cache hit ratio
507
+ try {
508
+ const cacheRes = await conn.execute(
509
+ `SELECT ROUND(
510
+ (1 - (SUM(DECODE(NAME, 'physical reads', VALUE, 0)) /
511
+ NULLIF(SUM(DECODE(NAME, 'db block gets', VALUE, 0)) + SUM(DECODE(NAME, 'consistent gets', VALUE, 0)), 0)
512
+ )) * 100, 2) AS HIT_RATIO
513
+ FROM V$SYSSTAT
514
+ WHERE NAME IN ('db block gets', 'consistent gets', 'physical reads')`,
515
+ [], { outFormat: oracledb.OUT_FORMAT_OBJECT }
516
+ );
517
+ const cacheRows = (cacheRes.rows || []) as Record<string, unknown>[];
518
+ cacheHitRatio = `${cacheRows[0]?.HIT_RATIO || 0}%`;
519
+ } catch { /* ignore */ }
520
+
521
+ // Slow queries
522
+ try {
523
+ const slowRes = await conn.execute(
524
+ `SELECT * FROM (
525
+ SELECT SUBSTR(SQL_TEXT, 1, 100) AS QUERY,
526
+ EXECUTIONS AS CALLS,
527
+ ROUND(ELAPSED_TIME / NULLIF(EXECUTIONS, 0) / 1000, 2) || 'ms' AS AVGTIME
528
+ FROM V$SQL
529
+ WHERE EXECUTIONS > 0
530
+ ORDER BY ELAPSED_TIME DESC
531
+ ) WHERE ROWNUM <= 5`,
532
+ [], { outFormat: oracledb.OUT_FORMAT_OBJECT }
533
+ );
534
+ for (const row of (slowRes.rows || []) as Record<string, unknown>[]) {
535
+ slowQueries.push({
536
+ query: String(row.QUERY || ''),
537
+ calls: Number(row.CALLS || 0),
538
+ avgTime: String(row.AVGTIME || 'N/A'),
539
+ });
540
+ }
541
+ } catch { /* V$SQL requires privileges */ }
542
+
543
+ // Active sessions
544
+ try {
545
+ const sessRes = await conn.execute(
546
+ `SELECT * FROM (
547
+ SELECT SID, USERNAME, STATUS, SUBSTR(NVL(SQL_ID, ''), 1, 100) AS QUERY,
548
+ SCHEMANAME AS "DATABASE",
549
+ NVL(TO_CHAR(LOGON_TIME, 'HH24:MI:SS'), 'N/A') AS DURATION
550
+ FROM V$SESSION
551
+ WHERE TYPE = 'USER' AND STATUS = 'ACTIVE'
552
+ ORDER BY LOGON_TIME DESC
553
+ ) WHERE ROWNUM <= 10`,
554
+ [], { outFormat: oracledb.OUT_FORMAT_OBJECT }
555
+ );
556
+ for (const row of (sessRes.rows || []) as Record<string, unknown>[]) {
557
+ activeSessions.push({
558
+ pid: String(row.SID || ''),
559
+ user: String(row.USERNAME || 'unknown'),
560
+ database: String(row.DATABASE || ''),
561
+ state: String(row.STATUS || 'unknown'),
562
+ query: String(row.QUERY || ''),
563
+ duration: String(row.DURATION || 'N/A'),
564
+ });
565
+ }
566
+ } catch { /* ignore */ }
567
+
568
+ return { activeConnections, databaseSize, cacheHitRatio, slowQueries, activeSessions };
569
+ } finally {
570
+ if (conn) await conn.close();
571
+ }
572
+ }
573
+
574
+ // ============================================================================
575
+ // Maintenance Operations
576
+ // ============================================================================
577
+
578
+ public async runMaintenance(type: MaintenanceType, target?: string): Promise<MaintenanceResult> {
579
+ this.ensureConnected();
580
+
581
+ const { result, executionTime } = await this.measureExecution(async () => {
582
+ let conn: oracledb.Connection | undefined;
583
+ try {
584
+ conn = await this.pool!.getConnection();
585
+ let sql = '';
586
+
587
+ switch (type) {
588
+ case 'analyze':
589
+ if (target) {
590
+ sql = `BEGIN DBMS_STATS.GATHER_TABLE_STATS(USER, '${target.replace(/'/g, "''")}'); END;`;
591
+ } else {
592
+ sql = `BEGIN DBMS_STATS.GATHER_SCHEMA_STATS(USER); END;`;
593
+ }
594
+ break;
595
+ case 'optimize':
596
+ if (target) {
597
+ sql = `ALTER INDEX "${target.replace(/"/g, '""')}" REBUILD`;
598
+ } else {
599
+ // Rebuild all indexes for user
600
+ const idxRes = await conn.execute(
601
+ `SELECT INDEX_NAME FROM USER_INDEXES WHERE INDEX_TYPE = 'NORMAL'`,
602
+ [], { outFormat: oracledb.OUT_FORMAT_OBJECT }
603
+ );
604
+ for (const row of (idxRes.rows || []) as Record<string, unknown>[]) {
605
+ try {
606
+ await conn.execute(`ALTER INDEX "${String(row.INDEX_NAME)}" REBUILD`);
607
+ } catch { /* individual index rebuild may fail */ }
608
+ }
609
+ return { success: true };
610
+ }
611
+ break;
612
+ case 'kill':
613
+ if (!target) {
614
+ throw new QueryError('Target SID,SERIAL# is required for kill operation', 'oracle');
615
+ }
616
+ sql = `ALTER SYSTEM KILL SESSION '${target.replace(/'/g, "''")}'`;
617
+ break;
618
+ default:
619
+ throw new QueryError(`Unsupported maintenance type: ${type}`, 'oracle');
620
+ }
621
+
622
+ if (sql) {
623
+ await conn.execute(sql);
624
+ }
625
+ return { success: true };
626
+ } finally {
627
+ if (conn) await conn.close();
628
+ }
629
+ });
630
+
631
+ return {
632
+ success: result.success,
633
+ executionTime,
634
+ message: `${type.toUpperCase()} completed successfully`,
635
+ };
636
+ }
637
+
638
+ // ============================================================================
639
+ // Pool Statistics
640
+ // ============================================================================
641
+
642
+ public getPoolStats() {
643
+ if (!this.pool) {
644
+ return { total: 0, idle: 0, active: 0, waiting: 0 };
645
+ }
646
+
647
+ return {
648
+ total: this.pool.connectionsOpen,
649
+ idle: this.pool.connectionsOpen - this.pool.connectionsInUse,
650
+ active: this.pool.connectionsInUse,
651
+ waiting: 0,
652
+ };
653
+ }
654
+
655
+ // ============================================================================
656
+ // Extended Monitoring Methods
657
+ // ============================================================================
658
+
659
+ public async getOverview(): Promise<DatabaseOverview> {
660
+ this.ensureConnected();
661
+
662
+ let conn: oracledb.Connection | undefined;
663
+ try {
664
+ conn = await this.pool!.getConnection();
665
+
666
+ let version = 'Oracle';
667
+ let uptime = 'N/A';
668
+ let startTime: Date | undefined;
669
+ let activeConnections = 0;
670
+ let maxConnections = 0;
671
+ let databaseSize = '0 bytes';
672
+ let databaseSizeBytes = 0;
673
+ let tableCount = 0;
674
+ let indexCount = 0;
675
+
676
+ // Version and uptime
677
+ try {
678
+ const vRes = await conn.execute(
679
+ `SELECT BANNER FROM V$VERSION WHERE ROWNUM = 1`,
680
+ [], { outFormat: oracledb.OUT_FORMAT_OBJECT }
681
+ );
682
+ const vRows = (vRes.rows || []) as Record<string, unknown>[];
683
+ if (vRows[0]?.BANNER) version = String(vRows[0].BANNER);
684
+ } catch { /* ignore */ }
685
+
686
+ try {
687
+ const upRes = await conn.execute(
688
+ `SELECT STARTUP_TIME, (SYSDATE - STARTUP_TIME) * 86400 AS UPTIME_SECS FROM V$INSTANCE`,
689
+ [], { outFormat: oracledb.OUT_FORMAT_OBJECT }
690
+ );
691
+ const upRows = (upRes.rows || []) as Record<string, unknown>[];
692
+ if (upRows[0]) {
693
+ const secs = Number(upRows[0].UPTIME_SECS || 0);
694
+ const days = Math.floor(secs / 86400);
695
+ const hours = Math.floor((secs % 86400) / 3600);
696
+ const minutes = Math.floor((secs % 3600) / 60);
697
+ uptime = days > 0 ? `${days}d ${hours}h ${minutes}m` : hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
698
+ if (upRows[0].STARTUP_TIME) startTime = new Date(String(upRows[0].STARTUP_TIME));
699
+ }
700
+ } catch { /* ignore */ }
701
+
702
+ // Connections
703
+ try {
704
+ const sessRes = await conn.execute(
705
+ `SELECT COUNT(*) AS CNT FROM V$SESSION WHERE TYPE = 'USER'`,
706
+ [], { outFormat: oracledb.OUT_FORMAT_OBJECT }
707
+ );
708
+ activeConnections = Number(((sessRes.rows || []) as Record<string, unknown>[])[0]?.CNT || 0);
709
+
710
+ const maxRes = await conn.execute(
711
+ `SELECT VALUE FROM V$PARAMETER WHERE NAME = 'sessions'`,
712
+ [], { outFormat: oracledb.OUT_FORMAT_OBJECT }
713
+ );
714
+ maxConnections = Number(((maxRes.rows || []) as Record<string, unknown>[])[0]?.VALUE || 0);
715
+ } catch { /* ignore */ }
716
+
717
+ // Database size
718
+ try {
719
+ const sizeRes = await conn.execute(
720
+ `SELECT SUM(BYTES) AS TOTAL FROM USER_SEGMENTS`,
721
+ [], { outFormat: oracledb.OUT_FORMAT_OBJECT }
722
+ );
723
+ databaseSizeBytes = Number(((sizeRes.rows || []) as Record<string, unknown>[])[0]?.TOTAL || 0);
724
+ databaseSize = formatBytes(databaseSizeBytes);
725
+ } catch { /* ignore */ }
726
+
727
+ // Table and index counts
728
+ try {
729
+ const cntRes = await conn.execute(
730
+ `SELECT
731
+ (SELECT COUNT(*) FROM USER_TABLES) AS TABLE_COUNT,
732
+ (SELECT COUNT(*) FROM USER_INDEXES) AS INDEX_COUNT
733
+ FROM DUAL`,
734
+ [], { outFormat: oracledb.OUT_FORMAT_OBJECT }
735
+ );
736
+ const cntRows = (cntRes.rows || []) as Record<string, unknown>[];
737
+ tableCount = Number(cntRows[0]?.TABLE_COUNT || 0);
738
+ indexCount = Number(cntRows[0]?.INDEX_COUNT || 0);
739
+ } catch { /* ignore */ }
740
+
741
+ return {
742
+ version, uptime, startTime, activeConnections, maxConnections,
743
+ databaseSize, databaseSizeBytes, tableCount, indexCount,
744
+ };
745
+ } finally {
746
+ if (conn) await conn.close();
747
+ }
748
+ }
749
+
750
+ public async getPerformanceMetrics(): Promise<PerformanceMetrics> {
751
+ this.ensureConnected();
752
+
753
+ let conn: oracledb.Connection | undefined;
754
+ try {
755
+ conn = await this.pool!.getConnection();
756
+
757
+ let cacheHitRatio = 100;
758
+ let bufferPoolUsage: number | undefined;
759
+
760
+ try {
761
+ const cacheRes = await conn.execute(
762
+ `SELECT ROUND(
763
+ (1 - (SUM(DECODE(NAME, 'physical reads', VALUE, 0)) /
764
+ NULLIF(SUM(DECODE(NAME, 'db block gets', VALUE, 0)) + SUM(DECODE(NAME, 'consistent gets', VALUE, 0)), 0)
765
+ )) * 100, 2) AS HIT_RATIO
766
+ FROM V$SYSSTAT
767
+ WHERE NAME IN ('db block gets', 'consistent gets', 'physical reads')`,
768
+ [], { outFormat: oracledb.OUT_FORMAT_OBJECT }
769
+ );
770
+ const rows = (cacheRes.rows || []) as Record<string, unknown>[];
771
+ cacheHitRatio = Number(rows[0]?.HIT_RATIO || 100);
772
+ bufferPoolUsage = cacheHitRatio;
773
+ } catch { /* ignore */ }
774
+
775
+ return {
776
+ cacheHitRatio,
777
+ bufferPoolUsage,
778
+ };
779
+ } finally {
780
+ if (conn) await conn.close();
781
+ }
782
+ }
783
+
784
+ public async getSlowQueries(options?: { limit?: number }): Promise<SlowQueryStats[]> {
785
+ this.ensureConnected();
786
+ const limit = options?.limit ?? 10;
787
+
788
+ let conn: oracledb.Connection | undefined;
789
+ try {
790
+ conn = await this.pool!.getConnection();
791
+
792
+ const res = await conn.execute(
793
+ `SELECT * FROM (
794
+ SELECT SQL_ID AS QUERY_ID,
795
+ SUBSTR(SQL_TEXT, 1, 500) AS QUERY,
796
+ EXECUTIONS AS CALLS,
797
+ ROUND(ELAPSED_TIME / 1000, 2) AS TOTAL_TIME,
798
+ ROUND(ELAPSED_TIME / NULLIF(EXECUTIONS, 0) / 1000, 2) AS AVG_TIME,
799
+ ROWS_PROCESSED AS ROW_CNT,
800
+ BUFFER_GETS AS BUF_GETS,
801
+ DISK_READS
802
+ FROM V$SQL
803
+ WHERE EXECUTIONS > 0
804
+ ORDER BY ELAPSED_TIME DESC
805
+ ) WHERE ROWNUM <= ${limit}`,
806
+ [], { outFormat: oracledb.OUT_FORMAT_OBJECT }
807
+ );
808
+
809
+ return ((res.rows || []) as Record<string, unknown>[]).map((r) => ({
810
+ queryId: String(r.QUERY_ID || ''),
811
+ query: String(r.QUERY || ''),
812
+ calls: Number(r.CALLS || 0),
813
+ totalTime: Number(r.TOTAL_TIME || 0),
814
+ avgTime: Number(r.AVG_TIME || 0),
815
+ rows: Number(r.ROW_CNT || 0),
816
+ sharedBlksHit: Number(r.BUF_GETS || 0),
817
+ sharedBlksRead: Number(r.DISK_READS || 0),
818
+ }));
819
+ } catch {
820
+ return [];
821
+ } finally {
822
+ if (conn) await conn.close();
823
+ }
824
+ }
825
+
826
+ public async getActiveSessions(options?: { limit?: number }): Promise<ActiveSessionDetails[]> {
827
+ this.ensureConnected();
828
+ const limit = options?.limit ?? 50;
829
+
830
+ let conn: oracledb.Connection | undefined;
831
+ try {
832
+ conn = await this.pool!.getConnection();
833
+
834
+ const res = await conn.execute(
835
+ `SELECT * FROM (
836
+ SELECT s.SID, s.SERIAL#, s.USERNAME, s.SCHEMANAME, s.PROGRAM,
837
+ s.MACHINE, s.STATUS, s.SQL_ID,
838
+ SUBSTR(sq.SQL_TEXT, 1, 500) AS QUERY,
839
+ s.LOGON_TIME,
840
+ ROUND((SYSDATE - s.LOGON_TIME) * 86400) AS DURATION_SECS,
841
+ s.WAIT_CLASS, s.EVENT
842
+ FROM V$SESSION s
843
+ LEFT JOIN V$SQL sq ON s.SQL_ID = sq.SQL_ID AND s.SQL_CHILD_NUMBER = sq.CHILD_NUMBER
844
+ WHERE s.TYPE = 'USER'
845
+ ORDER BY CASE s.STATUS WHEN 'ACTIVE' THEN 0 ELSE 1 END, s.LOGON_TIME DESC
846
+ ) WHERE ROWNUM <= ${limit}`,
847
+ [], { outFormat: oracledb.OUT_FORMAT_OBJECT }
848
+ );
849
+
850
+ return ((res.rows || []) as Record<string, unknown>[]).map((r) => {
851
+ const secs = Number(r.DURATION_SECS || 0);
852
+ const durationStr = secs > 3600 ? `${Math.floor(secs / 3600)}h ${Math.floor((secs % 3600) / 60)}m`
853
+ : secs > 60 ? `${Math.floor(secs / 60)}m ${secs % 60}s`
854
+ : `${secs}s`;
855
+
856
+ return {
857
+ pid: `${r.SID},${r['SERIAL#']}`,
858
+ user: String(r.USERNAME || 'unknown'),
859
+ database: String(r.SCHEMANAME || ''),
860
+ applicationName: String(r.PROGRAM || ''),
861
+ clientAddr: String(r.MACHINE || ''),
862
+ state: String(r.STATUS || 'unknown'),
863
+ query: String(r.QUERY || r.SQL_ID || ''),
864
+ queryStart: r.LOGON_TIME ? new Date(String(r.LOGON_TIME)) : undefined,
865
+ duration: durationStr,
866
+ durationMs: secs * 1000,
867
+ waitEventType: r.WAIT_CLASS ? String(r.WAIT_CLASS) : undefined,
868
+ waitEvent: r.EVENT ? String(r.EVENT) : undefined,
869
+ blocked: false,
870
+ };
871
+ });
872
+ } catch {
873
+ return [];
874
+ } finally {
875
+ if (conn) await conn.close();
876
+ }
877
+ }
878
+
879
+ public async getTableStats(): Promise<TableStats[]> {
880
+ this.ensureConnected();
881
+
882
+ let conn: oracledb.Connection | undefined;
883
+ try {
884
+ conn = await this.pool!.getConnection();
885
+ const owner = this.config.user?.toUpperCase() || '';
886
+
887
+ const res = await conn.execute(
888
+ `SELECT t.TABLE_NAME,
889
+ NVL(t.NUM_ROWS, 0) AS ROW_COUNT,
890
+ NVL(s.BYTES, 0) AS TABLE_SIZE_BYTES,
891
+ NVL(idx_size.BYTES, 0) AS INDEX_SIZE_BYTES,
892
+ t.LAST_ANALYZED
893
+ FROM ALL_TABLES t
894
+ LEFT JOIN USER_SEGMENTS s ON s.SEGMENT_NAME = t.TABLE_NAME AND s.SEGMENT_TYPE = 'TABLE'
895
+ LEFT JOIN (
896
+ SELECT TABLE_NAME, SUM(BYTES) AS BYTES
897
+ FROM USER_SEGMENTS
898
+ WHERE SEGMENT_TYPE = 'INDEX'
899
+ GROUP BY TABLE_NAME
900
+ ) idx_size ON idx_size.TABLE_NAME = t.TABLE_NAME
901
+ WHERE t.OWNER = :1
902
+ ORDER BY NVL(s.BYTES, 0) DESC`,
903
+ [owner],
904
+ { outFormat: oracledb.OUT_FORMAT_OBJECT }
905
+ );
906
+
907
+ return ((res.rows || []) as Record<string, unknown>[]).map((r) => {
908
+ const tableSizeBytes = Number(r.TABLE_SIZE_BYTES || 0);
909
+ const indexSizeBytes = Number(r.INDEX_SIZE_BYTES || 0);
910
+ return {
911
+ schemaName: owner,
912
+ tableName: String(r.TABLE_NAME || ''),
913
+ rowCount: Number(r.ROW_COUNT || 0),
914
+ tableSize: formatBytes(tableSizeBytes),
915
+ tableSizeBytes,
916
+ indexSize: formatBytes(indexSizeBytes),
917
+ indexSizeBytes,
918
+ totalSize: formatBytes(tableSizeBytes + indexSizeBytes),
919
+ totalSizeBytes: tableSizeBytes + indexSizeBytes,
920
+ lastAnalyze: r.LAST_ANALYZED ? new Date(String(r.LAST_ANALYZED)) : undefined,
921
+ };
922
+ });
923
+ } catch {
924
+ return [];
925
+ } finally {
926
+ if (conn) await conn.close();
927
+ }
928
+ }
929
+
930
+ public async getIndexStats(): Promise<IndexStats[]> {
931
+ this.ensureConnected();
932
+
933
+ let conn: oracledb.Connection | undefined;
934
+ try {
935
+ conn = await this.pool!.getConnection();
936
+ const owner = this.config.user?.toUpperCase() || '';
937
+
938
+ const res = await conn.execute(
939
+ `SELECT ai.TABLE_NAME, ai.INDEX_NAME, ai.INDEX_TYPE, ai.UNIQUENESS,
940
+ NVL(us.BYTES, 0) AS INDEX_SIZE_BYTES,
941
+ ai.LEAF_BLOCKS, ai.DISTINCT_KEYS
942
+ FROM ALL_INDEXES ai
943
+ LEFT JOIN USER_SEGMENTS us ON us.SEGMENT_NAME = ai.INDEX_NAME AND us.SEGMENT_TYPE = 'INDEX'
944
+ WHERE ai.OWNER = :1
945
+ ORDER BY NVL(us.BYTES, 0) DESC`,
946
+ [owner],
947
+ { outFormat: oracledb.OUT_FORMAT_OBJECT }
948
+ );
949
+
950
+ // Get columns for each index
951
+ const colRes = await conn.execute(
952
+ `SELECT INDEX_NAME, COLUMN_NAME, COLUMN_POSITION
953
+ FROM ALL_IND_COLUMNS WHERE INDEX_OWNER = :1
954
+ ORDER BY INDEX_NAME, COLUMN_POSITION`,
955
+ [owner],
956
+ { outFormat: oracledb.OUT_FORMAT_OBJECT }
957
+ );
958
+
959
+ const colMap = new Map<string, string[]>();
960
+ for (const c of (colRes.rows || []) as Record<string, unknown>[]) {
961
+ const idxName = String(c.INDEX_NAME || '');
962
+ if (!colMap.has(idxName)) colMap.set(idxName, []);
963
+ colMap.get(idxName)!.push(String(c.COLUMN_NAME || ''));
964
+ }
965
+
966
+ return ((res.rows || []) as Record<string, unknown>[]).map((r) => {
967
+ const idxName = String(r.INDEX_NAME || '');
968
+ const idxSizeBytes = Number(r.INDEX_SIZE_BYTES || 0);
969
+ return {
970
+ schemaName: owner,
971
+ tableName: String(r.TABLE_NAME || ''),
972
+ indexName: idxName,
973
+ indexType: String(r.INDEX_TYPE || ''),
974
+ columns: colMap.get(idxName) || [],
975
+ isUnique: String(r.UNIQUENESS || '') === 'UNIQUE',
976
+ isPrimary: false,
977
+ indexSize: formatBytes(idxSizeBytes),
978
+ indexSizeBytes: idxSizeBytes,
979
+ scans: 0,
980
+ };
981
+ });
982
+ } catch {
983
+ return [];
984
+ } finally {
985
+ if (conn) await conn.close();
986
+ }
987
+ }
988
+
989
+ public async getStorageStats(): Promise<StorageStats[]> {
990
+ this.ensureConnected();
991
+
992
+ let conn: oracledb.Connection | undefined;
993
+ try {
994
+ conn = await this.pool!.getConnection();
995
+ const results: StorageStats[] = [];
996
+
997
+ // Try DBA tablespaces first, fallback to USER
998
+ try {
999
+ const tsRes = await conn.execute(
1000
+ `SELECT TABLESPACE_NAME AS NAME,
1001
+ SUM(BYTES) AS SIZE_BYTES
1002
+ FROM DBA_DATA_FILES
1003
+ GROUP BY TABLESPACE_NAME
1004
+ ORDER BY SUM(BYTES) DESC`,
1005
+ [], { outFormat: oracledb.OUT_FORMAT_OBJECT }
1006
+ );
1007
+
1008
+ for (const row of (tsRes.rows || []) as Record<string, unknown>[]) {
1009
+ const sizeBytes = Number(row.SIZE_BYTES || 0);
1010
+ results.push({
1011
+ name: String(row.NAME || ''),
1012
+ size: formatBytes(sizeBytes),
1013
+ sizeBytes,
1014
+ });
1015
+ }
1016
+ } catch {
1017
+ // Fallback: user segments
1018
+ try {
1019
+ const segRes = await conn.execute(
1020
+ `SELECT TABLESPACE_NAME AS NAME,
1021
+ SUM(BYTES) AS SIZE_BYTES
1022
+ FROM USER_SEGMENTS
1023
+ GROUP BY TABLESPACE_NAME
1024
+ ORDER BY SUM(BYTES) DESC`,
1025
+ [], { outFormat: oracledb.OUT_FORMAT_OBJECT }
1026
+ );
1027
+
1028
+ for (const row of (segRes.rows || []) as Record<string, unknown>[]) {
1029
+ const sizeBytes = Number(row.SIZE_BYTES || 0);
1030
+ results.push({
1031
+ name: String(row.NAME || ''),
1032
+ size: formatBytes(sizeBytes),
1033
+ sizeBytes,
1034
+ });
1035
+ }
1036
+ } catch { /* ignore */ }
1037
+ }
1038
+
1039
+ return results;
1040
+ } finally {
1041
+ if (conn) await conn.close();
1042
+ }
1043
+ }
1044
+ }