@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.
- package/.claude/settings.local.json +127 -0
- package/.cursorrules +426 -0
- package/.devin/wiki.json +143 -0
- package/.dockerignore +80 -0
- package/.env.example +159 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +49 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +29 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +57 -0
- package/.github/workflows/ci.yml +185 -0
- package/.github/workflows/codeql.yml +57 -0
- package/.github/workflows/docker-build-push.yml +118 -0
- package/.github/workflows/helm-release.yml +113 -0
- package/CLAUDE.md +265 -0
- package/CODE_OF_CONDUCT.md +124 -0
- package/CONTRIBUTING.md +154 -0
- package/Dockerfile +73 -0
- package/LICENSE +21 -0
- package/README.md +614 -0
- package/SECURITY.md +107 -0
- package/artifacthub-repo.yml +4 -0
- package/bun.lock +1714 -0
- package/bunfig.toml +3 -0
- package/charts/libredb-studio/.helmignore +11 -0
- package/charts/libredb-studio/Chart.lock +6 -0
- package/charts/libredb-studio/Chart.yaml +50 -0
- package/charts/libredb-studio/README.md +206 -0
- package/charts/libredb-studio/templates/NOTES.txt +59 -0
- package/charts/libredb-studio/templates/_helpers.tpl +135 -0
- package/charts/libredb-studio/templates/configmap.yaml +37 -0
- package/charts/libredb-studio/templates/deployment.yaml +184 -0
- package/charts/libredb-studio/templates/hpa.yaml +32 -0
- package/charts/libredb-studio/templates/ingress.yaml +41 -0
- package/charts/libredb-studio/templates/networkpolicy.yaml +50 -0
- package/charts/libredb-studio/templates/pdb.yaml +18 -0
- package/charts/libredb-studio/templates/pvc.yaml +23 -0
- package/charts/libredb-studio/templates/secret.yaml +30 -0
- package/charts/libredb-studio/templates/seed-configmap.yaml +11 -0
- package/charts/libredb-studio/templates/service.yaml +22 -0
- package/charts/libredb-studio/templates/serviceaccount.yaml +13 -0
- package/charts/libredb-studio/values.schema.json +246 -0
- package/charts/libredb-studio/values.yaml +286 -0
- package/components.json +22 -0
- package/conductor/code_styleguides/typescript.md +43 -0
- package/conductor/product-guidelines.md +43 -0
- package/conductor/product.md +3 -0
- package/conductor/setup_state.json +1 -0
- package/conductor/tech-stack.md +39 -0
- package/conductor/tracks/enhance_postgres_monitoring_20251227/metadata.json +8 -0
- package/conductor/tracks/enhance_postgres_monitoring_20251227/plan.md +44 -0
- package/conductor/tracks/enhance_postgres_monitoring_20251227/spec.md +31 -0
- package/conductor/tracks.md +8 -0
- package/conductor/workflow.md +333 -0
- package/database-compose.yml +55 -0
- package/docker/postgres-init/01-extensions.sql +10 -0
- package/docker/postgres-init/02-sample-data.sql +585 -0
- package/docker/postgres.yml +68 -0
- package/docker-compose.yml +38 -0
- package/docs/AI_PLAN.md +74 -0
- package/docs/API_DOCS.md +875 -0
- package/docs/ARCHITECTURE.md +218 -0
- package/docs/DATABASE_PROVIDERS.md +358 -0
- package/docs/FEATURES.md +116 -0
- package/docs/HELM_CHART.md +252 -0
- package/docs/LOGIN_PAGE.md +178 -0
- package/docs/MONACO_EDITOR_PERFORMANCE.md +315 -0
- package/docs/OIDC_ARCH.md +681 -0
- package/docs/OIDC_SETUP.md +322 -0
- package/docs/POSTGRES_METRICS.md +516 -0
- package/docs/QUERY_OPTIMIZATION.md +370 -0
- package/docs/SEED_CONNECTIONS.md +468 -0
- package/docs/SQL_ALIAS_COMPLETION.md +190 -0
- package/docs/STORAGE_ARCHITECTURE.md +565 -0
- package/docs/STORAGE_QUICK_SETUP.md +419 -0
- package/docs/TECHNICAL_PLAN.md +36 -0
- package/docs/THEMING.md +345 -0
- package/docs/adding-a-new-database-provider.md +642 -0
- package/docs/backlogs/000-PLATFORM_DATA_SYNC_DATABASE.md +360 -0
- package/docs/backlogs/001-INLINE_DATA_EDITING.md +118 -0
- package/docs/backlogs/002-DATA_IMPORT.md +215 -0
- package/docs/backlogs/003-QUERY_TIME_MACHINE.md +183 -0
- package/docs/backlogs/004-AI_DATA_STORYTELLER.md +292 -0
- package/docs/backlogs/005-QUERY_PLAYGROUND.md +352 -0
- package/docs/backlogs/006-DATA_MASKING.md +418 -0
- package/docs/enterprise-features.md +718 -0
- package/docs/kubernetes-helm-chart-artifacthub-plan.md +803 -0
- package/docs/medium-koyeb-article-en.md +215 -0
- package/docs/plans/test-plans.md +445 -0
- package/docs/releases/RELEASE.V0.3.0.md +22 -0
- package/docs/releases/RELEASE.V0.4.0.md +154 -0
- package/docs/releases/RELEASE.V0.5.0.md +252 -0
- package/docs/releases/RELEASE_v0.5.6.md +145 -0
- package/docs/releases/RELEASE_v0.6.1.md +303 -0
- package/docs/releases/RELEASE_v0.6.7.md +292 -0
- package/docs/releases/RELEASE_v0.7.0.md +332 -0
- package/docs/releases/RELEASE_v0.8.0.md +521 -0
- package/docs/sampledb/titanic.sql +1379 -0
- package/docs/superpowers/plans/2026-03-25-seed-connections.md +1362 -0
- package/docs/superpowers/specs/2026-03-25-seed-connections-design.md +590 -0
- package/e2e/admin-dashboard.spec.ts +64 -0
- package/e2e/connection-management.spec.ts +58 -0
- package/e2e/export.spec.ts +34 -0
- package/e2e/login.spec.ts +85 -0
- package/e2e/query-execution.spec.ts +35 -0
- package/e2e/tab-management.spec.ts +64 -0
- package/eslint.config.mjs +28 -0
- package/fly.toml +43 -0
- package/next.config.ts +32 -0
- package/package.json +130 -0
- package/playwright.config.ts +34 -0
- package/postcss.config.mjs +7 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/logo.svg +32 -0
- package/public/next.svg +1 -0
- package/public/screenshots/code-generator.png +0 -0
- package/public/screenshots/connection-modal.png +0 -0
- package/public/screenshots/data-profiler.png +0 -0
- package/public/screenshots/erd-diagram.png +0 -0
- package/public/screenshots/hero-editor.png +0 -0
- package/public/screenshots/nl2sql.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/render.yaml +58 -0
- package/scripts/merge-lcov.mjs +239 -0
- package/sonar-project.properties +16 -0
- package/src/app/admin/error.tsx +46 -0
- package/src/app/admin/page.tsx +10 -0
- package/src/app/api/admin/audit/route.ts +52 -0
- package/src/app/api/admin/fleet-health/route.ts +81 -0
- package/src/app/api/ai/autopilot/route.ts +105 -0
- package/src/app/api/ai/chat/route.ts +132 -0
- package/src/app/api/ai/describe-schema/route.ts +52 -0
- package/src/app/api/ai/explain/route.ts +86 -0
- package/src/app/api/ai/impact/route.ts +97 -0
- package/src/app/api/ai/index-advisor/route.ts +98 -0
- package/src/app/api/ai/nl2sql/route.ts +87 -0
- package/src/app/api/ai/query-safety/route.ts +87 -0
- package/src/app/api/auth/login/route.ts +62 -0
- package/src/app/api/auth/logout/route.ts +25 -0
- package/src/app/api/auth/me/route.ts +10 -0
- package/src/app/api/auth/oidc/callback/route.ts +82 -0
- package/src/app/api/auth/oidc/login/route.ts +43 -0
- package/src/app/api/connections/managed/route.ts +35 -0
- package/src/app/api/db/cancel/route.ts +42 -0
- package/src/app/api/db/disconnect/route.ts +28 -0
- package/src/app/api/db/health/route.ts +49 -0
- package/src/app/api/db/maintenance/route.ts +72 -0
- package/src/app/api/db/monitoring/route.ts +62 -0
- package/src/app/api/db/multi-query/route.ts +116 -0
- package/src/app/api/db/pool-stats/route.ts +37 -0
- package/src/app/api/db/profile/route.ts +144 -0
- package/src/app/api/db/provider-meta/route.ts +49 -0
- package/src/app/api/db/query/route.ts +50 -0
- package/src/app/api/db/schema/route.ts +47 -0
- package/src/app/api/db/schema-snapshot/route.ts +42 -0
- package/src/app/api/db/test-connection/route.ts +55 -0
- package/src/app/api/db/transaction/route.ts +111 -0
- package/src/app/api/storage/[collection]/route.ts +67 -0
- package/src/app/api/storage/config/route.ts +17 -0
- package/src/app/api/storage/migrate/route.ts +45 -0
- package/src/app/api/storage/route.ts +32 -0
- package/src/app/error.tsx +49 -0
- package/src/app/global-error.tsx +55 -0
- package/src/app/globals.css +146 -0
- package/src/app/icon.svg +42 -0
- package/src/app/layout.tsx +34 -0
- package/src/app/login/login-form.tsx +301 -0
- package/src/app/login/page.tsx +11 -0
- package/src/app/monitoring/page.tsx +8 -0
- package/src/app/not-found.tsx +29 -0
- package/src/app/page.tsx +5 -0
- package/src/components/AIAutopilotPanel.tsx +238 -0
- package/src/components/CodeGenerator.tsx +271 -0
- package/src/components/CommandPalette.tsx +227 -0
- package/src/components/ConnectionModal.tsx +759 -0
- package/src/components/CreateTableModal.tsx +281 -0
- package/src/components/DataCharts.tsx +962 -0
- package/src/components/DataImportModal.tsx +582 -0
- package/src/components/DataProfiler.tsx +335 -0
- package/src/components/DatabaseDocs.tsx +251 -0
- package/src/components/MaskingSettings.tsx +414 -0
- package/src/components/MobileNav.tsx +50 -0
- package/src/components/NL2SQLPanel.tsx +281 -0
- package/src/components/PivotTable.tsx +257 -0
- package/src/components/QueryEditor.tsx +760 -0
- package/src/components/QueryHistory.tsx +344 -0
- package/src/components/QuerySafetyDialog.tsx +290 -0
- package/src/components/ResultsGrid.tsx +644 -0
- package/src/components/SaveQueryModal.tsx +104 -0
- package/src/components/SavedQueries.tsx +128 -0
- package/src/components/SchemaDiagram.tsx +473 -0
- package/src/components/SchemaDiff.tsx +473 -0
- package/src/components/SnapshotTimeline.tsx +116 -0
- package/src/components/Studio.tsx +639 -0
- package/src/components/TestDataGenerator.tsx +261 -0
- package/src/components/VisualExplain.tsx +820 -0
- package/src/components/admin/AdminDashboard.tsx +163 -0
- package/src/components/admin/tabs/AuditTab.tsx +531 -0
- package/src/components/admin/tabs/MonitoringEmbed.tsx +11 -0
- package/src/components/admin/tabs/OperationsTab.tsx +646 -0
- package/src/components/admin/tabs/OverviewTab.tsx +1328 -0
- package/src/components/admin/tabs/SecurityTab.tsx +284 -0
- package/src/components/community-section.tsx +92 -0
- package/src/components/icons/db-icons.tsx +84 -0
- package/src/components/libredb-logo.tsx +61 -0
- package/src/components/monitoring/MonitoringDashboard.tsx +345 -0
- package/src/components/monitoring/tabs/MetricChart.tsx +82 -0
- package/src/components/monitoring/tabs/OverviewTab.tsx +263 -0
- package/src/components/monitoring/tabs/PerformanceTab.tsx +254 -0
- package/src/components/monitoring/tabs/PoolTab.tsx +174 -0
- package/src/components/monitoring/tabs/QueriesTab.tsx +287 -0
- package/src/components/monitoring/tabs/SessionsTab.tsx +316 -0
- package/src/components/monitoring/tabs/StorageTab.tsx +335 -0
- package/src/components/monitoring/tabs/TablesTab.tsx +300 -0
- package/src/components/results-grid/ResultCard.tsx +111 -0
- package/src/components/results-grid/RowDetailSheet.tsx +178 -0
- package/src/components/results-grid/StatsBar.tsx +201 -0
- package/src/components/results-grid/index.ts +1 -0
- package/src/components/results-grid/utils.ts +23 -0
- package/src/components/schema-explorer/ColumnList.tsx +53 -0
- package/src/components/schema-explorer/SchemaExplorer.tsx +182 -0
- package/src/components/schema-explorer/TableItem.tsx +210 -0
- package/src/components/schema-explorer/index.ts +1 -0
- package/src/components/sidebar/ConnectionItem.tsx +105 -0
- package/src/components/sidebar/ConnectionsList.tsx +62 -0
- package/src/components/sidebar/Sidebar.tsx +130 -0
- package/src/components/sidebar/index.ts +2 -0
- package/src/components/studio/BottomPanel.tsx +286 -0
- package/src/components/studio/QueryToolbar.tsx +180 -0
- package/src/components/studio/StudioDesktopHeader.tsx +114 -0
- package/src/components/studio/StudioMobileHeader.tsx +340 -0
- package/src/components/studio/StudioTabBar.tsx +82 -0
- package/src/components/studio/index.ts +5 -0
- package/src/components/ui/accordion.tsx +66 -0
- package/src/components/ui/alert-dialog.tsx +157 -0
- package/src/components/ui/alert.tsx +66 -0
- package/src/components/ui/aspect-ratio.tsx +11 -0
- package/src/components/ui/avatar.tsx +53 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/breadcrumb.tsx +109 -0
- package/src/components/ui/button-group.tsx +83 -0
- package/src/components/ui/button.tsx +60 -0
- package/src/components/ui/calendar.tsx +216 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/carousel.tsx +241 -0
- package/src/components/ui/chart.tsx +357 -0
- package/src/components/ui/checkbox.tsx +32 -0
- package/src/components/ui/collapsible.tsx +33 -0
- package/src/components/ui/command.tsx +184 -0
- package/src/components/ui/context-menu.tsx +252 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/drawer.tsx +135 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/empty.tsx +104 -0
- package/src/components/ui/field.tsx +248 -0
- package/src/components/ui/form.tsx +167 -0
- package/src/components/ui/hover-card.tsx +44 -0
- package/src/components/ui/input-group.tsx +170 -0
- package/src/components/ui/input-otp.tsx +77 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/item.tsx +193 -0
- package/src/components/ui/kbd.tsx +28 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/menubar.tsx +276 -0
- package/src/components/ui/navigation-menu.tsx +168 -0
- package/src/components/ui/pagination.tsx +127 -0
- package/src/components/ui/popover.tsx +48 -0
- package/src/components/ui/progress.tsx +31 -0
- package/src/components/ui/radio-group.tsx +45 -0
- package/src/components/ui/resizable.tsx +56 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +187 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +139 -0
- package/src/components/ui/sidebar.tsx +726 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +63 -0
- package/src/components/ui/sonner.tsx +40 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +116 -0
- package/src/components/ui/tabs.tsx +66 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/toggle-group.tsx +83 -0
- package/src/components/ui/toggle.tsx +47 -0
- package/src/components/ui/tooltip.tsx +61 -0
- package/src/exports/components.ts +15 -0
- package/src/exports/index.ts +4 -0
- package/src/exports/providers.ts +4 -0
- package/src/exports/types.ts +26 -0
- package/src/hooks/use-ai-chat.ts +182 -0
- package/src/hooks/use-all-connections.ts +66 -0
- package/src/hooks/use-api-call.ts +71 -0
- package/src/hooks/use-auth.ts +51 -0
- package/src/hooks/use-connection-form.ts +349 -0
- package/src/hooks/use-connection-manager.ts +169 -0
- package/src/hooks/use-connection-payload.ts +15 -0
- package/src/hooks/use-inline-editing.ts +109 -0
- package/src/hooks/use-mobile.ts +20 -0
- package/src/hooks/use-monitoring-data.ts +270 -0
- package/src/hooks/use-provider-metadata.ts +62 -0
- package/src/hooks/use-query-execution.ts +478 -0
- package/src/hooks/use-storage-sync.ts +259 -0
- package/src/hooks/use-tab-manager.ts +231 -0
- package/src/hooks/use-toast.ts +20 -0
- package/src/hooks/use-transaction-control.ts +64 -0
- package/src/lib/api/error-codes.ts +30 -0
- package/src/lib/api/errors.ts +236 -0
- package/src/lib/api/with-error-handler.ts +41 -0
- package/src/lib/audit.ts +105 -0
- package/src/lib/auth.ts +87 -0
- package/src/lib/connection-string-parser.ts +172 -0
- package/src/lib/data-masking.ts +385 -0
- package/src/lib/db/base-provider.ts +325 -0
- package/src/lib/db/errors.ts +317 -0
- package/src/lib/db/factory.ts +324 -0
- package/src/lib/db/index.ts +123 -0
- package/src/lib/db/providers/document/index.ts +6 -0
- package/src/lib/db/providers/document/mongodb.ts +992 -0
- package/src/lib/db/providers/keyvalue/redis.ts +554 -0
- package/src/lib/db/providers/sql/index.ts +11 -0
- package/src/lib/db/providers/sql/mssql.ts +1065 -0
- package/src/lib/db/providers/sql/mysql.ts +978 -0
- package/src/lib/db/providers/sql/oracle.ts +1044 -0
- package/src/lib/db/providers/sql/postgres.ts +1179 -0
- package/src/lib/db/providers/sql/sql-base.ts +174 -0
- package/src/lib/db/providers/sql/sqlite.ts +721 -0
- package/src/lib/db/types.ts +437 -0
- package/src/lib/db/utils/pool-manager.ts +287 -0
- package/src/lib/db/utils/query-limiter.ts +239 -0
- package/src/lib/db-ui-config.ts +86 -0
- package/src/lib/editor/mongodb-completions.ts +172 -0
- package/src/lib/editor/sql-completions.ts +280 -0
- package/src/lib/llm/base-provider.ts +117 -0
- package/src/lib/llm/factory.ts +102 -0
- package/src/lib/llm/index.ts +90 -0
- package/src/lib/llm/providers/custom.ts +181 -0
- package/src/lib/llm/providers/gemini.ts +126 -0
- package/src/lib/llm/providers/ollama.ts +154 -0
- package/src/lib/llm/providers/openai.ts +146 -0
- package/src/lib/llm/types.ts +173 -0
- package/src/lib/llm/utils/config.ts +187 -0
- package/src/lib/llm/utils/retry.ts +119 -0
- package/src/lib/llm/utils/streaming.ts +202 -0
- package/src/lib/logger.ts +127 -0
- package/src/lib/monitoring-thresholds.ts +44 -0
- package/src/lib/oidc.ts +262 -0
- package/src/lib/query-generators.ts +61 -0
- package/src/lib/schema-diff/diff-engine.ts +273 -0
- package/src/lib/schema-diff/migration-generator.ts +208 -0
- package/src/lib/schema-diff/types.ts +55 -0
- package/src/lib/seed/config-loader.ts +79 -0
- package/src/lib/seed/connection-filter.ts +49 -0
- package/src/lib/seed/credential-resolver.ts +62 -0
- package/src/lib/seed/index.ts +40 -0
- package/src/lib/seed/resolve-connection.ts +57 -0
- package/src/lib/seed/types.ts +69 -0
- package/src/lib/sql/alias-extractor.ts +267 -0
- package/src/lib/sql/index.ts +8 -0
- package/src/lib/sql/statement-splitter.ts +167 -0
- package/src/lib/sql/types.ts +40 -0
- package/src/lib/ssh/tunnel.ts +142 -0
- package/src/lib/storage/factory.ts +84 -0
- package/src/lib/storage/index.ts +14 -0
- package/src/lib/storage/local-storage.ts +99 -0
- package/src/lib/storage/providers/postgres.ts +225 -0
- package/src/lib/storage/providers/sqlite.ts +153 -0
- package/src/lib/storage/storage-facade.ts +272 -0
- package/src/lib/storage/types.ts +75 -0
- package/src/lib/time-series-buffer.ts +58 -0
- package/src/lib/types.ts +173 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +104 -0
- package/src/types/db-drivers.d.ts +23 -0
- package/src/types/html2canvas.d.ts +9 -0
- package/tests/api/admin/audit.test.ts +178 -0
- package/tests/api/admin/fleet-health.test.ts +183 -0
- package/tests/api/ai/autopilot.test.ts +174 -0
- package/tests/api/ai/chat.test.ts +250 -0
- package/tests/api/ai/describe-schema.test.ts +266 -0
- package/tests/api/ai/explain.test.ts +199 -0
- package/tests/api/ai/impact.test.ts +168 -0
- package/tests/api/ai/index-advisor.test.ts +171 -0
- package/tests/api/ai/nl2sql.test.ts +202 -0
- package/tests/api/ai/query-safety.test.ts +196 -0
- package/tests/api/auth/login.test.ts +170 -0
- package/tests/api/auth/logout.test.ts +140 -0
- package/tests/api/auth/me.test.ts +73 -0
- package/tests/api/auth/oidc-callback.test.ts +215 -0
- package/tests/api/auth/oidc-login.test.ts +127 -0
- package/tests/api/db/cancel.test.ts +198 -0
- package/tests/api/db/disconnect.test.ts +124 -0
- package/tests/api/db/health.test.ts +222 -0
- package/tests/api/db/maintenance.test.ts +263 -0
- package/tests/api/db/monitoring.test.ts +221 -0
- package/tests/api/db/multi-query.test.ts +316 -0
- package/tests/api/db/pool-stats.test.ts +135 -0
- package/tests/api/db/profile.test.ts +330 -0
- package/tests/api/db/provider-meta.test.ts +193 -0
- package/tests/api/db/query.test.ts +314 -0
- package/tests/api/db/schema-snapshot.test.ts +170 -0
- package/tests/api/db/schema.test.ts +191 -0
- package/tests/api/db/test-connection.test.ts +185 -0
- package/tests/api/db/transaction.test.ts +314 -0
- package/tests/api/proxy.test.ts +191 -0
- package/tests/api/seed/managed-route.test.ts +113 -0
- package/tests/api/storage/config.test.ts +42 -0
- package/tests/api/storage/storage-routes.test.ts +309 -0
- package/tests/components/AIAutopilotPanel.test.tsx +756 -0
- package/tests/components/AdminPage.test.tsx +33 -0
- package/tests/components/CodeGenerator.test.tsx +182 -0
- package/tests/components/CommandPalette.test.tsx +428 -0
- package/tests/components/CommunitySection.test.tsx +91 -0
- package/tests/components/ConnectionModal.mobile.test.tsx +284 -0
- package/tests/components/ConnectionModal.test.tsx +570 -0
- package/tests/components/CreateTableModal.test.tsx +383 -0
- package/tests/components/DataCharts.test.tsx +739 -0
- package/tests/components/DataImportModal.test.tsx +751 -0
- package/tests/components/DataProfiler.test.tsx +589 -0
- package/tests/components/DatabaseDocs.test.tsx +353 -0
- package/tests/components/LoginPage.test.tsx +163 -0
- package/tests/components/LoginPageOIDC.test.tsx +92 -0
- package/tests/components/MaskingSettings.test.tsx +498 -0
- package/tests/components/MobileNav.test.tsx +30 -0
- package/tests/components/MonitoringPage.test.tsx +32 -0
- package/tests/components/NL2SQLPanel.test.tsx +621 -0
- package/tests/components/Page.test.tsx +33 -0
- package/tests/components/PivotTable.test.tsx +350 -0
- package/tests/components/QueryEditor.test.tsx +1730 -0
- package/tests/components/QueryHistory.test.tsx +572 -0
- package/tests/components/QuerySafetyDialog.test.tsx +586 -0
- package/tests/components/ResultsGrid.test.tsx +804 -0
- package/tests/components/RootLayout.test.tsx +83 -0
- package/tests/components/SaveQueryModal.test.tsx +25 -0
- package/tests/components/SavedQueries.test.tsx +43 -0
- package/tests/components/SchemaDiagram.test.tsx +1034 -0
- package/tests/components/SchemaDiff.test.tsx +906 -0
- package/tests/components/SnapshotTimeline.test.tsx +174 -0
- package/tests/components/Studio.test.tsx +1030 -0
- package/tests/components/TestDataGenerator.test.tsx +291 -0
- package/tests/components/VisualExplain.test.tsx +704 -0
- package/tests/components/admin/AdminDashboard.test.tsx +205 -0
- package/tests/components/admin/AuditTab.test.tsx +220 -0
- package/tests/components/admin/MonitoringEmbed.test.tsx +58 -0
- package/tests/components/admin/OperationsTab.test.tsx +975 -0
- package/tests/components/admin/OverviewTab.test.tsx +254 -0
- package/tests/components/admin/SecurityTab.test.tsx +467 -0
- package/tests/components/monitoring/MetricChart.test.tsx +111 -0
- package/tests/components/monitoring/MonitoringDashboard.test.tsx +259 -0
- package/tests/components/monitoring/OverviewTab.test.tsx +78 -0
- package/tests/components/monitoring/PerformanceTab.test.tsx +87 -0
- package/tests/components/monitoring/PoolTab.test.tsx +42 -0
- package/tests/components/monitoring/QueriesTab.test.tsx +80 -0
- package/tests/components/monitoring/SessionsTab.test.tsx +154 -0
- package/tests/components/monitoring/StorageTab.test.tsx +127 -0
- package/tests/components/monitoring/TablesTab.test.tsx +153 -0
- package/tests/components/results-grid/ResultCard.test.tsx +105 -0
- package/tests/components/results-grid/RowDetailSheet.test.tsx +308 -0
- package/tests/components/results-grid/StatsBar.test.tsx +162 -0
- package/tests/components/schema-explorer/ColumnList.test.tsx +151 -0
- package/tests/components/schema-explorer/SchemaExplorer.test.tsx +461 -0
- package/tests/components/schema-explorer/TableItem.test.tsx +415 -0
- package/tests/components/sidebar/ConnectionItem.test.tsx +201 -0
- package/tests/components/sidebar/ConnectionsList.test.tsx +176 -0
- package/tests/components/sidebar/Sidebar.test.tsx +187 -0
- package/tests/components/studio/BottomPanel.test.tsx +383 -0
- package/tests/components/studio/QueryToolbar.test.tsx +321 -0
- package/tests/components/studio/StudioDesktopHeader.test.tsx +377 -0
- package/tests/components/studio/StudioMobileHeader.test.tsx +198 -0
- package/tests/components/studio/StudioTabBar.test.tsx +331 -0
- package/tests/fixtures/connections.ts +96 -0
- package/tests/fixtures/masking-configs.ts +86 -0
- package/tests/fixtures/query-results.ts +71 -0
- package/tests/fixtures/schemas.ts +64 -0
- package/tests/fixtures/seed-connections/invalid-config.yaml +7 -0
- package/tests/fixtures/seed-connections/minimal-config.yaml +8 -0
- package/tests/fixtures/seed-connections/mixed-credentials.yaml +23 -0
- package/tests/fixtures/seed-connections/multi-role-config.yaml +30 -0
- package/tests/fixtures/seed-connections/valid-config.json +15 -0
- package/tests/fixtures/seed-connections/valid-config.yaml +51 -0
- package/tests/helpers/mock-fetch.ts +59 -0
- package/tests/helpers/mock-monaco.ts +112 -0
- package/tests/helpers/mock-navigation.ts +28 -0
- package/tests/helpers/mock-next.ts +80 -0
- package/tests/helpers/mock-provider.ts +133 -0
- package/tests/helpers/mock-sonner.ts +29 -0
- package/tests/helpers/render-with-providers.tsx +19 -0
- package/tests/hooks/use-ai-chat.test.ts +600 -0
- package/tests/hooks/use-auth.test.ts +371 -0
- package/tests/hooks/use-connection-form.test.ts +743 -0
- package/tests/hooks/use-connection-manager.test.ts +466 -0
- package/tests/hooks/use-inline-editing.test.ts +321 -0
- package/tests/hooks/use-mobile.test.ts +177 -0
- package/tests/hooks/use-monitoring-data.test.ts +819 -0
- package/tests/hooks/use-provider-metadata.test.ts +228 -0
- package/tests/hooks/use-query-execution.test.ts +1212 -0
- package/tests/hooks/use-tab-manager.test.ts +756 -0
- package/tests/hooks/use-toast.test.ts +74 -0
- package/tests/hooks/use-transaction-control.test.ts +211 -0
- package/tests/integration/db/mongodb-provider.test.ts +698 -0
- package/tests/integration/db/mssql-provider.test.ts +840 -0
- package/tests/integration/db/mysql-provider.test.ts +872 -0
- package/tests/integration/db/oracle-provider.test.ts +843 -0
- package/tests/integration/db/postgres-provider.test.ts +1382 -0
- package/tests/integration/db/redis-provider.test.ts +526 -0
- package/tests/integration/db/sqlite-provider.test.ts +480 -0
- package/tests/integration/seed/seed-pipeline.test.ts +102 -0
- package/tests/isolated/factory-singleton.test.ts +150 -0
- package/tests/isolated/use-storage-sync.test.ts +389 -0
- package/tests/run-components.sh +196 -0
- package/tests/setup-dom.ts +58 -0
- package/tests/setup.ts +40 -0
- package/tests/unit/api-errors.test.ts +210 -0
- package/tests/unit/code-generator-functions.test.ts +271 -0
- package/tests/unit/components/column-list.test.tsx +190 -0
- package/tests/unit/components/data-import-modal.test.tsx +441 -0
- package/tests/unit/components/studio-mobile-header.test.tsx +327 -0
- package/tests/unit/data-charts-functions.test.ts +496 -0
- package/tests/unit/data-import-functions.test.ts +320 -0
- package/tests/unit/data-import-utils.test.ts +125 -0
- package/tests/unit/db/base-provider.test.ts +517 -0
- package/tests/unit/db/errors.test.ts +403 -0
- package/tests/unit/db/factory.test.ts +436 -0
- package/tests/unit/db/pool-manager.test.ts +440 -0
- package/tests/unit/db/query-limiter.test.ts +387 -0
- package/tests/unit/db/sql-base.test.ts +438 -0
- package/tests/unit/lib/api/error-codes.test.ts +39 -0
- package/tests/unit/lib/audit.test.ts +326 -0
- package/tests/unit/lib/auth.test.ts +146 -0
- package/tests/unit/lib/connection-string-parser.test.ts +424 -0
- package/tests/unit/lib/data-masking.test.ts +583 -0
- package/tests/unit/lib/db-icons.test.tsx +41 -0
- package/tests/unit/lib/monitoring-thresholds.test.ts +133 -0
- package/tests/unit/lib/oidc.test.ts +509 -0
- package/tests/unit/lib/query-generators.test.ts +127 -0
- package/tests/unit/lib/storage/factory.test.ts +71 -0
- package/tests/unit/lib/storage/local-storage.test.ts +114 -0
- package/tests/unit/lib/storage/providers/postgres.test.ts +312 -0
- package/tests/unit/lib/storage/providers/sqlite.test.ts +232 -0
- package/tests/unit/lib/storage/storage-facade-extended.test.ts +331 -0
- package/tests/unit/lib/storage/storage-facade.test.ts +184 -0
- package/tests/unit/lib/storage.test.ts +317 -0
- package/tests/unit/lib/time-series-buffer.test.ts +212 -0
- package/tests/unit/lib/utils.test.ts +24 -0
- package/tests/unit/llm/base-provider.test.ts +238 -0
- package/tests/unit/llm/config.test.ts +262 -0
- package/tests/unit/llm/custom-provider.test.ts +281 -0
- package/tests/unit/llm/gemini-provider.test.ts +248 -0
- package/tests/unit/llm/llm-factory.test.ts +155 -0
- package/tests/unit/llm/ollama-provider.test.ts +288 -0
- package/tests/unit/llm/openai-provider.test.ts +324 -0
- package/tests/unit/llm/retry.test.ts +180 -0
- package/tests/unit/llm/streaming.test.ts +355 -0
- package/tests/unit/logger.test.ts +198 -0
- package/tests/unit/mongodb-completions.test.ts +516 -0
- package/tests/unit/pivot-table-functions.test.ts +76 -0
- package/tests/unit/query-cancelled-error.test.ts +81 -0
- package/tests/unit/schema-diff/diff-engine.test.ts +367 -0
- package/tests/unit/schema-diff/migration-generator.test.ts +513 -0
- package/tests/unit/seed/config-loader.test.ts +73 -0
- package/tests/unit/seed/connection-filter.test.ts +91 -0
- package/tests/unit/seed/credential-resolver.test.ts +85 -0
- package/tests/unit/seed/index.test.ts +72 -0
- package/tests/unit/seed/resolve-connection.test.ts +74 -0
- package/tests/unit/seed/types.test.ts +129 -0
- package/tests/unit/sql/alias-extractor.test.ts +444 -0
- package/tests/unit/sql/statement-splitter.test.ts +348 -0
- package/tests/unit/sql-completions.test.ts +463 -0
- package/tests/unit/ssh-tunnel.test.ts +465 -0
- 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
|
+
}
|