@useatlas/create 0.0.6 → 0.0.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/LICENSE +21 -0
- package/README.md +1 -1
- package/index.ts +253 -36
- package/package.json +4 -4
- package/templates/docker/Dockerfile +1 -1
- package/templates/docker/Dockerfile.sidecar +1 -1
- package/templates/docker/bin/__tests__/duckdb-ingest.test.ts +17 -14
- package/templates/docker/bin/__tests__/failure-threshold.test.ts +148 -0
- package/templates/docker/bin/__tests__/fatal-error-propagation.test.ts +267 -0
- package/templates/docker/bin/__tests__/profiler-heuristics.test.ts +5 -5
- package/templates/docker/bin/__tests__/schema-drift.test.ts +39 -0
- package/templates/docker/bin/atlas.ts +981 -1819
- package/templates/docker/bin/benchmark.ts +14 -16
- package/templates/docker/bin/enrich.ts +7 -2
- package/templates/docker/brand.css +13 -0
- package/templates/docker/data/cybersec-semantic/catalog.yml +222 -0
- package/templates/docker/data/cybersec-semantic/entities/alerts.yml +195 -0
- package/templates/docker/data/cybersec-semantic/entities/assets.yml +191 -0
- package/templates/docker/data/cybersec-semantic/entities/compliance_assessments.yml +170 -0
- package/templates/docker/data/cybersec-semantic/entities/incidents.yml +219 -0
- package/templates/docker/data/cybersec-semantic/entities/organizations.yml +136 -0
- package/templates/docker/data/cybersec-semantic/entities/plans.yml +114 -0
- package/templates/docker/data/cybersec-semantic/entities/remediation_actions.yml +212 -0
- package/templates/docker/data/cybersec-semantic/entities/scan_results.yml +215 -0
- package/templates/docker/data/cybersec-semantic/entities/scans.yml +180 -0
- package/templates/docker/data/cybersec-semantic/entities/subscriptions.yml +184 -0
- package/templates/docker/data/cybersec-semantic/entities/users.yml +140 -0
- package/templates/docker/data/cybersec-semantic/entities/vulnerabilities.yml +154 -0
- package/templates/docker/data/cybersec-semantic/glossary.yml +207 -0
- package/templates/docker/data/cybersec-semantic/metrics/business.yml +148 -0
- package/templates/docker/data/cybersec-semantic/metrics/compliance.yml +138 -0
- package/templates/docker/data/cybersec-semantic/metrics/security.yml +181 -0
- package/templates/docker/data/cybersec.sql +8 -8
- package/templates/docker/data/demo.sql +3 -0
- package/templates/docker/data/ecommerce-semantic/catalog.yml +221 -0
- package/templates/docker/data/ecommerce-semantic/entities/categories.yml +91 -0
- package/templates/docker/data/ecommerce-semantic/entities/customers.yml +133 -0
- package/templates/docker/data/ecommerce-semantic/entities/email_campaigns.yml +119 -0
- package/templates/docker/data/ecommerce-semantic/entities/inventory_levels.yml +153 -0
- package/templates/docker/data/ecommerce-semantic/entities/order_items.yml +159 -0
- package/templates/docker/data/ecommerce-semantic/entities/orders.yml +199 -0
- package/templates/docker/data/ecommerce-semantic/entities/payments.yml +140 -0
- package/templates/docker/data/ecommerce-semantic/entities/product_reviews.yml +155 -0
- package/templates/docker/data/ecommerce-semantic/entities/products.yml +178 -0
- package/templates/docker/data/ecommerce-semantic/entities/promotions.yml +171 -0
- package/templates/docker/data/ecommerce-semantic/entities/returns.yml +144 -0
- package/templates/docker/data/ecommerce-semantic/entities/sellers.yml +124 -0
- package/templates/docker/data/ecommerce-semantic/entities/shipments.yml +159 -0
- package/templates/docker/data/ecommerce-semantic/glossary.yml +193 -0
- package/templates/docker/data/ecommerce-semantic/metrics/customers.yml +116 -0
- package/templates/docker/data/ecommerce-semantic/metrics/operations.yml +131 -0
- package/templates/docker/data/ecommerce-semantic/metrics/revenue.yml +120 -0
- package/templates/docker/docs/deploy.md +2 -1
- package/templates/docker/ee/src/__mocks__/internal.ts +170 -0
- package/templates/docker/ee/src/audit/purge-scheduler.ts +113 -0
- package/templates/docker/ee/src/audit/retention.ts +467 -0
- package/templates/docker/ee/src/auth/ip-allowlist.ts +367 -0
- package/templates/docker/ee/src/auth/roles.ts +562 -0
- package/templates/docker/ee/src/auth/scim.ts +343 -0
- package/templates/docker/ee/src/auth/sso.ts +538 -0
- package/templates/docker/ee/src/backups/engine.ts +355 -0
- package/templates/docker/ee/src/backups/index.ts +26 -0
- package/templates/docker/ee/src/backups/restore.ts +169 -0
- package/templates/docker/ee/src/backups/scheduler.ts +153 -0
- package/templates/docker/ee/src/backups/verify.ts +124 -0
- package/templates/docker/ee/src/branding/white-label.ts +228 -0
- package/templates/docker/ee/src/compliance/masking.ts +477 -0
- package/templates/docker/ee/src/compliance/patterns.ts +16 -0
- package/templates/docker/ee/src/compliance/pii-detection.ts +217 -0
- package/templates/docker/ee/src/compliance/reports.ts +402 -0
- package/templates/docker/ee/src/deploy-mode.ts +37 -0
- package/templates/docker/ee/src/governance/approval.ts +699 -0
- package/templates/docker/ee/src/index.ts +74 -0
- package/templates/docker/ee/src/platform/domains.ts +562 -0
- package/templates/docker/ee/src/platform/model-routing.ts +382 -0
- package/templates/docker/ee/src/platform/residency.ts +265 -0
- package/templates/docker/ee/src/sla/alerting.ts +382 -0
- package/templates/docker/ee/src/sla/index.ts +12 -0
- package/templates/docker/ee/src/sla/metrics.ts +275 -0
- package/templates/docker/ee/src/test-setup.ts +1 -0
- package/templates/docker/next.config.ts +4 -1
- package/templates/docker/package.json +49 -29
- package/templates/docker/sidecar/Dockerfile +1 -1
- package/templates/docker/src/api/index.ts +336 -24
- package/templates/docker/src/api/routes/actions.ts +443 -176
- package/templates/docker/src/api/routes/admin-abuse.ts +219 -0
- package/templates/docker/src/api/routes/admin-approval.ts +418 -0
- package/templates/docker/src/api/routes/admin-audit-retention.ts +405 -0
- package/templates/docker/src/api/routes/admin-auth.ts +122 -0
- package/templates/docker/src/api/routes/admin-branding.ts +252 -0
- package/templates/docker/src/api/routes/admin-compliance.ts +352 -0
- package/templates/docker/src/api/routes/admin-domains.ts +334 -0
- package/templates/docker/src/api/routes/admin-integrations.ts +2667 -0
- package/templates/docker/src/api/routes/admin-ip-allowlist.ts +261 -0
- package/templates/docker/src/api/routes/admin-learned-patterns.ts +525 -0
- package/templates/docker/src/api/routes/admin-model-config.ts +252 -0
- package/templates/docker/src/api/routes/admin-onboarding-emails.ts +145 -0
- package/templates/docker/src/api/routes/admin-orgs.ts +710 -0
- package/templates/docker/src/api/routes/admin-prompts.ts +694 -0
- package/templates/docker/src/api/routes/admin-residency.ts +570 -0
- package/templates/docker/src/api/routes/admin-roles.ts +296 -0
- package/templates/docker/src/api/routes/admin-router.ts +120 -0
- package/templates/docker/src/api/routes/admin-sandbox.ts +417 -0
- package/templates/docker/src/api/routes/admin-scim.ts +262 -0
- package/templates/docker/src/api/routes/admin-sso.ts +545 -0
- package/templates/docker/src/api/routes/admin-suggestions.ts +176 -0
- package/templates/docker/src/api/routes/admin-usage.ts +310 -0
- package/templates/docker/src/api/routes/admin.ts +4156 -898
- package/templates/docker/src/api/routes/auth-preamble.ts +105 -0
- package/templates/docker/src/api/routes/billing.ts +397 -0
- package/templates/docker/src/api/routes/chat.ts +597 -334
- package/templates/docker/src/api/routes/conversations.ts +987 -132
- package/templates/docker/src/api/routes/demo.ts +673 -0
- package/templates/docker/src/api/routes/discord.ts +274 -0
- package/templates/docker/src/api/routes/ee-error-handler.ts +32 -0
- package/templates/docker/src/api/routes/health.ts +129 -14
- package/templates/docker/src/api/routes/middleware.ts +244 -0
- package/templates/docker/src/api/routes/onboarding-emails.ts +134 -0
- package/templates/docker/src/api/routes/onboarding.ts +1109 -0
- package/templates/docker/src/api/routes/openapi.ts +184 -1597
- package/templates/docker/src/api/routes/platform-admin.ts +760 -0
- package/templates/docker/src/api/routes/platform-backups.ts +436 -0
- package/templates/docker/src/api/routes/platform-domains.ts +235 -0
- package/templates/docker/src/api/routes/platform-residency.ts +257 -0
- package/templates/docker/src/api/routes/platform-sla.ts +379 -0
- package/templates/docker/src/api/routes/prompts.ts +221 -0
- package/templates/docker/src/api/routes/public-branding.ts +106 -0
- package/templates/docker/src/api/routes/query.ts +330 -219
- package/templates/docker/src/api/routes/scheduled-tasks.ts +393 -297
- package/templates/docker/src/api/routes/semantic.ts +179 -0
- package/templates/docker/src/api/routes/sessions.ts +210 -0
- package/templates/docker/src/api/routes/shared-domains.ts +98 -0
- package/templates/docker/src/api/routes/shared-schemas.ts +139 -0
- package/templates/docker/src/api/routes/slack.ts +209 -52
- package/templates/docker/src/api/routes/suggestions.ts +233 -0
- package/templates/docker/src/api/routes/tables.ts +67 -0
- package/templates/docker/src/api/routes/teams.ts +222 -0
- package/templates/docker/src/api/routes/validate-sql.ts +188 -0
- package/templates/docker/src/api/routes/validation-hook.ts +62 -0
- package/templates/docker/src/api/routes/widget-loader.ts +356 -0
- package/templates/docker/src/api/routes/widget.ts +428 -0
- package/templates/docker/src/api/routes/wizard.ts +852 -0
- package/templates/docker/src/api/server.ts +187 -69
- package/templates/docker/src/app/error.tsx +5 -2
- package/templates/docker/src/app/globals.css +1 -1
- package/templates/docker/src/app/layout.tsx +7 -2
- package/templates/docker/src/app/page.tsx +39 -5
- package/templates/docker/src/components/data-table/data-table-column-header.tsx +99 -0
- package/templates/docker/src/components/data-table/data-table-date-filter.tsx +225 -0
- package/templates/docker/src/components/data-table/data-table-expandable.tsx +125 -0
- package/templates/docker/src/components/data-table/data-table-faceted-filter.tsx +189 -0
- package/templates/docker/src/components/data-table/data-table-pagination.tsx +112 -0
- package/templates/docker/src/components/data-table/data-table-range-filter.tsx +122 -0
- package/templates/docker/src/components/data-table/data-table-slider-filter.tsx +256 -0
- package/templates/docker/src/components/data-table/data-table-sort-list.tsx +407 -0
- package/templates/docker/src/components/data-table/data-table-toolbar.tsx +149 -0
- package/templates/docker/src/components/data-table/data-table-view-options.tsx +89 -0
- package/templates/docker/src/components/data-table/data-table.tsx +105 -0
- package/templates/docker/src/components/form-dialog.tsx +135 -0
- package/templates/docker/src/components/ui/accordion.tsx +66 -0
- package/templates/docker/src/components/ui/calendar.tsx +220 -0
- package/templates/docker/src/components/ui/checkbox.tsx +32 -0
- package/templates/docker/src/components/ui/faceted.tsx +283 -0
- package/templates/docker/src/components/ui/form.tsx +167 -0
- package/templates/docker/src/components/ui/label.tsx +24 -0
- package/templates/docker/src/components/ui/popover.tsx +89 -0
- package/templates/docker/src/components/ui/progress.tsx +31 -0
- package/templates/docker/src/components/ui/scroll-area.tsx +6 -2
- package/templates/docker/src/components/ui/slider.tsx +63 -0
- package/templates/docker/src/components/ui/sortable.tsx +581 -0
- package/templates/docker/src/components/ui/switch.tsx +35 -0
- package/templates/docker/src/components/ui/textarea.tsx +18 -0
- package/templates/docker/src/config/data-table.ts +82 -0
- package/templates/docker/src/env-check.ts +74 -0
- package/templates/docker/src/hooks/use-callback-ref.ts +27 -0
- package/templates/docker/src/hooks/use-data-table.ts +316 -0
- package/templates/docker/src/hooks/use-debounced-callback.ts +28 -0
- package/templates/docker/src/lib/action-types.ts +7 -41
- package/templates/docker/src/lib/agent-query.ts +4 -2
- package/templates/docker/src/lib/agent.ts +363 -31
- package/templates/docker/src/lib/auth/admin-permissions.ts +38 -0
- package/templates/docker/src/lib/auth/audit.ts +19 -4
- package/templates/docker/src/lib/auth/byot.ts +3 -3
- package/templates/docker/src/lib/auth/client.ts +33 -3
- package/templates/docker/src/lib/auth/detect.ts +29 -8
- package/templates/docker/src/lib/auth/managed.ts +104 -14
- package/templates/docker/src/lib/auth/middleware.ts +53 -6
- package/templates/docker/src/lib/auth/migrate.ts +140 -15
- package/templates/docker/src/lib/auth/oauth-state.ts +123 -0
- package/templates/docker/src/lib/auth/org-permissions.ts +55 -0
- package/templates/docker/src/lib/auth/permissions.ts +26 -19
- package/templates/docker/src/lib/auth/server.ts +355 -9
- package/templates/docker/src/lib/auth/simple-key.ts +3 -3
- package/templates/docker/src/lib/auth/types.ts +15 -21
- package/templates/docker/src/lib/billing/enforcement.ts +368 -0
- package/templates/docker/src/lib/billing/plans.ts +155 -0
- package/templates/docker/src/lib/cache/index.ts +92 -0
- package/templates/docker/src/lib/cache/keys.ts +30 -0
- package/templates/docker/src/lib/cache/lru.ts +79 -0
- package/templates/docker/src/lib/cache/types.ts +31 -0
- package/templates/docker/src/lib/compose-refs.ts +62 -0
- package/templates/docker/src/lib/config.ts +563 -11
- package/templates/docker/src/lib/connection-types.ts +9 -0
- package/templates/docker/src/lib/conversation-types.ts +1 -25
- package/templates/docker/src/lib/conversations.ts +345 -14
- package/templates/docker/src/lib/data-table.ts +61 -0
- package/templates/docker/src/lib/db/connection.ts +793 -39
- package/templates/docker/src/lib/db/internal.ts +985 -139
- package/templates/docker/src/lib/db/migrate.ts +295 -0
- package/templates/docker/src/lib/db/migrations/0000_baseline.sql +703 -0
- package/templates/docker/src/lib/db/migrations/0001_teams_installations.sql +14 -0
- package/templates/docker/src/lib/db/migrations/0002_discord_installations.sql +14 -0
- package/templates/docker/src/lib/db/migrations/0003_telegram_installations.sql +15 -0
- package/templates/docker/src/lib/db/migrations/0004_sandbox_credentials.sql +18 -0
- package/templates/docker/src/lib/db/migrations/0005_oauth_state.sql +16 -0
- package/templates/docker/src/lib/db/migrations/0006_byot_credentials.sql +14 -0
- package/templates/docker/src/lib/db/migrations/0007_gchat_installations.sql +15 -0
- package/templates/docker/src/lib/db/migrations/0008_github_installations.sql +14 -0
- package/templates/docker/src/lib/db/migrations/0009_linear_installations.sql +15 -0
- package/templates/docker/src/lib/db/migrations/0010_whatsapp_installations.sql +14 -0
- package/templates/docker/src/lib/db/migrations/0011_email_installations.sql +16 -0
- package/templates/docker/src/lib/db/migrations/0012_region_migrations.sql +25 -0
- package/templates/docker/src/lib/db/schema.ts +1120 -0
- package/templates/docker/src/lib/db/source-rate-limit.ts +89 -139
- package/templates/docker/src/lib/demo.ts +308 -0
- package/templates/docker/src/lib/discord/store.ts +225 -0
- package/templates/docker/src/lib/effect/ai.ts +243 -0
- package/templates/docker/src/lib/effect/errors.ts +234 -0
- package/templates/docker/src/lib/effect/hono.ts +454 -0
- package/templates/docker/src/lib/effect/index.ts +137 -0
- package/templates/docker/src/lib/effect/layers.ts +496 -0
- package/templates/docker/src/lib/effect/services.ts +776 -0
- package/templates/docker/src/lib/effect/sql.ts +178 -0
- package/templates/docker/src/lib/effect/toolkit.ts +123 -0
- package/templates/docker/src/lib/email/delivery.ts +232 -0
- package/templates/docker/src/lib/email/engine.ts +349 -0
- package/templates/docker/src/lib/email/hooks.ts +107 -0
- package/templates/docker/src/lib/email/index.ts +16 -0
- package/templates/docker/src/lib/email/scheduler.ts +72 -0
- package/templates/docker/src/lib/email/sequence.ts +73 -0
- package/templates/docker/src/lib/email/store.ts +163 -0
- package/templates/docker/src/lib/email/templates.ts +215 -0
- package/templates/docker/src/lib/format.ts +67 -0
- package/templates/docker/src/lib/gchat/store.ts +202 -0
- package/templates/docker/src/lib/github/store.ts +197 -0
- package/templates/docker/src/lib/id.ts +29 -0
- package/templates/docker/src/lib/integrations/types.ts +166 -0
- package/templates/docker/src/lib/learn/pattern-analyzer.ts +224 -0
- package/templates/docker/src/lib/learn/pattern-cache.ts +229 -0
- package/templates/docker/src/lib/learn/pattern-proposer.ts +87 -0
- package/templates/docker/src/lib/learn/suggestion-helpers.ts +34 -0
- package/templates/docker/src/lib/learn/suggestions.ts +139 -0
- package/templates/docker/src/lib/linear/store.ts +200 -0
- package/templates/docker/src/lib/logger.ts +35 -3
- package/templates/docker/src/lib/metering.ts +272 -0
- package/templates/docker/src/lib/parsers.ts +99 -0
- package/templates/docker/src/lib/plugins/hooks.ts +13 -11
- package/templates/docker/src/lib/plugins/index.ts +3 -1
- package/templates/docker/src/lib/plugins/registry.ts +58 -6
- package/templates/docker/src/lib/plugins/settings.ts +147 -0
- package/templates/docker/src/lib/plugins/wiring.ts +6 -9
- package/templates/docker/src/lib/profiler.ts +1665 -0
- package/templates/docker/src/lib/providers.ts +188 -13
- package/templates/docker/src/lib/rls.ts +172 -60
- package/templates/docker/src/lib/sandbox/credentials.ts +206 -0
- package/templates/docker/src/lib/sandbox/validate.ts +179 -0
- package/templates/docker/src/lib/scheduled-task-types.ts +26 -94
- package/templates/docker/src/lib/scheduled-tasks.ts +174 -34
- package/templates/docker/src/lib/scheduler/delivery.ts +248 -150
- package/templates/docker/src/lib/scheduler/engine.ts +190 -154
- package/templates/docker/src/lib/scheduler/executor.ts +74 -23
- package/templates/docker/src/lib/scheduler/preview.ts +72 -0
- package/templates/docker/src/lib/security/abuse.ts +463 -0
- package/templates/docker/src/lib/semantic/diff.ts +267 -0
- package/templates/docker/src/lib/semantic/entities.ts +167 -0
- package/templates/docker/src/lib/semantic/files.ts +283 -0
- package/templates/docker/src/lib/semantic/index.ts +27 -0
- package/templates/docker/src/lib/{semantic-index.ts → semantic/search.ts} +80 -9
- package/templates/docker/src/lib/semantic/sync.ts +581 -0
- package/templates/docker/src/lib/{semantic.ts → semantic/whitelist.ts} +189 -3
- package/templates/docker/src/lib/settings.ts +817 -0
- package/templates/docker/src/lib/sidecar-types.ts +13 -0
- package/templates/docker/src/lib/slack/store.ts +134 -25
- package/templates/docker/src/lib/startup.ts +528 -362
- package/templates/docker/src/lib/teams/store.ts +216 -0
- package/templates/docker/src/lib/telegram/store.ts +202 -0
- package/templates/docker/src/lib/telemetry.ts +40 -0
- package/templates/docker/src/lib/tools/actions/audit.ts +8 -5
- package/templates/docker/src/lib/tools/actions/email.ts +3 -1
- package/templates/docker/src/lib/tools/actions/handler.ts +276 -93
- package/templates/docker/src/lib/tools/actions/jira.ts +2 -2
- package/templates/docker/src/lib/tools/backends/detect.ts +16 -0
- package/templates/docker/src/lib/tools/backends/index.ts +11 -0
- package/templates/docker/src/lib/tools/backends/nsjail.ts +213 -0
- package/templates/docker/src/lib/tools/backends/shared.ts +103 -0
- package/templates/docker/src/lib/tools/backends/types.ts +26 -0
- package/templates/docker/src/lib/tools/explore-nsjail.ts +7 -228
- package/templates/docker/src/lib/tools/explore-sandbox.ts +4 -29
- package/templates/docker/src/lib/tools/explore-sidecar.ts +18 -2
- package/templates/docker/src/lib/tools/explore.ts +246 -54
- package/templates/docker/src/lib/tools/index.ts +17 -0
- package/templates/docker/src/lib/tools/python-nsjail.ts +11 -139
- package/templates/docker/src/lib/tools/python-sandbox.ts +9 -132
- package/templates/docker/src/lib/tools/python-sidecar.ts +184 -3
- package/templates/docker/src/lib/tools/python-stream.ts +33 -0
- package/templates/docker/src/lib/tools/python-wrapper.ts +129 -0
- package/templates/docker/src/lib/tools/python.ts +115 -15
- package/templates/docker/src/lib/tools/registry.ts +14 -2
- package/templates/docker/src/lib/tools/sql.ts +778 -362
- package/templates/docker/src/lib/tracing.ts +16 -0
- package/templates/docker/src/lib/whatsapp/store.ts +198 -0
- package/templates/docker/src/lib/workspace.ts +89 -0
- package/templates/docker/src/progress.ts +121 -0
- package/templates/docker/src/types/data-table.ts +48 -0
- package/templates/docker/src/ui/atlas-chat-reexport.ts +3 -0
- package/templates/docker/src/ui/components/actions/action-approval-card.tsx +26 -19
- package/templates/docker/src/ui/components/actions/action-status-badge.tsx +3 -3
- package/templates/docker/src/ui/components/admin/admin-layout.tsx +57 -39
- package/templates/docker/src/ui/components/admin/admin-sidebar.tsx +213 -35
- package/templates/docker/src/ui/components/admin/delivery-status-badge.tsx +53 -0
- package/templates/docker/src/ui/components/admin/empty-state.tsx +27 -6
- package/templates/docker/src/ui/components/admin/entity-detail.tsx +3 -52
- package/templates/docker/src/ui/components/admin/error-banner.tsx +2 -2
- package/templates/docker/src/ui/components/admin/feature-disabled.tsx +28 -5
- package/templates/docker/src/ui/components/admin-content-wrapper.tsx +87 -0
- package/templates/docker/src/ui/components/atlas-chat.tsx +449 -166
- package/templates/docker/src/ui/components/branding-head.tsx +41 -0
- package/templates/docker/src/ui/components/chart/chart-detection.ts +62 -5
- package/templates/docker/src/ui/components/chart/result-chart.tsx +316 -125
- package/templates/docker/src/ui/components/chat/api-key-bar.tsx +4 -4
- package/templates/docker/src/ui/components/chat/data-table.tsx +45 -4
- package/templates/docker/src/ui/components/chat/error-banner.tsx +86 -5
- package/templates/docker/src/ui/components/chat/follow-up-chips.tsx +29 -0
- package/templates/docker/src/ui/components/chat/markdown.tsx +24 -0
- package/templates/docker/src/ui/components/chat/prompt-library.tsx +206 -0
- package/templates/docker/src/ui/components/chat/python-result-card.tsx +106 -78
- package/templates/docker/src/ui/components/chat/result-card-base.tsx +101 -0
- package/templates/docker/src/ui/components/chat/share-dialog.tsx +377 -0
- package/templates/docker/src/ui/components/chat/sql-result-card.tsx +94 -73
- package/templates/docker/src/ui/components/chat/suggestion-chips.tsx +46 -0
- package/templates/docker/src/ui/components/chat/tool-part.tsx +16 -4
- package/templates/docker/src/ui/components/conversations/conversation-item.tsx +48 -17
- package/templates/docker/src/ui/components/conversations/conversation-list.tsx +38 -24
- package/templates/docker/src/ui/components/conversations/conversation-sidebar.tsx +66 -7
- package/templates/docker/src/ui/components/conversations/delete-confirmation.tsx +9 -2
- package/templates/docker/src/ui/components/error-boundary.tsx +66 -0
- package/templates/docker/src/ui/components/notebook/delete-cell-dialog.tsx +48 -0
- package/templates/docker/src/ui/components/notebook/fork-branch-selector.tsx +68 -0
- package/templates/docker/src/ui/components/notebook/notebook-cell-input.tsx +76 -0
- package/templates/docker/src/ui/components/notebook/notebook-cell-output.tsx +58 -0
- package/templates/docker/src/ui/components/notebook/notebook-cell-toolbar.tsx +91 -0
- package/templates/docker/src/ui/components/notebook/notebook-cell.tsx +119 -0
- package/templates/docker/src/ui/components/notebook/notebook-empty-state.tsx +19 -0
- package/templates/docker/src/ui/components/notebook/notebook-export.ts +287 -0
- package/templates/docker/src/ui/components/notebook/notebook-input-bar.tsx +49 -0
- package/templates/docker/src/ui/components/notebook/notebook-shell.tsx +266 -0
- package/templates/docker/src/ui/components/notebook/notebook-text-cell.tsx +152 -0
- package/templates/docker/src/ui/components/notebook/types.ts +39 -0
- package/templates/docker/src/ui/components/notebook/use-keyboard-nav.ts +109 -0
- package/templates/docker/src/ui/components/notebook/use-notebook.ts +684 -0
- package/templates/docker/src/ui/components/org-switcher.tsx +111 -0
- package/templates/docker/src/ui/components/region-picker.tsx +103 -0
- package/templates/docker/src/ui/components/schema-explorer/schema-explorer.tsx +522 -0
- package/templates/docker/src/ui/components/social-icons.tsx +26 -0
- package/templates/docker/src/ui/components/tour/guided-tour.tsx +81 -0
- package/templates/docker/src/ui/components/tour/index.ts +5 -0
- package/templates/docker/src/ui/components/tour/nav-bar.tsx +100 -0
- package/templates/docker/src/ui/components/tour/tour-overlay.tsx +298 -0
- package/templates/docker/src/ui/components/tour/tour-steps.ts +43 -0
- package/templates/docker/src/ui/components/tour/types.ts +21 -0
- package/templates/docker/src/ui/components/tour/use-tour.ts +193 -0
- package/templates/docker/src/ui/context-reexport.ts +3 -0
- package/templates/docker/src/ui/hooks/theme-init-script.ts +17 -0
- package/templates/docker/src/ui/hooks/use-admin-fetch.ts +38 -30
- package/templates/docker/src/ui/hooks/use-admin-mutation.ts +188 -0
- package/templates/docker/src/ui/hooks/use-atlas-transport.ts +225 -0
- package/templates/docker/src/ui/hooks/use-branding.ts +68 -0
- package/templates/docker/src/ui/hooks/use-conversations.ts +106 -83
- package/templates/docker/src/ui/hooks/use-dark-mode.ts +134 -10
- package/templates/docker/src/ui/hooks/use-deploy-mode.ts +36 -0
- package/templates/docker/src/ui/hooks/use-platform-admin-guard.ts +49 -0
- package/templates/docker/src/ui/lib/action-types.ts +11 -63
- package/templates/docker/src/ui/lib/admin-schemas.ts +744 -0
- package/templates/docker/src/ui/lib/fetch-client.ts +84 -0
- package/templates/docker/src/ui/lib/fetch-error.ts +54 -0
- package/templates/docker/src/ui/lib/helpers.ts +94 -1
- package/templates/docker/src/ui/lib/types.ts +149 -140
- package/templates/docker/tsconfig.json +4 -2
- package/templates/nextjs-standalone/bin/__tests__/duckdb-ingest.test.ts +17 -14
- package/templates/nextjs-standalone/bin/__tests__/failure-threshold.test.ts +148 -0
- package/templates/nextjs-standalone/bin/__tests__/fatal-error-propagation.test.ts +267 -0
- package/templates/nextjs-standalone/bin/__tests__/profiler-heuristics.test.ts +5 -5
- package/templates/nextjs-standalone/bin/__tests__/schema-drift.test.ts +39 -0
- package/templates/nextjs-standalone/bin/atlas.ts +981 -1819
- package/templates/nextjs-standalone/bin/benchmark.ts +14 -16
- package/templates/nextjs-standalone/bin/enrich.ts +7 -2
- package/templates/nextjs-standalone/brand.css +13 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/catalog.yml +222 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/alerts.yml +195 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/assets.yml +191 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/compliance_assessments.yml +170 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/incidents.yml +219 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/organizations.yml +136 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/plans.yml +114 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/remediation_actions.yml +212 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/scan_results.yml +215 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/scans.yml +180 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/subscriptions.yml +184 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/users.yml +140 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/entities/vulnerabilities.yml +154 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/glossary.yml +207 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/metrics/business.yml +148 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/metrics/compliance.yml +138 -0
- package/templates/nextjs-standalone/data/cybersec-semantic/metrics/security.yml +181 -0
- package/templates/nextjs-standalone/data/cybersec.sql +8 -8
- package/templates/nextjs-standalone/data/demo.sql +3 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/catalog.yml +221 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/categories.yml +91 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/customers.yml +133 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/email_campaigns.yml +119 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/inventory_levels.yml +153 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/order_items.yml +159 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/orders.yml +199 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/payments.yml +140 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/product_reviews.yml +155 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/products.yml +178 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/promotions.yml +171 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/returns.yml +144 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/sellers.yml +124 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/entities/shipments.yml +159 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/glossary.yml +193 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/metrics/customers.yml +116 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/metrics/operations.yml +131 -0
- package/templates/nextjs-standalone/data/ecommerce-semantic/metrics/revenue.yml +120 -0
- package/templates/nextjs-standalone/docs/deploy.md +2 -1
- package/templates/nextjs-standalone/ee/src/__mocks__/internal.ts +170 -0
- package/templates/nextjs-standalone/ee/src/audit/purge-scheduler.ts +113 -0
- package/templates/nextjs-standalone/ee/src/audit/retention.ts +467 -0
- package/templates/nextjs-standalone/ee/src/auth/ip-allowlist.ts +367 -0
- package/templates/nextjs-standalone/ee/src/auth/roles.ts +562 -0
- package/templates/nextjs-standalone/ee/src/auth/scim.ts +343 -0
- package/templates/nextjs-standalone/ee/src/auth/sso.ts +538 -0
- package/templates/nextjs-standalone/ee/src/backups/engine.ts +355 -0
- package/templates/nextjs-standalone/ee/src/backups/index.ts +26 -0
- package/templates/nextjs-standalone/ee/src/backups/restore.ts +169 -0
- package/templates/nextjs-standalone/ee/src/backups/scheduler.ts +153 -0
- package/templates/nextjs-standalone/ee/src/backups/verify.ts +124 -0
- package/templates/nextjs-standalone/ee/src/branding/white-label.ts +228 -0
- package/templates/nextjs-standalone/ee/src/compliance/masking.ts +477 -0
- package/templates/nextjs-standalone/ee/src/compliance/patterns.ts +16 -0
- package/templates/nextjs-standalone/ee/src/compliance/pii-detection.ts +217 -0
- package/templates/nextjs-standalone/ee/src/compliance/reports.ts +402 -0
- package/templates/nextjs-standalone/ee/src/deploy-mode.ts +37 -0
- package/templates/nextjs-standalone/ee/src/governance/approval.ts +699 -0
- package/templates/nextjs-standalone/ee/src/index.ts +74 -0
- package/templates/nextjs-standalone/ee/src/platform/domains.ts +562 -0
- package/templates/nextjs-standalone/ee/src/platform/model-routing.ts +382 -0
- package/templates/nextjs-standalone/ee/src/platform/residency.ts +265 -0
- package/templates/nextjs-standalone/ee/src/sla/alerting.ts +382 -0
- package/templates/nextjs-standalone/ee/src/sla/index.ts +12 -0
- package/templates/nextjs-standalone/ee/src/sla/metrics.ts +275 -0
- package/templates/nextjs-standalone/ee/src/test-setup.ts +1 -0
- package/templates/nextjs-standalone/next.config.ts +1 -1
- package/templates/nextjs-standalone/package.json +50 -30
- package/templates/nextjs-standalone/src/api/index.ts +336 -24
- package/templates/nextjs-standalone/src/api/routes/actions.ts +443 -176
- package/templates/nextjs-standalone/src/api/routes/admin-abuse.ts +219 -0
- package/templates/nextjs-standalone/src/api/routes/admin-approval.ts +418 -0
- package/templates/nextjs-standalone/src/api/routes/admin-audit-retention.ts +405 -0
- package/templates/nextjs-standalone/src/api/routes/admin-auth.ts +122 -0
- package/templates/nextjs-standalone/src/api/routes/admin-branding.ts +252 -0
- package/templates/nextjs-standalone/src/api/routes/admin-compliance.ts +352 -0
- package/templates/nextjs-standalone/src/api/routes/admin-domains.ts +334 -0
- package/templates/nextjs-standalone/src/api/routes/admin-integrations.ts +2667 -0
- package/templates/nextjs-standalone/src/api/routes/admin-ip-allowlist.ts +261 -0
- package/templates/nextjs-standalone/src/api/routes/admin-learned-patterns.ts +525 -0
- package/templates/nextjs-standalone/src/api/routes/admin-model-config.ts +252 -0
- package/templates/nextjs-standalone/src/api/routes/admin-onboarding-emails.ts +145 -0
- package/templates/nextjs-standalone/src/api/routes/admin-orgs.ts +710 -0
- package/templates/nextjs-standalone/src/api/routes/admin-prompts.ts +694 -0
- package/templates/nextjs-standalone/src/api/routes/admin-residency.ts +570 -0
- package/templates/nextjs-standalone/src/api/routes/admin-roles.ts +296 -0
- package/templates/nextjs-standalone/src/api/routes/admin-router.ts +120 -0
- package/templates/nextjs-standalone/src/api/routes/admin-sandbox.ts +417 -0
- package/templates/nextjs-standalone/src/api/routes/admin-scim.ts +262 -0
- package/templates/nextjs-standalone/src/api/routes/admin-sso.ts +545 -0
- package/templates/nextjs-standalone/src/api/routes/admin-suggestions.ts +176 -0
- package/templates/nextjs-standalone/src/api/routes/admin-usage.ts +310 -0
- package/templates/nextjs-standalone/src/api/routes/admin.ts +4156 -898
- package/templates/nextjs-standalone/src/api/routes/auth-preamble.ts +105 -0
- package/templates/nextjs-standalone/src/api/routes/billing.ts +397 -0
- package/templates/nextjs-standalone/src/api/routes/chat.ts +597 -334
- package/templates/nextjs-standalone/src/api/routes/conversations.ts +987 -132
- package/templates/nextjs-standalone/src/api/routes/demo.ts +673 -0
- package/templates/nextjs-standalone/src/api/routes/discord.ts +274 -0
- package/templates/nextjs-standalone/src/api/routes/ee-error-handler.ts +32 -0
- package/templates/nextjs-standalone/src/api/routes/health.ts +129 -14
- package/templates/nextjs-standalone/src/api/routes/middleware.ts +244 -0
- package/templates/nextjs-standalone/src/api/routes/onboarding-emails.ts +134 -0
- package/templates/nextjs-standalone/src/api/routes/onboarding.ts +1109 -0
- package/templates/nextjs-standalone/src/api/routes/openapi.ts +184 -1597
- package/templates/nextjs-standalone/src/api/routes/platform-admin.ts +760 -0
- package/templates/nextjs-standalone/src/api/routes/platform-backups.ts +436 -0
- package/templates/nextjs-standalone/src/api/routes/platform-domains.ts +235 -0
- package/templates/nextjs-standalone/src/api/routes/platform-residency.ts +257 -0
- package/templates/nextjs-standalone/src/api/routes/platform-sla.ts +379 -0
- package/templates/nextjs-standalone/src/api/routes/prompts.ts +221 -0
- package/templates/nextjs-standalone/src/api/routes/public-branding.ts +106 -0
- package/templates/nextjs-standalone/src/api/routes/query.ts +330 -219
- package/templates/nextjs-standalone/src/api/routes/scheduled-tasks.ts +393 -297
- package/templates/nextjs-standalone/src/api/routes/semantic.ts +179 -0
- package/templates/nextjs-standalone/src/api/routes/sessions.ts +210 -0
- package/templates/nextjs-standalone/src/api/routes/shared-domains.ts +98 -0
- package/templates/nextjs-standalone/src/api/routes/shared-schemas.ts +139 -0
- package/templates/nextjs-standalone/src/api/routes/slack.ts +209 -52
- package/templates/nextjs-standalone/src/api/routes/suggestions.ts +233 -0
- package/templates/nextjs-standalone/src/api/routes/tables.ts +67 -0
- package/templates/nextjs-standalone/src/api/routes/teams.ts +222 -0
- package/templates/nextjs-standalone/src/api/routes/validate-sql.ts +188 -0
- package/templates/nextjs-standalone/src/api/routes/validation-hook.ts +62 -0
- package/templates/nextjs-standalone/src/api/routes/widget-loader.ts +356 -0
- package/templates/nextjs-standalone/src/api/routes/widget.ts +428 -0
- package/templates/nextjs-standalone/src/api/routes/wizard.ts +852 -0
- package/templates/nextjs-standalone/src/api/server.ts +187 -69
- package/templates/nextjs-standalone/src/app/error.tsx +5 -2
- package/templates/nextjs-standalone/src/app/globals.css +1 -1
- package/templates/nextjs-standalone/src/app/layout.tsx +7 -2
- package/templates/nextjs-standalone/src/app/page.tsx +39 -5
- package/templates/nextjs-standalone/src/components/data-table/data-table-column-header.tsx +99 -0
- package/templates/nextjs-standalone/src/components/data-table/data-table-date-filter.tsx +225 -0
- package/templates/nextjs-standalone/src/components/data-table/data-table-expandable.tsx +125 -0
- package/templates/nextjs-standalone/src/components/data-table/data-table-faceted-filter.tsx +189 -0
- package/templates/nextjs-standalone/src/components/data-table/data-table-pagination.tsx +112 -0
- package/templates/nextjs-standalone/src/components/data-table/data-table-range-filter.tsx +122 -0
- package/templates/nextjs-standalone/src/components/data-table/data-table-slider-filter.tsx +256 -0
- package/templates/nextjs-standalone/src/components/data-table/data-table-sort-list.tsx +407 -0
- package/templates/nextjs-standalone/src/components/data-table/data-table-toolbar.tsx +149 -0
- package/templates/nextjs-standalone/src/components/data-table/data-table-view-options.tsx +89 -0
- package/templates/nextjs-standalone/src/components/data-table/data-table.tsx +105 -0
- package/templates/nextjs-standalone/src/components/form-dialog.tsx +135 -0
- package/templates/nextjs-standalone/src/components/ui/accordion.tsx +66 -0
- package/templates/nextjs-standalone/src/components/ui/calendar.tsx +220 -0
- package/templates/nextjs-standalone/src/components/ui/checkbox.tsx +32 -0
- package/templates/nextjs-standalone/src/components/ui/faceted.tsx +283 -0
- package/templates/nextjs-standalone/src/components/ui/form.tsx +167 -0
- package/templates/nextjs-standalone/src/components/ui/label.tsx +24 -0
- package/templates/nextjs-standalone/src/components/ui/popover.tsx +89 -0
- package/templates/nextjs-standalone/src/components/ui/progress.tsx +31 -0
- package/templates/nextjs-standalone/src/components/ui/scroll-area.tsx +6 -2
- package/templates/nextjs-standalone/src/components/ui/slider.tsx +63 -0
- package/templates/nextjs-standalone/src/components/ui/sortable.tsx +581 -0
- package/templates/nextjs-standalone/src/components/ui/switch.tsx +35 -0
- package/templates/nextjs-standalone/src/components/ui/textarea.tsx +18 -0
- package/templates/nextjs-standalone/src/config/data-table.ts +82 -0
- package/templates/nextjs-standalone/src/env-check.ts +74 -0
- package/templates/nextjs-standalone/src/hooks/use-callback-ref.ts +27 -0
- package/templates/nextjs-standalone/src/hooks/use-data-table.ts +316 -0
- package/templates/nextjs-standalone/src/hooks/use-debounced-callback.ts +28 -0
- package/templates/nextjs-standalone/src/lib/action-types.ts +7 -41
- package/templates/nextjs-standalone/src/lib/agent-query.ts +4 -2
- package/templates/nextjs-standalone/src/lib/agent.ts +363 -31
- package/templates/nextjs-standalone/src/lib/api-url.ts +2 -3
- package/templates/nextjs-standalone/src/lib/auth/admin-permissions.ts +38 -0
- package/templates/nextjs-standalone/src/lib/auth/audit.ts +19 -4
- package/templates/nextjs-standalone/src/lib/auth/byot.ts +3 -3
- package/templates/nextjs-standalone/src/lib/auth/detect.ts +29 -8
- package/templates/nextjs-standalone/src/lib/auth/managed.ts +104 -14
- package/templates/nextjs-standalone/src/lib/auth/middleware.ts +53 -6
- package/templates/nextjs-standalone/src/lib/auth/migrate.ts +140 -15
- package/templates/nextjs-standalone/src/lib/auth/oauth-state.ts +123 -0
- package/templates/nextjs-standalone/src/lib/auth/org-permissions.ts +55 -0
- package/templates/nextjs-standalone/src/lib/auth/permissions.ts +26 -19
- package/templates/nextjs-standalone/src/lib/auth/server.ts +355 -9
- package/templates/nextjs-standalone/src/lib/auth/simple-key.ts +3 -3
- package/templates/nextjs-standalone/src/lib/auth/types.ts +15 -21
- package/templates/nextjs-standalone/src/lib/billing/enforcement.ts +368 -0
- package/templates/nextjs-standalone/src/lib/billing/plans.ts +155 -0
- package/templates/nextjs-standalone/src/lib/cache/index.ts +92 -0
- package/templates/nextjs-standalone/src/lib/cache/keys.ts +30 -0
- package/templates/nextjs-standalone/src/lib/cache/lru.ts +79 -0
- package/templates/nextjs-standalone/src/lib/cache/types.ts +31 -0
- package/templates/nextjs-standalone/src/lib/compose-refs.ts +62 -0
- package/templates/nextjs-standalone/src/lib/config.ts +563 -11
- package/templates/nextjs-standalone/src/lib/connection-types.ts +9 -0
- package/templates/nextjs-standalone/src/lib/conversation-types.ts +1 -25
- package/templates/nextjs-standalone/src/lib/conversations.ts +345 -14
- package/templates/nextjs-standalone/src/lib/data-table.ts +61 -0
- package/templates/nextjs-standalone/src/lib/db/connection.ts +793 -39
- package/templates/nextjs-standalone/src/lib/db/internal.ts +985 -139
- package/templates/nextjs-standalone/src/lib/db/migrate.ts +295 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0000_baseline.sql +703 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0001_teams_installations.sql +14 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0002_discord_installations.sql +14 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0003_telegram_installations.sql +15 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0004_sandbox_credentials.sql +18 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0005_oauth_state.sql +16 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0006_byot_credentials.sql +14 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0007_gchat_installations.sql +15 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0008_github_installations.sql +14 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0009_linear_installations.sql +15 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0010_whatsapp_installations.sql +14 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0011_email_installations.sql +16 -0
- package/templates/nextjs-standalone/src/lib/db/migrations/0012_region_migrations.sql +25 -0
- package/templates/nextjs-standalone/src/lib/db/schema.ts +1120 -0
- package/templates/nextjs-standalone/src/lib/db/source-rate-limit.ts +89 -139
- package/templates/nextjs-standalone/src/lib/demo.ts +308 -0
- package/templates/nextjs-standalone/src/lib/discord/store.ts +225 -0
- package/templates/nextjs-standalone/src/lib/effect/ai.ts +243 -0
- package/templates/nextjs-standalone/src/lib/effect/errors.ts +234 -0
- package/templates/nextjs-standalone/src/lib/effect/hono.ts +454 -0
- package/templates/nextjs-standalone/src/lib/effect/index.ts +137 -0
- package/templates/nextjs-standalone/src/lib/effect/layers.ts +496 -0
- package/templates/nextjs-standalone/src/lib/effect/services.ts +776 -0
- package/templates/nextjs-standalone/src/lib/effect/sql.ts +178 -0
- package/templates/nextjs-standalone/src/lib/effect/toolkit.ts +123 -0
- package/templates/nextjs-standalone/src/lib/email/delivery.ts +232 -0
- package/templates/nextjs-standalone/src/lib/email/engine.ts +349 -0
- package/templates/nextjs-standalone/src/lib/email/hooks.ts +107 -0
- package/templates/nextjs-standalone/src/lib/email/index.ts +16 -0
- package/templates/nextjs-standalone/src/lib/email/scheduler.ts +72 -0
- package/templates/nextjs-standalone/src/lib/email/sequence.ts +73 -0
- package/templates/nextjs-standalone/src/lib/email/store.ts +163 -0
- package/templates/nextjs-standalone/src/lib/email/templates.ts +215 -0
- package/templates/nextjs-standalone/src/lib/format.test.ts +117 -0
- package/templates/nextjs-standalone/src/lib/format.ts +67 -0
- package/templates/nextjs-standalone/src/lib/gchat/store.ts +202 -0
- package/templates/nextjs-standalone/src/lib/github/store.ts +197 -0
- package/templates/nextjs-standalone/src/lib/id.ts +29 -0
- package/templates/nextjs-standalone/src/lib/integrations/types.ts +166 -0
- package/templates/nextjs-standalone/src/lib/learn/pattern-analyzer.ts +224 -0
- package/templates/nextjs-standalone/src/lib/learn/pattern-cache.ts +229 -0
- package/templates/nextjs-standalone/src/lib/learn/pattern-proposer.ts +87 -0
- package/templates/nextjs-standalone/src/lib/learn/suggestion-helpers.ts +34 -0
- package/templates/nextjs-standalone/src/lib/learn/suggestions.ts +139 -0
- package/templates/nextjs-standalone/src/lib/linear/store.ts +200 -0
- package/templates/nextjs-standalone/src/lib/logger.ts +35 -3
- package/templates/nextjs-standalone/src/lib/metering.ts +272 -0
- package/templates/nextjs-standalone/src/lib/parsers.ts +99 -0
- package/templates/nextjs-standalone/src/lib/plugins/hooks.ts +13 -11
- package/templates/nextjs-standalone/src/lib/plugins/index.ts +3 -1
- package/templates/nextjs-standalone/src/lib/plugins/registry.ts +58 -6
- package/templates/nextjs-standalone/src/lib/plugins/settings.ts +147 -0
- package/templates/nextjs-standalone/src/lib/plugins/wiring.ts +6 -9
- package/templates/nextjs-standalone/src/lib/profiler.ts +1665 -0
- package/templates/nextjs-standalone/src/lib/providers.ts +188 -13
- package/templates/nextjs-standalone/src/lib/rls.ts +172 -60
- package/templates/nextjs-standalone/src/lib/sandbox/credentials.ts +206 -0
- package/templates/nextjs-standalone/src/lib/sandbox/validate.ts +179 -0
- package/templates/nextjs-standalone/src/lib/scheduled-task-types.ts +26 -94
- package/templates/nextjs-standalone/src/lib/scheduled-tasks.ts +174 -34
- package/templates/nextjs-standalone/src/lib/scheduler/delivery.ts +248 -150
- package/templates/nextjs-standalone/src/lib/scheduler/engine.ts +190 -154
- package/templates/nextjs-standalone/src/lib/scheduler/executor.ts +74 -23
- package/templates/nextjs-standalone/src/lib/scheduler/preview.ts +72 -0
- package/templates/nextjs-standalone/src/lib/security/abuse.ts +463 -0
- package/templates/nextjs-standalone/src/lib/semantic/diff.ts +267 -0
- package/templates/nextjs-standalone/src/lib/semantic/entities.ts +167 -0
- package/templates/nextjs-standalone/src/lib/semantic/files.ts +283 -0
- package/templates/nextjs-standalone/src/lib/semantic/index.ts +27 -0
- package/templates/nextjs-standalone/src/lib/{semantic-index.ts → semantic/search.ts} +80 -9
- package/templates/nextjs-standalone/src/lib/semantic/sync.ts +581 -0
- package/templates/nextjs-standalone/src/lib/{semantic.ts → semantic/whitelist.ts} +189 -3
- package/templates/nextjs-standalone/src/lib/settings.ts +817 -0
- package/templates/nextjs-standalone/src/lib/sidecar-types.ts +13 -0
- package/templates/nextjs-standalone/src/lib/slack/store.ts +134 -25
- package/templates/nextjs-standalone/src/lib/startup.ts +528 -362
- package/templates/nextjs-standalone/src/lib/teams/store.ts +216 -0
- package/templates/nextjs-standalone/src/lib/telegram/store.ts +202 -0
- package/templates/nextjs-standalone/src/lib/telemetry.ts +40 -0
- package/templates/nextjs-standalone/src/lib/tools/actions/audit.ts +8 -5
- package/templates/nextjs-standalone/src/lib/tools/actions/email.ts +3 -1
- package/templates/nextjs-standalone/src/lib/tools/actions/handler.ts +276 -93
- package/templates/nextjs-standalone/src/lib/tools/actions/jira.ts +2 -2
- package/templates/nextjs-standalone/src/lib/tools/backends/detect.ts +16 -0
- package/templates/nextjs-standalone/src/lib/tools/backends/index.ts +11 -0
- package/templates/nextjs-standalone/src/lib/tools/backends/nsjail.ts +213 -0
- package/templates/nextjs-standalone/src/lib/tools/backends/shared.ts +103 -0
- package/templates/nextjs-standalone/src/lib/tools/backends/types.ts +26 -0
- package/templates/nextjs-standalone/src/lib/tools/explore-nsjail.ts +7 -228
- package/templates/nextjs-standalone/src/lib/tools/explore-sandbox.ts +4 -29
- package/templates/nextjs-standalone/src/lib/tools/explore-sidecar.ts +18 -2
- package/templates/nextjs-standalone/src/lib/tools/explore.ts +246 -54
- package/templates/nextjs-standalone/src/lib/tools/index.ts +17 -0
- package/templates/nextjs-standalone/src/lib/tools/python-nsjail.ts +11 -139
- package/templates/nextjs-standalone/src/lib/tools/python-sandbox.ts +9 -132
- package/templates/nextjs-standalone/src/lib/tools/python-sidecar.ts +184 -3
- package/templates/nextjs-standalone/src/lib/tools/python-stream.ts +33 -0
- package/templates/nextjs-standalone/src/lib/tools/python-wrapper.ts +129 -0
- package/templates/nextjs-standalone/src/lib/tools/python.ts +115 -15
- package/templates/nextjs-standalone/src/lib/tools/registry.ts +14 -2
- package/templates/nextjs-standalone/src/lib/tools/sql.ts +778 -362
- package/templates/nextjs-standalone/src/lib/tracing.ts +16 -0
- package/templates/nextjs-standalone/src/lib/whatsapp/store.ts +198 -0
- package/templates/nextjs-standalone/src/lib/workspace.ts +89 -0
- package/templates/nextjs-standalone/src/progress.ts +121 -0
- package/templates/nextjs-standalone/src/types/data-table.ts +48 -0
- package/templates/nextjs-standalone/src/ui/atlas-chat-reexport.ts +3 -0
- package/templates/nextjs-standalone/src/ui/components/actions/action-approval-card.tsx +26 -19
- package/templates/nextjs-standalone/src/ui/components/actions/action-status-badge.tsx +3 -3
- package/templates/nextjs-standalone/src/ui/components/admin/admin-layout.tsx +57 -39
- package/templates/nextjs-standalone/src/ui/components/admin/admin-sidebar.tsx +213 -35
- package/templates/nextjs-standalone/src/ui/components/admin/delivery-status-badge.tsx +53 -0
- package/templates/nextjs-standalone/src/ui/components/admin/empty-state.tsx +27 -6
- package/templates/nextjs-standalone/src/ui/components/admin/entity-detail.tsx +3 -52
- package/templates/nextjs-standalone/src/ui/components/admin/error-banner.tsx +2 -2
- package/templates/nextjs-standalone/src/ui/components/admin/feature-disabled.tsx +28 -5
- package/templates/nextjs-standalone/src/ui/components/admin-content-wrapper.tsx +87 -0
- package/templates/nextjs-standalone/src/ui/components/atlas-chat.tsx +449 -166
- package/templates/nextjs-standalone/src/ui/components/branding-head.tsx +41 -0
- package/templates/nextjs-standalone/src/ui/components/chart/chart-detection.ts +62 -5
- package/templates/nextjs-standalone/src/ui/components/chart/result-chart.tsx +316 -125
- package/templates/nextjs-standalone/src/ui/components/chat/api-key-bar.tsx +4 -4
- package/templates/nextjs-standalone/src/ui/components/chat/data-table.tsx +45 -4
- package/templates/nextjs-standalone/src/ui/components/chat/error-banner.tsx +86 -5
- package/templates/nextjs-standalone/src/ui/components/chat/follow-up-chips.tsx +29 -0
- package/templates/nextjs-standalone/src/ui/components/chat/markdown.tsx +24 -0
- package/templates/nextjs-standalone/src/ui/components/chat/prompt-library.tsx +206 -0
- package/templates/nextjs-standalone/src/ui/components/chat/python-result-card.tsx +106 -78
- package/templates/nextjs-standalone/src/ui/components/chat/result-card-base.tsx +101 -0
- package/templates/nextjs-standalone/src/ui/components/chat/share-dialog.tsx +377 -0
- package/templates/nextjs-standalone/src/ui/components/chat/sql-result-card.tsx +94 -73
- package/templates/nextjs-standalone/src/ui/components/chat/suggestion-chips.tsx +46 -0
- package/templates/nextjs-standalone/src/ui/components/chat/tool-part.tsx +16 -4
- package/templates/nextjs-standalone/src/ui/components/conversations/conversation-item.tsx +48 -17
- package/templates/nextjs-standalone/src/ui/components/conversations/conversation-list.tsx +38 -24
- package/templates/nextjs-standalone/src/ui/components/conversations/conversation-sidebar.tsx +66 -7
- package/templates/nextjs-standalone/src/ui/components/conversations/delete-confirmation.tsx +9 -2
- package/templates/nextjs-standalone/src/ui/components/error-boundary.tsx +66 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/delete-cell-dialog.tsx +48 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/fork-branch-selector.tsx +68 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/notebook-cell-input.tsx +76 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/notebook-cell-output.tsx +58 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/notebook-cell-toolbar.tsx +91 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/notebook-cell.tsx +119 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/notebook-empty-state.tsx +19 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/notebook-export.ts +287 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/notebook-input-bar.tsx +49 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/notebook-shell.tsx +266 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/notebook-text-cell.tsx +152 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/types.ts +39 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/use-keyboard-nav.ts +109 -0
- package/templates/nextjs-standalone/src/ui/components/notebook/use-notebook.ts +684 -0
- package/templates/nextjs-standalone/src/ui/components/org-switcher.tsx +111 -0
- package/templates/nextjs-standalone/src/ui/components/region-picker.tsx +103 -0
- package/templates/nextjs-standalone/src/ui/components/schema-explorer/schema-explorer.tsx +522 -0
- package/templates/nextjs-standalone/src/ui/components/social-icons.tsx +26 -0
- package/templates/nextjs-standalone/src/ui/components/tour/guided-tour.tsx +81 -0
- package/templates/nextjs-standalone/src/ui/components/tour/index.ts +5 -0
- package/templates/nextjs-standalone/src/ui/components/tour/nav-bar.tsx +100 -0
- package/templates/nextjs-standalone/src/ui/components/tour/tour-overlay.tsx +298 -0
- package/templates/nextjs-standalone/src/ui/components/tour/tour-steps.ts +43 -0
- package/templates/nextjs-standalone/src/ui/components/tour/types.ts +21 -0
- package/templates/nextjs-standalone/src/ui/components/tour/use-tour.ts +193 -0
- package/templates/nextjs-standalone/src/ui/context-reexport.ts +3 -0
- package/templates/nextjs-standalone/src/ui/hooks/theme-init-script.ts +17 -0
- package/templates/nextjs-standalone/src/ui/hooks/use-admin-fetch.ts +38 -30
- package/templates/nextjs-standalone/src/ui/hooks/use-admin-mutation.ts +188 -0
- package/templates/nextjs-standalone/src/ui/hooks/use-atlas-transport.ts +225 -0
- package/templates/nextjs-standalone/src/ui/hooks/use-branding.ts +68 -0
- package/templates/nextjs-standalone/src/ui/hooks/use-conversations.ts +106 -83
- package/templates/nextjs-standalone/src/ui/hooks/use-dark-mode.ts +134 -10
- package/templates/nextjs-standalone/src/ui/hooks/use-deploy-mode.ts +36 -0
- package/templates/nextjs-standalone/src/ui/hooks/use-platform-admin-guard.ts +49 -0
- package/templates/nextjs-standalone/src/ui/lib/action-types.ts +11 -63
- package/templates/nextjs-standalone/src/ui/lib/admin-schemas.ts +744 -0
- package/templates/nextjs-standalone/src/ui/lib/fetch-client.ts +84 -0
- package/templates/nextjs-standalone/src/ui/lib/fetch-error.ts +54 -0
- package/templates/nextjs-standalone/src/ui/lib/helpers.ts +94 -1
- package/templates/nextjs-standalone/src/ui/lib/types.ts +149 -140
- package/templates/nextjs-standalone/tsconfig.json +3 -2
- package/templates/docker/src/api/__tests__/actions.test.ts +0 -683
- package/templates/docker/src/api/__tests__/admin.test.ts +0 -820
- package/templates/docker/src/api/__tests__/auth.test.ts +0 -165
- package/templates/docker/src/api/__tests__/chat.test.ts +0 -376
- package/templates/docker/src/api/__tests__/conversations.test.ts +0 -555
- package/templates/docker/src/api/__tests__/cors.test.ts +0 -135
- package/templates/docker/src/api/__tests__/health-plugin.test.ts +0 -176
- package/templates/docker/src/api/__tests__/health.test.ts +0 -283
- package/templates/docker/src/api/__tests__/query.test.ts +0 -891
- package/templates/docker/src/api/__tests__/scheduled-tasks.test.ts +0 -601
- package/templates/docker/src/api/__tests__/slack.test.ts +0 -847
- package/templates/docker/src/lib/__tests__/agent-cache.test.ts +0 -439
- package/templates/docker/src/lib/__tests__/agent-dialect.test.ts +0 -131
- package/templates/docker/src/lib/__tests__/agent-health-annotations.test.ts +0 -166
- package/templates/docker/src/lib/__tests__/agent-integration.test.ts +0 -516
- package/templates/docker/src/lib/__tests__/config-actions.test.ts +0 -166
- package/templates/docker/src/lib/__tests__/config.test.ts +0 -1113
- package/templates/docker/src/lib/__tests__/conversations.test.ts +0 -589
- package/templates/docker/src/lib/__tests__/errors.test.ts +0 -256
- package/templates/docker/src/lib/__tests__/logger.test.ts +0 -200
- package/templates/docker/src/lib/__tests__/plugin-aware-validation.test.ts +0 -321
- package/templates/docker/src/lib/__tests__/providers.test.ts +0 -130
- package/templates/docker/src/lib/__tests__/rls.test.ts +0 -435
- package/templates/docker/src/lib/__tests__/scheduled-task-types.test.ts +0 -124
- package/templates/docker/src/lib/__tests__/scheduled-tasks.test.ts +0 -550
- package/templates/docker/src/lib/__tests__/semantic-index.test.ts +0 -547
- package/templates/docker/src/lib/__tests__/semantic-multisource.test.ts +0 -544
- package/templates/docker/src/lib/__tests__/semantic.test.ts +0 -363
- package/templates/docker/src/lib/__tests__/startup-actions.test.ts +0 -461
- package/templates/docker/src/lib/__tests__/startup-first-run.test.ts +0 -429
- package/templates/docker/src/lib/__tests__/startup.test.ts +0 -470
- package/templates/docker/src/lib/__tests__/tracing.test.ts +0 -28
- package/templates/docker/src/lib/auth/__tests__/audit.test.ts +0 -418
- package/templates/docker/src/lib/auth/__tests__/byot-integration.test.ts +0 -222
- package/templates/docker/src/lib/auth/__tests__/byot.test.ts +0 -366
- package/templates/docker/src/lib/auth/__tests__/detect.test.ts +0 -190
- package/templates/docker/src/lib/auth/__tests__/managed.test.ts +0 -173
- package/templates/docker/src/lib/auth/__tests__/middleware.test.ts +0 -456
- package/templates/docker/src/lib/auth/__tests__/migrate.test.ts +0 -203
- package/templates/docker/src/lib/auth/__tests__/permissions.test.ts +0 -225
- package/templates/docker/src/lib/auth/__tests__/server.test.ts +0 -34
- package/templates/docker/src/lib/auth/__tests__/simple-key.test.ts +0 -176
- package/templates/docker/src/lib/auth/__tests__/types.test.ts +0 -44
- package/templates/docker/src/lib/db/__tests__/connection.test.ts +0 -144
- package/templates/docker/src/lib/db/__tests__/internal.test.ts +0 -387
- package/templates/docker/src/lib/db/__tests__/registry-health.test.ts +0 -190
- package/templates/docker/src/lib/db/__tests__/registry-pool-limits.test.ts +0 -137
- package/templates/docker/src/lib/db/__tests__/registry.test.ts +0 -398
- package/templates/docker/src/lib/db/__tests__/source-rate-limit.test.ts +0 -130
- package/templates/docker/src/lib/errors.ts +0 -154
- package/templates/docker/src/lib/plugins/__tests__/hooks-integration.test.ts +0 -204
- package/templates/docker/src/lib/plugins/__tests__/hooks.test.ts +0 -529
- package/templates/docker/src/lib/plugins/__tests__/migrate.test.ts +0 -875
- package/templates/docker/src/lib/plugins/__tests__/registry.test.ts +0 -373
- package/templates/docker/src/lib/plugins/__tests__/tools.test.ts +0 -49
- package/templates/docker/src/lib/plugins/__tests__/wiring.test.ts +0 -799
- package/templates/docker/src/lib/scheduler/__tests__/delivery.test.ts +0 -192
- package/templates/docker/src/lib/scheduler/__tests__/engine.test.ts +0 -248
- package/templates/docker/src/lib/scheduler/__tests__/format-email.test.ts +0 -96
- package/templates/docker/src/lib/scheduler/__tests__/format-slack.test.ts +0 -78
- package/templates/docker/src/lib/scheduler/__tests__/format-webhook.test.ts +0 -78
- package/templates/docker/src/lib/scheduler/index.ts +0 -7
- package/templates/docker/src/lib/slack/__tests__/api.test.ts +0 -160
- package/templates/docker/src/lib/slack/__tests__/format.test.ts +0 -237
- package/templates/docker/src/lib/slack/__tests__/store.test.ts +0 -188
- package/templates/docker/src/lib/slack/__tests__/threads.test.ts +0 -112
- package/templates/docker/src/lib/slack/__tests__/verify.test.ts +0 -111
- package/templates/docker/src/lib/tools/__tests__/action-permissions.test.ts +0 -594
- package/templates/docker/src/lib/tools/__tests__/custom-validation.test.ts +0 -240
- package/templates/docker/src/lib/tools/__tests__/explore-backend.test.ts +0 -267
- package/templates/docker/src/lib/tools/__tests__/explore-nsjail.test.ts +0 -506
- package/templates/docker/src/lib/tools/__tests__/explore-plugin.test.ts +0 -374
- package/templates/docker/src/lib/tools/__tests__/explore-sdk-compat.test.ts +0 -82
- package/templates/docker/src/lib/tools/__tests__/explore-sidecar.test.ts +0 -210
- package/templates/docker/src/lib/tools/__tests__/python-nsjail.test.ts +0 -515
- package/templates/docker/src/lib/tools/__tests__/python-sandbox.test.ts +0 -397
- package/templates/docker/src/lib/tools/__tests__/python-sidecar.test.ts +0 -365
- package/templates/docker/src/lib/tools/__tests__/python.test.ts +0 -331
- package/templates/docker/src/lib/tools/__tests__/registry-actions.test.ts +0 -132
- package/templates/docker/src/lib/tools/__tests__/registry.test.ts +0 -242
- package/templates/docker/src/lib/tools/__tests__/sql-audit.test.ts +0 -227
- package/templates/docker/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +0 -100
- package/templates/docker/src/lib/tools/__tests__/sql-ratelimit.test.ts +0 -227
- package/templates/docker/src/lib/tools/__tests__/sql.test.ts +0 -709
- package/templates/docker/src/lib/tools/actions/__tests__/audit.test.ts +0 -211
- package/templates/docker/src/lib/tools/actions/__tests__/email.test.ts +0 -378
- package/templates/docker/src/lib/tools/actions/__tests__/handler.test.ts +0 -681
- package/templates/docker/src/lib/tools/actions/__tests__/jira.test.ts +0 -427
- package/templates/docker/src/test-setup.ts +0 -38
- package/templates/docker/src/types/vercel-sandbox.d.ts +0 -61
- package/templates/docker/src/ui/components/chat/managed-auth-card.tsx +0 -116
- package/templates/nextjs-standalone/src/api/__tests__/actions.test.ts +0 -683
- package/templates/nextjs-standalone/src/api/__tests__/admin.test.ts +0 -820
- package/templates/nextjs-standalone/src/api/__tests__/auth.test.ts +0 -165
- package/templates/nextjs-standalone/src/api/__tests__/chat.test.ts +0 -376
- package/templates/nextjs-standalone/src/api/__tests__/conversations.test.ts +0 -555
- package/templates/nextjs-standalone/src/api/__tests__/cors.test.ts +0 -135
- package/templates/nextjs-standalone/src/api/__tests__/health-plugin.test.ts +0 -176
- package/templates/nextjs-standalone/src/api/__tests__/health.test.ts +0 -283
- package/templates/nextjs-standalone/src/api/__tests__/query.test.ts +0 -891
- package/templates/nextjs-standalone/src/api/__tests__/scheduled-tasks.test.ts +0 -601
- package/templates/nextjs-standalone/src/api/__tests__/slack.test.ts +0 -847
- package/templates/nextjs-standalone/src/app/global-error.tsx +0 -68
- package/templates/nextjs-standalone/src/lib/__tests__/agent-cache.test.ts +0 -439
- package/templates/nextjs-standalone/src/lib/__tests__/agent-dialect.test.ts +0 -131
- package/templates/nextjs-standalone/src/lib/__tests__/agent-health-annotations.test.ts +0 -166
- package/templates/nextjs-standalone/src/lib/__tests__/agent-integration.test.ts +0 -516
- package/templates/nextjs-standalone/src/lib/__tests__/config-actions.test.ts +0 -166
- package/templates/nextjs-standalone/src/lib/__tests__/config.test.ts +0 -1113
- package/templates/nextjs-standalone/src/lib/__tests__/conversations.test.ts +0 -589
- package/templates/nextjs-standalone/src/lib/__tests__/errors.test.ts +0 -256
- package/templates/nextjs-standalone/src/lib/__tests__/logger.test.ts +0 -200
- package/templates/nextjs-standalone/src/lib/__tests__/plugin-aware-validation.test.ts +0 -321
- package/templates/nextjs-standalone/src/lib/__tests__/providers.test.ts +0 -130
- package/templates/nextjs-standalone/src/lib/__tests__/rls.test.ts +0 -435
- package/templates/nextjs-standalone/src/lib/__tests__/scheduled-task-types.test.ts +0 -124
- package/templates/nextjs-standalone/src/lib/__tests__/scheduled-tasks.test.ts +0 -550
- package/templates/nextjs-standalone/src/lib/__tests__/semantic-index.test.ts +0 -547
- package/templates/nextjs-standalone/src/lib/__tests__/semantic-multisource.test.ts +0 -544
- package/templates/nextjs-standalone/src/lib/__tests__/semantic.test.ts +0 -363
- package/templates/nextjs-standalone/src/lib/__tests__/startup-actions.test.ts +0 -461
- package/templates/nextjs-standalone/src/lib/__tests__/startup-first-run.test.ts +0 -429
- package/templates/nextjs-standalone/src/lib/__tests__/startup.test.ts +0 -470
- package/templates/nextjs-standalone/src/lib/__tests__/tracing.test.ts +0 -28
- package/templates/nextjs-standalone/src/lib/auth/__tests__/audit.test.ts +0 -418
- package/templates/nextjs-standalone/src/lib/auth/__tests__/byot-integration.test.ts +0 -222
- package/templates/nextjs-standalone/src/lib/auth/__tests__/byot.test.ts +0 -366
- package/templates/nextjs-standalone/src/lib/auth/__tests__/detect.test.ts +0 -190
- package/templates/nextjs-standalone/src/lib/auth/__tests__/managed.test.ts +0 -173
- package/templates/nextjs-standalone/src/lib/auth/__tests__/middleware.test.ts +0 -456
- package/templates/nextjs-standalone/src/lib/auth/__tests__/migrate.test.ts +0 -203
- package/templates/nextjs-standalone/src/lib/auth/__tests__/permissions.test.ts +0 -225
- package/templates/nextjs-standalone/src/lib/auth/__tests__/server.test.ts +0 -34
- package/templates/nextjs-standalone/src/lib/auth/__tests__/simple-key.test.ts +0 -176
- package/templates/nextjs-standalone/src/lib/auth/__tests__/types.test.ts +0 -44
- package/templates/nextjs-standalone/src/lib/db/__tests__/connection.test.ts +0 -144
- package/templates/nextjs-standalone/src/lib/db/__tests__/internal.test.ts +0 -387
- package/templates/nextjs-standalone/src/lib/db/__tests__/registry-health.test.ts +0 -190
- package/templates/nextjs-standalone/src/lib/db/__tests__/registry-pool-limits.test.ts +0 -137
- package/templates/nextjs-standalone/src/lib/db/__tests__/registry.test.ts +0 -398
- package/templates/nextjs-standalone/src/lib/db/__tests__/source-rate-limit.test.ts +0 -130
- package/templates/nextjs-standalone/src/lib/errors.ts +0 -154
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks-integration.test.ts +0 -204
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks.test.ts +0 -529
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/migrate.test.ts +0 -875
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/registry.test.ts +0 -373
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/tools.test.ts +0 -49
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/wiring.test.ts +0 -799
- package/templates/nextjs-standalone/src/lib/scheduler/__tests__/delivery.test.ts +0 -192
- package/templates/nextjs-standalone/src/lib/scheduler/__tests__/engine.test.ts +0 -248
- package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-email.test.ts +0 -96
- package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-slack.test.ts +0 -78
- package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-webhook.test.ts +0 -78
- package/templates/nextjs-standalone/src/lib/scheduler/index.ts +0 -7
- package/templates/nextjs-standalone/src/lib/slack/__tests__/api.test.ts +0 -160
- package/templates/nextjs-standalone/src/lib/slack/__tests__/format.test.ts +0 -237
- package/templates/nextjs-standalone/src/lib/slack/__tests__/store.test.ts +0 -188
- package/templates/nextjs-standalone/src/lib/slack/__tests__/threads.test.ts +0 -112
- package/templates/nextjs-standalone/src/lib/slack/__tests__/verify.test.ts +0 -111
- package/templates/nextjs-standalone/src/lib/tools/__tests__/action-permissions.test.ts +0 -594
- package/templates/nextjs-standalone/src/lib/tools/__tests__/custom-validation.test.ts +0 -240
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-backend.test.ts +0 -267
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-nsjail.test.ts +0 -506
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-plugin.test.ts +0 -374
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sdk-compat.test.ts +0 -82
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sidecar.test.ts +0 -210
- package/templates/nextjs-standalone/src/lib/tools/__tests__/python-nsjail.test.ts +0 -515
- package/templates/nextjs-standalone/src/lib/tools/__tests__/python-sandbox.test.ts +0 -397
- package/templates/nextjs-standalone/src/lib/tools/__tests__/python-sidecar.test.ts +0 -365
- package/templates/nextjs-standalone/src/lib/tools/__tests__/python.test.ts +0 -331
- package/templates/nextjs-standalone/src/lib/tools/__tests__/registry-actions.test.ts +0 -132
- package/templates/nextjs-standalone/src/lib/tools/__tests__/registry.test.ts +0 -242
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-audit.test.ts +0 -227
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +0 -100
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-ratelimit.test.ts +0 -227
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql.test.ts +0 -709
- package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/audit.test.ts +0 -211
- package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/email.test.ts +0 -378
- package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/handler.test.ts +0 -681
- package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/jira.test.ts +0 -427
- package/templates/nextjs-standalone/src/test-setup.ts +0 -38
- package/templates/nextjs-standalone/src/ui/components/chat/managed-auth-card.tsx +0 -116
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* tables and views are profiled automatically.
|
|
37
37
|
*
|
|
38
38
|
* Requires ATLAS_DATASOURCE_URL in environment.
|
|
39
|
-
* Supports PostgreSQL
|
|
39
|
+
* Supports PostgreSQL, MySQL, ClickHouse, Snowflake, DuckDB, and Salesforce.
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
42
|
import { Pool } from "pg";
|
|
@@ -46,6 +46,76 @@ import * as yaml from "js-yaml";
|
|
|
46
46
|
import * as p from "@clack/prompts";
|
|
47
47
|
import pc from "picocolors";
|
|
48
48
|
import { type DBType } from "@atlas/api/lib/db/connection";
|
|
49
|
+
import type { DuckDBConnection } from "@duckdb/node-api";
|
|
50
|
+
import type { SObjectField } from "../../../plugins/salesforce/src/connection";
|
|
51
|
+
import { checkEnvFile } from "../src/env-check";
|
|
52
|
+
import { type ProfileProgressCallbacks, createProgressTracker, formatDuration } from "../src/progress";
|
|
53
|
+
import {
|
|
54
|
+
type DatabaseObject,
|
|
55
|
+
type ColumnProfile,
|
|
56
|
+
type ForeignKey,
|
|
57
|
+
type TableProfile,
|
|
58
|
+
type ProfileError,
|
|
59
|
+
type ProfilingResult,
|
|
60
|
+
type ProfileLogger,
|
|
61
|
+
isFatalConnectionError,
|
|
62
|
+
checkFailureThreshold,
|
|
63
|
+
isView,
|
|
64
|
+
isMatView,
|
|
65
|
+
isViewLike,
|
|
66
|
+
mapSQLType,
|
|
67
|
+
analyzeTableProfiles,
|
|
68
|
+
generateEntityYAML,
|
|
69
|
+
generateCatalogYAML,
|
|
70
|
+
generateMetricYAML,
|
|
71
|
+
generateGlossaryYAML,
|
|
72
|
+
outputDirForDatasource,
|
|
73
|
+
listPostgresObjects,
|
|
74
|
+
listMySQLObjects,
|
|
75
|
+
profilePostgres,
|
|
76
|
+
profileMySQL,
|
|
77
|
+
} from "@atlas/api/lib/profiler";
|
|
78
|
+
|
|
79
|
+
/** Adapts the profiler's structured logger to CLI console output. */
|
|
80
|
+
const cliProfileLogger: ProfileLogger = {
|
|
81
|
+
info(_obj, msg) { console.log(` ${msg}`); },
|
|
82
|
+
warn(obj, msg) {
|
|
83
|
+
const ctx = [obj.table, obj.column].filter(Boolean).join(".");
|
|
84
|
+
console.warn(` Warning: ${msg}${ctx ? ` (${ctx})` : ""}${obj.err ? `: ${obj.err}` : ""}`);
|
|
85
|
+
},
|
|
86
|
+
error(obj, msg) {
|
|
87
|
+
const ctx = [obj.table, obj.column].filter(Boolean).join(".");
|
|
88
|
+
console.error(` ${msg}${ctx ? ` (${ctx})` : ""}${obj.err ? `: ${obj.err}` : ""}`);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Re-export from shared profiler for test backward compatibility
|
|
93
|
+
export {
|
|
94
|
+
type ColumnProfile,
|
|
95
|
+
type TableProfile,
|
|
96
|
+
type ProfileError,
|
|
97
|
+
type ProfilingResult,
|
|
98
|
+
FATAL_ERROR_PATTERN,
|
|
99
|
+
isFatalConnectionError,
|
|
100
|
+
checkFailureThreshold,
|
|
101
|
+
generateEntityYAML,
|
|
102
|
+
generateCatalogYAML,
|
|
103
|
+
generateMetricYAML,
|
|
104
|
+
generateGlossaryYAML,
|
|
105
|
+
mapSQLType,
|
|
106
|
+
isView,
|
|
107
|
+
isMatView,
|
|
108
|
+
isViewLike,
|
|
109
|
+
entityName,
|
|
110
|
+
outputDirForDatasource,
|
|
111
|
+
inferForeignKeys,
|
|
112
|
+
detectAbandonedTables,
|
|
113
|
+
detectEnumInconsistency,
|
|
114
|
+
detectDenormalizedTables,
|
|
115
|
+
analyzeTableProfiles,
|
|
116
|
+
pluralize,
|
|
117
|
+
singularize,
|
|
118
|
+
} from "@atlas/api/lib/profiler";
|
|
49
119
|
|
|
50
120
|
/** CLI-local DB type detection — supports all URL schemes (core + plugin databases). */
|
|
51
121
|
function detectDBType(url: string): DBType {
|
|
@@ -70,67 +140,19 @@ async function loadDuckDB() {
|
|
|
70
140
|
const SEMANTIC_DIR = path.resolve("semantic");
|
|
71
141
|
const ENTITIES_DIR = path.join(SEMANTIC_DIR, "entities");
|
|
72
142
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
unique_count: number | null;
|
|
87
|
-
null_count: number | null;
|
|
88
|
-
sample_values: string[];
|
|
89
|
-
is_primary_key: boolean;
|
|
90
|
-
is_foreign_key: boolean;
|
|
91
|
-
fk_target_table: string | null;
|
|
92
|
-
fk_target_column: string | null;
|
|
93
|
-
is_enum_like: boolean;
|
|
94
|
-
profiler_notes: string[];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export interface ForeignKey {
|
|
98
|
-
from_column: string;
|
|
99
|
-
to_table: string;
|
|
100
|
-
to_column: string;
|
|
101
|
-
source: "constraint" | "inferred";
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export interface TableProfile {
|
|
105
|
-
table_name: string;
|
|
106
|
-
object_type: ObjectType;
|
|
107
|
-
row_count: number;
|
|
108
|
-
columns: ColumnProfile[];
|
|
109
|
-
primary_key_columns: string[];
|
|
110
|
-
foreign_keys: ForeignKey[];
|
|
111
|
-
inferred_foreign_keys: ForeignKey[];
|
|
112
|
-
profiler_notes: string[];
|
|
113
|
-
table_flags: {
|
|
114
|
-
possibly_abandoned: boolean;
|
|
115
|
-
possibly_denormalized: boolean;
|
|
116
|
-
};
|
|
117
|
-
matview_populated?: boolean;
|
|
118
|
-
partition_info?: { strategy: "range" | "list" | "hash"; key: string; children: string[] };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Check whether a profile represents a database view. */
|
|
122
|
-
export function isView(profile: TableProfile): boolean {
|
|
123
|
-
return profile.object_type === "view";
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Check whether a profile represents a materialized view. */
|
|
127
|
-
export function isMatView(profile: TableProfile): boolean {
|
|
128
|
-
return profile.object_type === "materialized_view";
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Check whether a profile is view-like (view or materialized view) — skip PK/FK/measures/patterns. */
|
|
132
|
-
export function isViewLike(profile: TableProfile): boolean {
|
|
133
|
-
return profile.object_type === "view" || profile.object_type === "materialized_view";
|
|
143
|
+
/** Log a warning summary for profiling errors (first 5 + overflow). CLI-specific: uses console.warn formatting rather than the profiler's structured logger. */
|
|
144
|
+
export function logProfilingErrors(errors: ProfileError[], total: number): void {
|
|
145
|
+
const pct = Math.round((errors.length / total) * 100);
|
|
146
|
+
console.warn(
|
|
147
|
+
`\nWarning: ${errors.length}/${total} tables (${pct}%) failed to profile:`
|
|
148
|
+
);
|
|
149
|
+
const preview = errors.slice(0, 5);
|
|
150
|
+
for (const e of preview) {
|
|
151
|
+
console.warn(` - ${e.table}: ${e.error}`);
|
|
152
|
+
}
|
|
153
|
+
if (errors.length > 5) {
|
|
154
|
+
console.warn(` ... and ${errors.length - 5} more`);
|
|
155
|
+
}
|
|
134
156
|
}
|
|
135
157
|
|
|
136
158
|
// --- Shared helpers ---
|
|
@@ -163,638 +185,11 @@ function requireFlagIdentifier(args: string[], flag: string, label: string): str
|
|
|
163
185
|
return value;
|
|
164
186
|
}
|
|
165
187
|
|
|
166
|
-
// --- PostgreSQL profiler ---
|
|
167
|
-
|
|
168
|
-
/** Schema-qualified table reference for SQL queries. */
|
|
169
|
-
function pgTableRef(tableName: string, schema: string): string {
|
|
170
|
-
const safeTable = tableName.replace(/"/g, '""');
|
|
171
|
-
const safeSchema = schema.replace(/"/g, '""');
|
|
172
|
-
return schema === "public" ? `"${safeTable}"` : `"${safeSchema}"."${safeTable}"`;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async function queryPrimaryKeys(
|
|
176
|
-
pool: Pool,
|
|
177
|
-
tableName: string,
|
|
178
|
-
schema: string = "public"
|
|
179
|
-
): Promise<string[]> {
|
|
180
|
-
const result = await pool.query(
|
|
181
|
-
`
|
|
182
|
-
SELECT a.attname AS column_name
|
|
183
|
-
FROM pg_constraint c
|
|
184
|
-
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
|
|
185
|
-
WHERE c.contype = 'p'
|
|
186
|
-
AND c.conrelid = $1::regclass
|
|
187
|
-
ORDER BY a.attnum
|
|
188
|
-
`,
|
|
189
|
-
[pgTableRef(tableName, schema)]
|
|
190
|
-
);
|
|
191
|
-
return result.rows.map((r: { column_name: string }) => r.column_name);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async function queryForeignKeys(
|
|
195
|
-
pool: Pool,
|
|
196
|
-
tableName: string,
|
|
197
|
-
schema: string = "public"
|
|
198
|
-
): Promise<ForeignKey[]> {
|
|
199
|
-
const result = await pool.query(
|
|
200
|
-
`
|
|
201
|
-
SELECT
|
|
202
|
-
a_from.attname AS from_column,
|
|
203
|
-
cl_to.relname AS to_table,
|
|
204
|
-
a_to.attname AS to_column,
|
|
205
|
-
ns_to.nspname AS to_schema
|
|
206
|
-
FROM pg_constraint c
|
|
207
|
-
JOIN pg_attribute a_from
|
|
208
|
-
ON a_from.attrelid = c.conrelid AND a_from.attnum = ANY(c.conkey)
|
|
209
|
-
JOIN pg_class cl_to
|
|
210
|
-
ON cl_to.oid = c.confrelid
|
|
211
|
-
JOIN pg_namespace ns_to
|
|
212
|
-
ON ns_to.oid = cl_to.relnamespace
|
|
213
|
-
JOIN pg_attribute a_to
|
|
214
|
-
ON a_to.attrelid = c.confrelid AND a_to.attnum = ANY(c.confkey)
|
|
215
|
-
WHERE c.contype = 'f'
|
|
216
|
-
AND c.conrelid = $1::regclass
|
|
217
|
-
ORDER BY a_from.attname
|
|
218
|
-
`,
|
|
219
|
-
[pgTableRef(tableName, schema)]
|
|
220
|
-
);
|
|
221
|
-
return result.rows.map(
|
|
222
|
-
(r: { from_column: string; to_table: string; to_column: string; to_schema: string }) => ({
|
|
223
|
-
from_column: r.from_column,
|
|
224
|
-
// Qualify FK target with schema when it differs from the profiled schema
|
|
225
|
-
to_table: r.to_schema !== schema ? `${r.to_schema}.${r.to_table}` : r.to_table,
|
|
226
|
-
to_column: r.to_column,
|
|
227
|
-
source: "constraint" as const,
|
|
228
|
-
})
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export async function listPostgresObjects(connectionString: string, schema: string = "public"): Promise<DatabaseObject[]> {
|
|
233
|
-
const pool = new Pool({ connectionString, max: 1, connectionTimeoutMillis: 5000 });
|
|
234
|
-
try {
|
|
235
|
-
const result = await pool.query(
|
|
236
|
-
`SELECT table_name, table_type FROM information_schema.tables
|
|
237
|
-
WHERE table_schema = $1 AND table_type IN ('BASE TABLE', 'VIEW')
|
|
238
|
-
ORDER BY table_name`,
|
|
239
|
-
[schema]
|
|
240
|
-
);
|
|
241
|
-
const objects: DatabaseObject[] = result.rows.map((r: { table_name: string; table_type: string }) => ({
|
|
242
|
-
name: r.table_name,
|
|
243
|
-
type: r.table_type === "VIEW" ? "view" as const : "table" as const,
|
|
244
|
-
}));
|
|
245
|
-
|
|
246
|
-
// Materialized views are not in information_schema.tables — query pg_class directly
|
|
247
|
-
try {
|
|
248
|
-
const matviewResult = await pool.query(
|
|
249
|
-
`SELECT c.relname AS table_name
|
|
250
|
-
FROM pg_class c
|
|
251
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
252
|
-
WHERE n.nspname = $1 AND c.relkind = 'm'
|
|
253
|
-
ORDER BY c.relname`,
|
|
254
|
-
[schema]
|
|
255
|
-
);
|
|
256
|
-
for (const r of matviewResult.rows as { table_name: string }[]) {
|
|
257
|
-
objects.push({ name: r.table_name, type: "materialized_view" });
|
|
258
|
-
}
|
|
259
|
-
} catch (mvErr) {
|
|
260
|
-
console.warn(` Warning: Could not discover materialized views: ${mvErr instanceof Error ? mvErr.message : String(mvErr)}`);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return objects.sort((a, b) => a.name.localeCompare(b.name));
|
|
264
|
-
} finally {
|
|
265
|
-
await pool.end();
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
export async function listMySQLObjects(connectionString: string): Promise<DatabaseObject[]> {
|
|
270
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
271
|
-
const mysql = require("mysql2/promise");
|
|
272
|
-
const pool = mysql.createPool({
|
|
273
|
-
uri: connectionString,
|
|
274
|
-
connectionLimit: 1,
|
|
275
|
-
connectTimeout: 5000,
|
|
276
|
-
});
|
|
277
|
-
try {
|
|
278
|
-
const [rows] = await pool.execute(
|
|
279
|
-
`SELECT TABLE_NAME, TABLE_TYPE FROM information_schema.TABLES
|
|
280
|
-
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE IN ('BASE TABLE', 'VIEW')
|
|
281
|
-
ORDER BY TABLE_NAME`
|
|
282
|
-
);
|
|
283
|
-
return (rows as { TABLE_NAME: string; TABLE_TYPE: string }[]).map((r) => ({
|
|
284
|
-
name: r.TABLE_NAME,
|
|
285
|
-
type: r.TABLE_TYPE === "VIEW" ? "view" as const : "table" as const,
|
|
286
|
-
}));
|
|
287
|
-
} finally {
|
|
288
|
-
await pool.end();
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
export async function profilePostgres(
|
|
293
|
-
connectionString: string,
|
|
294
|
-
filterTables?: string[],
|
|
295
|
-
prefetchedObjects?: DatabaseObject[],
|
|
296
|
-
schema: string = "public"
|
|
297
|
-
): Promise<TableProfile[]> {
|
|
298
|
-
const pool = new Pool({ connectionString, max: 3 });
|
|
299
|
-
const profiles: TableProfile[] = [];
|
|
300
|
-
const errors: { table: string; error: string }[] = [];
|
|
301
|
-
|
|
302
|
-
let allObjects: DatabaseObject[];
|
|
303
|
-
if (prefetchedObjects) {
|
|
304
|
-
allObjects = prefetchedObjects;
|
|
305
|
-
} else {
|
|
306
|
-
const tablesResult = await pool.query(
|
|
307
|
-
`SELECT table_name, table_type FROM information_schema.tables
|
|
308
|
-
WHERE table_schema = $1 AND table_type IN ('BASE TABLE', 'VIEW')
|
|
309
|
-
ORDER BY table_name`,
|
|
310
|
-
[schema]
|
|
311
|
-
);
|
|
312
|
-
allObjects = tablesResult.rows.map((r: { table_name: string; table_type: string }) => ({
|
|
313
|
-
name: r.table_name,
|
|
314
|
-
type: r.table_type === "VIEW" ? "view" as const : "table" as const,
|
|
315
|
-
}));
|
|
316
|
-
|
|
317
|
-
// Materialized views are not in information_schema.tables — query pg_class directly
|
|
318
|
-
try {
|
|
319
|
-
const matviewResult = await pool.query(
|
|
320
|
-
`SELECT c.relname AS table_name
|
|
321
|
-
FROM pg_class c
|
|
322
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
323
|
-
WHERE n.nspname = $1 AND c.relkind = 'm'
|
|
324
|
-
ORDER BY c.relname`,
|
|
325
|
-
[schema]
|
|
326
|
-
);
|
|
327
|
-
for (const r of matviewResult.rows as { table_name: string }[]) {
|
|
328
|
-
allObjects.push({ name: r.table_name, type: "materialized_view" });
|
|
329
|
-
}
|
|
330
|
-
} catch (mvErr) {
|
|
331
|
-
console.warn(` Warning: Could not discover materialized views: ${mvErr instanceof Error ? mvErr.message : String(mvErr)}`);
|
|
332
|
-
}
|
|
333
|
-
allObjects.sort((a, b) => a.name.localeCompare(b.name));
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const objectsToProfile = filterTables
|
|
337
|
-
? allObjects.filter((o) => filterTables.includes(o.name))
|
|
338
|
-
: allObjects;
|
|
339
|
-
|
|
340
|
-
for (const [i, obj] of objectsToProfile.entries()) {
|
|
341
|
-
const table_name = obj.name;
|
|
342
|
-
const objectType = obj.type;
|
|
343
|
-
const objectLabel = objectType === "view" ? " [view]" : objectType === "materialized_view" ? " [matview]" : "";
|
|
344
|
-
console.log(` [${i + 1}/${objectsToProfile.length}] Profiling ${table_name}${objectLabel}...`);
|
|
345
|
-
|
|
346
|
-
try {
|
|
347
|
-
// Check matview populated status BEFORE COUNT(*) — unpopulated matviews throw on scan
|
|
348
|
-
let matview_populated: boolean | undefined;
|
|
349
|
-
if (objectType === "materialized_view") {
|
|
350
|
-
try {
|
|
351
|
-
const mvResult = await pool.query(
|
|
352
|
-
`SELECT ispopulated FROM pg_matviews WHERE schemaname = $1 AND matviewname = $2`,
|
|
353
|
-
[schema, table_name]
|
|
354
|
-
);
|
|
355
|
-
if (mvResult.rows.length > 0) {
|
|
356
|
-
matview_populated = mvResult.rows[0].ispopulated;
|
|
357
|
-
}
|
|
358
|
-
} catch (mvErr) {
|
|
359
|
-
console.warn(` Warning: Could not read matview status for ${table_name}: ${mvErr instanceof Error ? mvErr.message : String(mvErr)}`);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Skip COUNT(*) for unpopulated matviews — they error on table scan
|
|
364
|
-
let rowCount: number;
|
|
365
|
-
if (matview_populated === false) {
|
|
366
|
-
rowCount = 0;
|
|
367
|
-
console.log(` Materialized view ${table_name} is not populated — skipping data profiling`);
|
|
368
|
-
} else {
|
|
369
|
-
const countResult = await pool.query(
|
|
370
|
-
`SELECT COUNT(*) as c FROM ${pgTableRef(table_name, schema)}`
|
|
371
|
-
);
|
|
372
|
-
rowCount = parseInt(countResult.rows[0].c, 10);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Get primary keys and foreign keys from system catalogs (skip for views and matviews)
|
|
376
|
-
let primaryKeyColumns: string[] = [];
|
|
377
|
-
let foreignKeys: ForeignKey[] = [];
|
|
378
|
-
if (objectType === "table") {
|
|
379
|
-
try {
|
|
380
|
-
primaryKeyColumns = await queryPrimaryKeys(pool, table_name, schema);
|
|
381
|
-
} catch (pkErr) {
|
|
382
|
-
console.warn(` Warning: Could not read PK constraints for ${table_name}: ${pkErr instanceof Error ? pkErr.message : String(pkErr)}`);
|
|
383
|
-
}
|
|
384
|
-
try {
|
|
385
|
-
foreignKeys = await queryForeignKeys(pool, table_name, schema);
|
|
386
|
-
} catch (fkErr) {
|
|
387
|
-
console.warn(` Warning: Could not read FK constraints for ${table_name}: ${fkErr instanceof Error ? fkErr.message : String(fkErr)}`);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const fkLookup = new Map(
|
|
392
|
-
foreignKeys.map((fk) => [fk.from_column, fk])
|
|
393
|
-
);
|
|
394
|
-
|
|
395
|
-
// information_schema.columns excludes materialized views in PostgreSQL,
|
|
396
|
-
// so use pg_attribute + pg_type for matviews (#255)
|
|
397
|
-
const colResult = objectType === "materialized_view"
|
|
398
|
-
? await pool.query(
|
|
399
|
-
`
|
|
400
|
-
SELECT a.attname AS column_name,
|
|
401
|
-
pg_catalog.format_type(a.atttypid, a.atttypmod) AS data_type,
|
|
402
|
-
CASE WHEN a.attnotnull THEN 'NO' ELSE 'YES' END AS is_nullable
|
|
403
|
-
FROM pg_attribute a
|
|
404
|
-
JOIN pg_class c ON c.oid = a.attrelid
|
|
405
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
406
|
-
WHERE n.nspname = $2
|
|
407
|
-
AND c.relname = $1
|
|
408
|
-
AND a.attnum > 0
|
|
409
|
-
AND NOT a.attisdropped
|
|
410
|
-
ORDER BY a.attnum
|
|
411
|
-
`,
|
|
412
|
-
[table_name, schema]
|
|
413
|
-
)
|
|
414
|
-
: await pool.query(
|
|
415
|
-
`
|
|
416
|
-
SELECT column_name, data_type, is_nullable
|
|
417
|
-
FROM information_schema.columns
|
|
418
|
-
WHERE table_name = $1 AND table_schema = $2
|
|
419
|
-
ORDER BY ordinal_position
|
|
420
|
-
`,
|
|
421
|
-
[table_name, schema]
|
|
422
|
-
);
|
|
423
|
-
|
|
424
|
-
const columns: ColumnProfile[] = [];
|
|
425
|
-
|
|
426
|
-
for (const col of colResult.rows) {
|
|
427
|
-
let unique_count: number | null = null;
|
|
428
|
-
let null_count: number | null = null;
|
|
429
|
-
let sample_values: string[] = [];
|
|
430
|
-
let isEnumLike = false;
|
|
431
|
-
|
|
432
|
-
const isPK = primaryKeyColumns.includes(col.column_name);
|
|
433
|
-
const fkInfo = fkLookup.get(col.column_name);
|
|
434
|
-
const isFK = !!fkInfo;
|
|
435
|
-
|
|
436
|
-
// Skip data profiling for unpopulated matviews — no data to scan
|
|
437
|
-
if (matview_populated !== false) {
|
|
438
|
-
try {
|
|
439
|
-
const tableRef = pgTableRef(table_name, schema);
|
|
440
|
-
const uq = await pool.query(
|
|
441
|
-
`SELECT COUNT(DISTINCT "${col.column_name}") as c FROM ${tableRef}`
|
|
442
|
-
);
|
|
443
|
-
unique_count = parseInt(uq.rows[0].c, 10);
|
|
444
|
-
|
|
445
|
-
const nc = await pool.query(
|
|
446
|
-
`SELECT COUNT(*) as c FROM ${tableRef} WHERE "${col.column_name}" IS NULL`
|
|
447
|
-
);
|
|
448
|
-
null_count = parseInt(nc.rows[0].c, 10);
|
|
449
|
-
|
|
450
|
-
// For enum-like columns, get ALL distinct values; otherwise sample 10
|
|
451
|
-
const isTextType =
|
|
452
|
-
col.data_type === "text" ||
|
|
453
|
-
col.data_type === "character varying" ||
|
|
454
|
-
col.data_type === "character";
|
|
455
|
-
isEnumLike =
|
|
456
|
-
isTextType &&
|
|
457
|
-
unique_count !== null &&
|
|
458
|
-
unique_count < 20 &&
|
|
459
|
-
rowCount > 0 &&
|
|
460
|
-
unique_count / rowCount <= 0.05;
|
|
461
|
-
|
|
462
|
-
const sampleLimit = isEnumLike ? 100 : 10;
|
|
463
|
-
const sv = await pool.query(
|
|
464
|
-
`SELECT DISTINCT "${col.column_name}" as v FROM ${tableRef} WHERE "${col.column_name}" IS NOT NULL ORDER BY "${col.column_name}" LIMIT ${sampleLimit}`
|
|
465
|
-
);
|
|
466
|
-
sample_values = sv.rows.map((r: { v: unknown }) => String(r.v));
|
|
467
|
-
} catch (colErr) {
|
|
468
|
-
console.warn(` Warning: Could not profile column ${table_name}.${col.column_name}: ${colErr instanceof Error ? colErr.message : String(colErr)}`);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
columns.push({
|
|
473
|
-
name: col.column_name,
|
|
474
|
-
type: col.data_type,
|
|
475
|
-
nullable: col.is_nullable === "YES",
|
|
476
|
-
unique_count,
|
|
477
|
-
null_count,
|
|
478
|
-
sample_values,
|
|
479
|
-
is_primary_key: isPK,
|
|
480
|
-
is_foreign_key: isFK,
|
|
481
|
-
fk_target_table: fkInfo?.to_table ?? null,
|
|
482
|
-
fk_target_column: fkInfo?.to_column ?? null,
|
|
483
|
-
is_enum_like: isEnumLike,
|
|
484
|
-
profiler_notes: [],
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
profiles.push({
|
|
489
|
-
table_name,
|
|
490
|
-
object_type: objectType,
|
|
491
|
-
row_count: rowCount,
|
|
492
|
-
columns,
|
|
493
|
-
primary_key_columns: primaryKeyColumns,
|
|
494
|
-
foreign_keys: foreignKeys,
|
|
495
|
-
inferred_foreign_keys: [],
|
|
496
|
-
profiler_notes: [],
|
|
497
|
-
table_flags: { possibly_abandoned: false, possibly_denormalized: false },
|
|
498
|
-
...(matview_populated !== undefined ? { matview_populated } : {}),
|
|
499
|
-
});
|
|
500
|
-
} catch (err) {
|
|
501
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
502
|
-
console.error(` Warning: Failed to profile ${table_name}: ${msg}`);
|
|
503
|
-
errors.push({ table: table_name, error: msg });
|
|
504
|
-
continue;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Batch-query partition metadata and attach to profiled tables
|
|
509
|
-
const partitionMap = new Map<string, { strategy: "range" | "list" | "hash"; key: string }>();
|
|
510
|
-
try {
|
|
511
|
-
const partResult = await pool.query(
|
|
512
|
-
`SELECT c.relname,
|
|
513
|
-
CASE pt.partstrat WHEN 'r' THEN 'range' WHEN 'l' THEN 'list' WHEN 'h' THEN 'hash' ELSE pt.partstrat END as strategy,
|
|
514
|
-
pg_get_partkeydef(c.oid) as partition_key
|
|
515
|
-
FROM pg_partitioned_table pt
|
|
516
|
-
JOIN pg_class c ON c.oid = pt.partrelid
|
|
517
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
518
|
-
WHERE n.nspname = $1`,
|
|
519
|
-
[schema]
|
|
520
|
-
);
|
|
521
|
-
|
|
522
|
-
for (const r of partResult.rows as { relname: string; strategy: string; partition_key: string }[]) {
|
|
523
|
-
if (r.strategy !== "range" && r.strategy !== "list" && r.strategy !== "hash") {
|
|
524
|
-
console.warn(` Warning: Unrecognized partition strategy '${r.strategy}' for ${r.relname} — skipping`);
|
|
525
|
-
continue;
|
|
526
|
-
}
|
|
527
|
-
partitionMap.set(r.relname, { strategy: r.strategy, key: r.partition_key });
|
|
528
|
-
}
|
|
529
|
-
} catch (partErr) {
|
|
530
|
-
console.warn(` Warning: Could not read partition metadata: ${partErr instanceof Error ? partErr.message : String(partErr)}`);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
const childrenMap = new Map<string, string[]>();
|
|
534
|
-
try {
|
|
535
|
-
const childResult = await pool.query(
|
|
536
|
-
`SELECT p.relname as parent, c.relname as child
|
|
537
|
-
FROM pg_inherits i
|
|
538
|
-
JOIN pg_class c ON c.oid = i.inhrelid
|
|
539
|
-
JOIN pg_class p ON p.oid = i.inhparent
|
|
540
|
-
JOIN pg_namespace n ON n.oid = p.relnamespace
|
|
541
|
-
WHERE n.nspname = $1
|
|
542
|
-
ORDER BY p.relname, c.relname`,
|
|
543
|
-
[schema]
|
|
544
|
-
);
|
|
545
|
-
for (const r of childResult.rows as { parent: string; child: string }[]) {
|
|
546
|
-
const children = childrenMap.get(r.parent) ?? [];
|
|
547
|
-
children.push(r.child);
|
|
548
|
-
childrenMap.set(r.parent, children);
|
|
549
|
-
}
|
|
550
|
-
} catch (childErr) {
|
|
551
|
-
console.warn(` Warning: Could not read partition children: ${childErr instanceof Error ? childErr.message : String(childErr)}`);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
for (const profile of profiles) {
|
|
555
|
-
const partInfo = partitionMap.get(profile.table_name);
|
|
556
|
-
if (partInfo) {
|
|
557
|
-
profile.partition_info = {
|
|
558
|
-
strategy: partInfo.strategy,
|
|
559
|
-
key: partInfo.key,
|
|
560
|
-
children: childrenMap.get(profile.table_name) ?? [],
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
await pool.end();
|
|
566
|
-
|
|
567
|
-
if (errors.length > 0) {
|
|
568
|
-
console.log(`\nWarning: ${errors.length} table(s)/view(s) failed to profile:`);
|
|
569
|
-
for (const e of errors) {
|
|
570
|
-
console.log(` - ${e.table}: ${e.error}`);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
return profiles;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// --- MySQL profiler ---
|
|
578
|
-
|
|
579
|
-
async function queryMySQLPrimaryKeys(
|
|
580
|
-
pool: import("mysql2/promise").Pool,
|
|
581
|
-
tableName: string
|
|
582
|
-
): Promise<string[]> {
|
|
583
|
-
const [rows] = await pool.execute(
|
|
584
|
-
`SELECT COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE
|
|
585
|
-
WHERE TABLE_SCHEMA = DATABASE()
|
|
586
|
-
AND TABLE_NAME = ?
|
|
587
|
-
AND CONSTRAINT_NAME = 'PRIMARY'
|
|
588
|
-
ORDER BY ORDINAL_POSITION`,
|
|
589
|
-
[tableName]
|
|
590
|
-
);
|
|
591
|
-
return (rows as { COLUMN_NAME: string }[]).map((r) => r.COLUMN_NAME);
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
async function queryMySQLForeignKeys(
|
|
595
|
-
pool: import("mysql2/promise").Pool,
|
|
596
|
-
tableName: string
|
|
597
|
-
): Promise<ForeignKey[]> {
|
|
598
|
-
const [rows] = await pool.execute(
|
|
599
|
-
`SELECT COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
|
|
600
|
-
FROM information_schema.KEY_COLUMN_USAGE
|
|
601
|
-
WHERE TABLE_SCHEMA = DATABASE()
|
|
602
|
-
AND TABLE_NAME = ?
|
|
603
|
-
AND REFERENCED_TABLE_NAME IS NOT NULL
|
|
604
|
-
ORDER BY COLUMN_NAME`,
|
|
605
|
-
[tableName]
|
|
606
|
-
);
|
|
607
|
-
return (rows as { COLUMN_NAME: string; REFERENCED_TABLE_NAME: string; REFERENCED_COLUMN_NAME: string }[]).map((r) => ({
|
|
608
|
-
from_column: r.COLUMN_NAME,
|
|
609
|
-
to_table: r.REFERENCED_TABLE_NAME,
|
|
610
|
-
to_column: r.REFERENCED_COLUMN_NAME,
|
|
611
|
-
source: "constraint" as const,
|
|
612
|
-
}));
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
export async function profileMySQL(
|
|
616
|
-
connectionString: string,
|
|
617
|
-
filterTables?: string[],
|
|
618
|
-
prefetchedObjects?: DatabaseObject[]
|
|
619
|
-
): Promise<TableProfile[]> {
|
|
620
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
621
|
-
const mysql = require("mysql2/promise");
|
|
622
|
-
const pool = mysql.createPool({
|
|
623
|
-
uri: connectionString,
|
|
624
|
-
connectionLimit: 3,
|
|
625
|
-
supportBigNumbers: true,
|
|
626
|
-
bigNumberStrings: true,
|
|
627
|
-
});
|
|
628
|
-
const profiles: TableProfile[] = [];
|
|
629
|
-
const errors: { table: string; error: string }[] = [];
|
|
630
|
-
|
|
631
|
-
try {
|
|
632
|
-
let allObjects: DatabaseObject[];
|
|
633
|
-
if (prefetchedObjects) {
|
|
634
|
-
allObjects = prefetchedObjects;
|
|
635
|
-
} else {
|
|
636
|
-
const [tablesRows] = await pool.execute(
|
|
637
|
-
`SELECT TABLE_NAME, TABLE_TYPE FROM information_schema.TABLES
|
|
638
|
-
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE IN ('BASE TABLE', 'VIEW')
|
|
639
|
-
ORDER BY TABLE_NAME`
|
|
640
|
-
);
|
|
641
|
-
allObjects = (tablesRows as { TABLE_NAME: string; TABLE_TYPE: string }[]).map((r) => ({
|
|
642
|
-
name: r.TABLE_NAME,
|
|
643
|
-
type: r.TABLE_TYPE === "VIEW" ? "view" as const : "table" as const,
|
|
644
|
-
}));
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
const objectsToProfile = filterTables
|
|
648
|
-
? allObjects.filter((o) => filterTables.includes(o.name))
|
|
649
|
-
: allObjects;
|
|
650
|
-
|
|
651
|
-
for (const [i, obj] of objectsToProfile.entries()) {
|
|
652
|
-
const table_name = obj.name;
|
|
653
|
-
const objectType = obj.type;
|
|
654
|
-
console.log(` [${i + 1}/${objectsToProfile.length}] Profiling ${table_name}${objectType === "view" ? " [view]" : ""}...`);
|
|
655
|
-
|
|
656
|
-
try {
|
|
657
|
-
const [countRows] = await pool.execute(
|
|
658
|
-
`SELECT COUNT(*) as c FROM \`${table_name}\``
|
|
659
|
-
);
|
|
660
|
-
const rowCount = parseInt(String((countRows as { c: number }[])[0].c), 10);
|
|
661
|
-
|
|
662
|
-
let primaryKeyColumns: string[] = [];
|
|
663
|
-
let foreignKeys: ForeignKey[] = [];
|
|
664
|
-
if (objectType === "table") {
|
|
665
|
-
try {
|
|
666
|
-
primaryKeyColumns = await queryMySQLPrimaryKeys(pool, table_name);
|
|
667
|
-
} catch (pkErr) {
|
|
668
|
-
console.warn(` Warning: Could not read PK constraints for ${table_name}: ${pkErr instanceof Error ? pkErr.message : String(pkErr)}`);
|
|
669
|
-
}
|
|
670
|
-
try {
|
|
671
|
-
foreignKeys = await queryMySQLForeignKeys(pool, table_name);
|
|
672
|
-
} catch (fkErr) {
|
|
673
|
-
console.warn(` Warning: Could not read FK constraints for ${table_name}: ${fkErr instanceof Error ? fkErr.message : String(fkErr)}`);
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
const fkLookup = new Map(
|
|
678
|
-
foreignKeys.map((fk) => [fk.from_column, fk])
|
|
679
|
-
);
|
|
680
|
-
|
|
681
|
-
const [colRows] = await pool.execute(
|
|
682
|
-
`SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_TYPE
|
|
683
|
-
FROM information_schema.COLUMNS
|
|
684
|
-
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?
|
|
685
|
-
ORDER BY ORDINAL_POSITION`,
|
|
686
|
-
[table_name]
|
|
687
|
-
);
|
|
688
|
-
|
|
689
|
-
const columns: ColumnProfile[] = [];
|
|
690
|
-
|
|
691
|
-
for (const col of colRows as { COLUMN_NAME: string; DATA_TYPE: string; IS_NULLABLE: string; COLUMN_TYPE: string }[]) {
|
|
692
|
-
let unique_count: number | null = null;
|
|
693
|
-
let null_count: number | null = null;
|
|
694
|
-
let sample_values: string[] = [];
|
|
695
|
-
let isEnumLike = false;
|
|
696
|
-
|
|
697
|
-
const isPK = primaryKeyColumns.includes(col.COLUMN_NAME);
|
|
698
|
-
const fkInfo = fkLookup.get(col.COLUMN_NAME);
|
|
699
|
-
const isFK = !!fkInfo;
|
|
700
|
-
|
|
701
|
-
try {
|
|
702
|
-
const [uqRows] = await pool.execute(
|
|
703
|
-
`SELECT COUNT(DISTINCT \`${col.COLUMN_NAME}\`) as c FROM \`${table_name}\``
|
|
704
|
-
);
|
|
705
|
-
unique_count = parseInt(String((uqRows as { c: number }[])[0].c), 10);
|
|
706
|
-
|
|
707
|
-
const [ncRows] = await pool.execute(
|
|
708
|
-
`SELECT COUNT(*) as c FROM \`${table_name}\` WHERE \`${col.COLUMN_NAME}\` IS NULL`
|
|
709
|
-
);
|
|
710
|
-
null_count = parseInt(String((ncRows as { c: number }[])[0].c), 10);
|
|
711
|
-
|
|
712
|
-
// Enum-like detection: text/enum/set columns with <20 distinct values and <=5% cardinality
|
|
713
|
-
const dataType = col.DATA_TYPE.toLowerCase();
|
|
714
|
-
const isTextType =
|
|
715
|
-
dataType === "varchar" ||
|
|
716
|
-
dataType === "char" ||
|
|
717
|
-
dataType === "text" ||
|
|
718
|
-
dataType === "tinytext" ||
|
|
719
|
-
dataType === "mediumtext" ||
|
|
720
|
-
dataType === "longtext" ||
|
|
721
|
-
dataType === "enum" ||
|
|
722
|
-
dataType === "set";
|
|
723
|
-
isEnumLike =
|
|
724
|
-
isTextType &&
|
|
725
|
-
unique_count !== null &&
|
|
726
|
-
unique_count < 20 &&
|
|
727
|
-
rowCount > 0 &&
|
|
728
|
-
unique_count / rowCount <= 0.05;
|
|
729
|
-
|
|
730
|
-
const sampleLimit = isEnumLike ? 100 : 10;
|
|
731
|
-
const [svRows] = await pool.execute(
|
|
732
|
-
`SELECT DISTINCT \`${col.COLUMN_NAME}\` as v FROM \`${table_name}\` WHERE \`${col.COLUMN_NAME}\` IS NOT NULL ORDER BY \`${col.COLUMN_NAME}\` LIMIT ${sampleLimit}`
|
|
733
|
-
);
|
|
734
|
-
sample_values = (svRows as { v: unknown }[]).map((r) => String(r.v));
|
|
735
|
-
} catch (colErr) {
|
|
736
|
-
console.warn(` Warning: Could not profile column ${table_name}.${col.COLUMN_NAME}: ${colErr instanceof Error ? colErr.message : String(colErr)}`);
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
columns.push({
|
|
740
|
-
name: col.COLUMN_NAME,
|
|
741
|
-
type: col.DATA_TYPE,
|
|
742
|
-
nullable: col.IS_NULLABLE === "YES",
|
|
743
|
-
unique_count,
|
|
744
|
-
null_count,
|
|
745
|
-
sample_values,
|
|
746
|
-
is_primary_key: isPK,
|
|
747
|
-
is_foreign_key: isFK,
|
|
748
|
-
fk_target_table: fkInfo?.to_table ?? null,
|
|
749
|
-
fk_target_column: fkInfo?.to_column ?? null,
|
|
750
|
-
is_enum_like: isEnumLike,
|
|
751
|
-
profiler_notes: [],
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
profiles.push({
|
|
756
|
-
table_name,
|
|
757
|
-
object_type: objectType,
|
|
758
|
-
row_count: rowCount,
|
|
759
|
-
columns,
|
|
760
|
-
primary_key_columns: primaryKeyColumns,
|
|
761
|
-
foreign_keys: foreignKeys,
|
|
762
|
-
inferred_foreign_keys: [],
|
|
763
|
-
profiler_notes: [],
|
|
764
|
-
table_flags: { possibly_abandoned: false, possibly_denormalized: false },
|
|
765
|
-
});
|
|
766
|
-
} catch (err) {
|
|
767
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
768
|
-
// Fail fast on connection-level errors that will affect all remaining tables
|
|
769
|
-
if (/PROTOCOL_CONNECTION_LOST|ER_SERVER_SHUTDOWN|ER_NET_READ_ERROR|ER_NET_WRITE_ERROR|ECONNRESET|ECONNREFUSED|EHOSTUNREACH|ENOTFOUND|EPIPE|ETIMEDOUT/i.test(msg)) {
|
|
770
|
-
throw new Error(`Fatal database error while profiling ${table_name}: ${msg}`, { cause: err });
|
|
771
|
-
}
|
|
772
|
-
console.error(` Warning: Failed to profile ${table_name}: ${msg}`);
|
|
773
|
-
errors.push({ table: table_name, error: msg });
|
|
774
|
-
continue;
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
} finally {
|
|
779
|
-
await pool.end().catch((err: unknown) => {
|
|
780
|
-
console.warn(`[atlas] MySQL pool cleanup warning: ${err instanceof Error ? err.message : String(err)}`);
|
|
781
|
-
});
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
if (errors.length > 0) {
|
|
785
|
-
console.log(`\nWarning: ${errors.length} table(s)/view(s) failed to profile:`);
|
|
786
|
-
for (const e of errors) {
|
|
787
|
-
console.log(` - ${e.table}: ${e.error}`);
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
188
|
|
|
791
|
-
return profiles;
|
|
792
|
-
}
|
|
793
189
|
|
|
794
190
|
// --- ClickHouse profiler ---
|
|
795
191
|
|
|
796
|
-
|
|
797
|
-
type ClickHouseClient = { query: (opts: { query: string; format: string }) => Promise<{ json: () => Promise<any> }>; close: () => Promise<void> };
|
|
192
|
+
type ClickHouseClient = { query: (opts: { query: string; format: string }) => Promise<{ json: () => Promise<{ data: Record<string, unknown>[] }> }>; close: () => Promise<void> };
|
|
798
193
|
|
|
799
194
|
/** Run a single query against ClickHouse and return rows. */
|
|
800
195
|
async function clickhouseQuery<T = Record<string, unknown>>(
|
|
@@ -816,7 +211,7 @@ function rewriteClickHouseUrl(url: string): string {
|
|
|
816
211
|
return url.replace(/^clickhouses:\/\//, "https://").replace(/^clickhouse:\/\//, "http://");
|
|
817
212
|
}
|
|
818
213
|
|
|
819
|
-
|
|
214
|
+
async function listClickHouseObjects(connectionString: string): Promise<DatabaseObject[]> {
|
|
820
215
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
821
216
|
const { createClient } = require("@clickhouse/client");
|
|
822
217
|
const client = createClient({ url: rewriteClickHouseUrl(connectionString) });
|
|
@@ -862,17 +257,18 @@ function mapClickHouseType(chType: string): string {
|
|
|
862
257
|
return "string";
|
|
863
258
|
}
|
|
864
259
|
|
|
865
|
-
|
|
260
|
+
async function profileClickHouse(
|
|
866
261
|
connectionString: string,
|
|
867
262
|
filterTables?: string[],
|
|
868
|
-
prefetchedObjects?: DatabaseObject[]
|
|
869
|
-
|
|
263
|
+
prefetchedObjects?: DatabaseObject[],
|
|
264
|
+
progress?: ProfileProgressCallbacks
|
|
265
|
+
): Promise<ProfilingResult> {
|
|
870
266
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
871
267
|
const { createClient } = require("@clickhouse/client");
|
|
872
268
|
const client = createClient({ url: rewriteClickHouseUrl(connectionString) });
|
|
873
269
|
|
|
874
270
|
const profiles: TableProfile[] = [];
|
|
875
|
-
const errors:
|
|
271
|
+
const errors: ProfileError[] = [];
|
|
876
272
|
|
|
877
273
|
try {
|
|
878
274
|
let allObjects: DatabaseObject[];
|
|
@@ -896,11 +292,18 @@ export async function profileClickHouse(
|
|
|
896
292
|
? allObjects.filter((o) => filterTables.includes(o.name))
|
|
897
293
|
: allObjects;
|
|
898
294
|
|
|
295
|
+
progress?.onStart(objectsToProfile.length);
|
|
296
|
+
|
|
899
297
|
for (const [i, obj] of objectsToProfile.entries()) {
|
|
900
298
|
const table_name = obj.name;
|
|
901
299
|
const objectType = obj.type;
|
|
902
300
|
const safeTable = table_name.replace(/'/g, "''");
|
|
903
|
-
|
|
301
|
+
const objectLabel = objectType === "view" ? " [view]" : "";
|
|
302
|
+
if (progress) {
|
|
303
|
+
progress.onTableStart(table_name + objectLabel, i, objectsToProfile.length);
|
|
304
|
+
} else {
|
|
305
|
+
console.log(` [${i + 1}/${objectsToProfile.length}] Profiling ${table_name}${objectLabel}...`);
|
|
306
|
+
}
|
|
904
307
|
|
|
905
308
|
try {
|
|
906
309
|
const countRows = await clickhouseQuery<{ c: string }>(
|
|
@@ -916,6 +319,7 @@ export async function profileClickHouse(
|
|
|
916
319
|
try {
|
|
917
320
|
primaryKeyColumns = await queryClickHousePrimaryKeys(client, table_name);
|
|
918
321
|
} catch (pkErr) {
|
|
322
|
+
if (isFatalConnectionError(pkErr)) throw pkErr;
|
|
919
323
|
console.warn(` Warning: Could not read PK columns for ${table_name}: ${pkErr instanceof Error ? pkErr.message : String(pkErr)}`);
|
|
920
324
|
}
|
|
921
325
|
}
|
|
@@ -974,6 +378,7 @@ export async function profileClickHouse(
|
|
|
974
378
|
);
|
|
975
379
|
sample_values = svRows.map((r) => String(r.v));
|
|
976
380
|
} catch (colErr) {
|
|
381
|
+
if (isFatalConnectionError(colErr)) throw colErr;
|
|
977
382
|
console.warn(` Warning: Could not profile column ${table_name}.${col.name}: ${colErr instanceof Error ? colErr.message : String(colErr)}`);
|
|
978
383
|
}
|
|
979
384
|
|
|
@@ -1004,12 +409,18 @@ export async function profileClickHouse(
|
|
|
1004
409
|
profiler_notes: [],
|
|
1005
410
|
table_flags: { possibly_abandoned: false, possibly_denormalized: false },
|
|
1006
411
|
});
|
|
412
|
+
progress?.onTableDone(table_name, i, objectsToProfile.length);
|
|
1007
413
|
} catch (err) {
|
|
1008
414
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1009
|
-
|
|
415
|
+
// Fail fast on connection-level errors that will affect all remaining tables
|
|
416
|
+
if (isFatalConnectionError(err)) {
|
|
1010
417
|
throw new Error(`Fatal database error while profiling ${table_name}: ${msg}`, { cause: err });
|
|
1011
418
|
}
|
|
1012
|
-
|
|
419
|
+
if (progress) {
|
|
420
|
+
progress.onTableError(table_name, msg, i, objectsToProfile.length);
|
|
421
|
+
} else {
|
|
422
|
+
console.error(` Warning: Failed to profile ${table_name}: ${msg}`);
|
|
423
|
+
}
|
|
1013
424
|
errors.push({ table: table_name, error: msg });
|
|
1014
425
|
continue;
|
|
1015
426
|
}
|
|
@@ -1020,14 +431,7 @@ export async function profileClickHouse(
|
|
|
1020
431
|
});
|
|
1021
432
|
}
|
|
1022
433
|
|
|
1023
|
-
|
|
1024
|
-
console.log(`\nWarning: ${errors.length} table(s)/view(s) failed to profile:`);
|
|
1025
|
-
for (const e of errors) {
|
|
1026
|
-
console.log(` - ${e.table}: ${e.error}`);
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
return profiles;
|
|
434
|
+
return { profiles, errors };
|
|
1031
435
|
}
|
|
1032
436
|
|
|
1033
437
|
// --- Snowflake profiler ---
|
|
@@ -1087,7 +491,7 @@ async function createSnowflakePool(connectionString: string, max = 1) {
|
|
|
1087
491
|
return { pool, opts };
|
|
1088
492
|
}
|
|
1089
493
|
|
|
1090
|
-
|
|
494
|
+
async function listSnowflakeObjects(connectionString: string): Promise<DatabaseObject[]> {
|
|
1091
495
|
const { pool } = await createSnowflakePool(connectionString, 1);
|
|
1092
496
|
|
|
1093
497
|
try {
|
|
@@ -1167,16 +571,17 @@ function mapSnowflakeType(sfType: string): string {
|
|
|
1167
571
|
return "text";
|
|
1168
572
|
}
|
|
1169
573
|
|
|
1170
|
-
|
|
574
|
+
async function profileSnowflake(
|
|
1171
575
|
connectionString: string,
|
|
1172
576
|
filterTables?: string[],
|
|
1173
577
|
prefetchedObjects?: DatabaseObject[],
|
|
1174
|
-
|
|
578
|
+
progress?: ProfileProgressCallbacks
|
|
579
|
+
): Promise<ProfilingResult> {
|
|
1175
580
|
const { pool, opts } = await createSnowflakePool(connectionString, 3);
|
|
1176
581
|
|
|
1177
582
|
|
|
1178
583
|
const profiles: TableProfile[] = [];
|
|
1179
|
-
const errors:
|
|
584
|
+
const errors: ProfileError[] = [];
|
|
1180
585
|
const escId = (name: string) => name.replace(/"/g, '""');
|
|
1181
586
|
|
|
1182
587
|
try {
|
|
@@ -1200,10 +605,17 @@ export async function profileSnowflake(
|
|
|
1200
605
|
? allObjects.filter((o) => filterTables.includes(o.name))
|
|
1201
606
|
: allObjects;
|
|
1202
607
|
|
|
608
|
+
progress?.onStart(objectsToProfile.length);
|
|
609
|
+
|
|
1203
610
|
for (const [i, obj] of objectsToProfile.entries()) {
|
|
1204
611
|
const table_name = obj.name;
|
|
1205
612
|
const objectType = obj.type;
|
|
1206
|
-
|
|
613
|
+
const objectLabel = objectType === "view" ? " [view]" : "";
|
|
614
|
+
if (progress) {
|
|
615
|
+
progress.onTableStart(table_name + objectLabel, i, objectsToProfile.length);
|
|
616
|
+
} else {
|
|
617
|
+
console.log(` [${i + 1}/${objectsToProfile.length}] Profiling ${table_name}${objectLabel}...`);
|
|
618
|
+
}
|
|
1207
619
|
|
|
1208
620
|
try {
|
|
1209
621
|
let primaryKeyColumns: string[] = [];
|
|
@@ -1212,11 +624,13 @@ export async function profileSnowflake(
|
|
|
1212
624
|
try {
|
|
1213
625
|
primaryKeyColumns = await querySnowflakePrimaryKeys(pool, table_name, opts.database, opts.schema);
|
|
1214
626
|
} catch (pkErr) {
|
|
627
|
+
if (isFatalConnectionError(pkErr)) throw pkErr;
|
|
1215
628
|
console.warn(` Warning: Could not read PK constraints for ${table_name}: ${pkErr instanceof Error ? pkErr.message : String(pkErr)}`);
|
|
1216
629
|
}
|
|
1217
630
|
try {
|
|
1218
631
|
foreignKeys = await querySnowflakeForeignKeys(pool, table_name, opts.database, opts.schema);
|
|
1219
632
|
} catch (fkErr) {
|
|
633
|
+
if (isFatalConnectionError(fkErr)) throw fkErr;
|
|
1220
634
|
console.warn(` Warning: Could not read FK constraints for ${table_name}: ${fkErr instanceof Error ? fkErr.message : String(fkErr)}`);
|
|
1221
635
|
}
|
|
1222
636
|
}
|
|
@@ -1253,11 +667,13 @@ export async function profileSnowflake(
|
|
|
1253
667
|
});
|
|
1254
668
|
}
|
|
1255
669
|
} catch (bulkErr) {
|
|
670
|
+
if (isFatalConnectionError(bulkErr)) throw bulkErr;
|
|
1256
671
|
console.warn(` Warning: Bulk stats query failed for ${table_name}, falling back to row count only: ${bulkErr instanceof Error ? bulkErr.message : String(bulkErr)}`);
|
|
1257
672
|
try {
|
|
1258
673
|
const countResult = await snowflakeQuery(pool, `SELECT COUNT(*) as "RC" FROM "${escId(table_name)}"`);
|
|
1259
674
|
rowCount = parseInt(String(countResult.rows[0]?.RC ?? "0"), 10);
|
|
1260
675
|
} catch (countErr) {
|
|
676
|
+
if (isFatalConnectionError(countErr)) throw countErr;
|
|
1261
677
|
console.warn(` Warning: Row count query also failed for ${table_name}: ${countErr instanceof Error ? countErr.message : String(countErr)}`);
|
|
1262
678
|
}
|
|
1263
679
|
}
|
|
@@ -1266,6 +682,7 @@ export async function profileSnowflake(
|
|
|
1266
682
|
const countResult = await snowflakeQuery(pool, `SELECT COUNT(*) as "RC" FROM "${escId(table_name)}"`);
|
|
1267
683
|
rowCount = parseInt(String(countResult.rows[0]?.RC ?? "0"), 10);
|
|
1268
684
|
} catch (countErr) {
|
|
685
|
+
if (isFatalConnectionError(countErr)) throw countErr;
|
|
1269
686
|
console.warn(` Warning: Row count query failed for ${table_name}: ${countErr instanceof Error ? countErr.message : String(countErr)}`);
|
|
1270
687
|
}
|
|
1271
688
|
}
|
|
@@ -1298,6 +715,7 @@ export async function profileSnowflake(
|
|
|
1298
715
|
samplesMap.get(cn)!.push(String(row.V));
|
|
1299
716
|
}
|
|
1300
717
|
} catch (sampleErr) {
|
|
718
|
+
if (isFatalConnectionError(sampleErr)) throw sampleErr;
|
|
1301
719
|
console.warn(` Warning: Batched sample values query failed for ${table_name} (${colMeta.length} columns affected): ${sampleErr instanceof Error ? sampleErr.message : String(sampleErr)}`);
|
|
1302
720
|
}
|
|
1303
721
|
}
|
|
@@ -1335,12 +753,19 @@ export async function profileSnowflake(
|
|
|
1335
753
|
profiler_notes: [],
|
|
1336
754
|
table_flags: { possibly_abandoned: false, possibly_denormalized: false },
|
|
1337
755
|
});
|
|
756
|
+
progress?.onTableDone(table_name, i, objectsToProfile.length);
|
|
1338
757
|
} catch (err) {
|
|
1339
758
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1340
|
-
|
|
759
|
+
// Fail fast on connection-level errors that will affect all remaining tables
|
|
760
|
+
// Snowflake-specific: 390100 = auth token expired, 390114 = auth token invalid, 250001 = connection failure
|
|
761
|
+
if (isFatalConnectionError(err) || /390100|390114|250001/.test(msg)) {
|
|
1341
762
|
throw new Error(`Fatal database error while profiling ${table_name}: ${msg}`, { cause: err });
|
|
1342
763
|
}
|
|
1343
|
-
|
|
764
|
+
if (progress) {
|
|
765
|
+
progress.onTableError(table_name, msg, i, objectsToProfile.length);
|
|
766
|
+
} else {
|
|
767
|
+
console.error(` Warning: Failed to profile ${table_name}: ${msg}`);
|
|
768
|
+
}
|
|
1344
769
|
errors.push({ table: table_name, error: msg });
|
|
1345
770
|
continue;
|
|
1346
771
|
}
|
|
@@ -1354,58 +779,15 @@ export async function profileSnowflake(
|
|
|
1354
779
|
}
|
|
1355
780
|
}
|
|
1356
781
|
|
|
1357
|
-
|
|
1358
|
-
console.log(`\nWarning: ${errors.length} table(s)/view(s) failed to profile:`);
|
|
1359
|
-
for (const e of errors) {
|
|
1360
|
-
console.log(` - ${e.table}: ${e.error}`);
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
return profiles;
|
|
782
|
+
return { profiles, errors };
|
|
1365
783
|
}
|
|
1366
784
|
|
|
1367
785
|
// --- Salesforce profiler ---
|
|
1368
786
|
|
|
1369
|
-
|
|
1370
|
-
function mapSalesforceFieldType(sfType: string): string {
|
|
1371
|
-
const lower = sfType.toLowerCase();
|
|
1372
|
-
switch (lower) {
|
|
1373
|
-
case "int":
|
|
1374
|
-
case "long":
|
|
1375
|
-
return "integer";
|
|
1376
|
-
case "double":
|
|
1377
|
-
case "currency":
|
|
1378
|
-
case "percent":
|
|
1379
|
-
return "real";
|
|
1380
|
-
case "boolean":
|
|
1381
|
-
return "boolean";
|
|
1382
|
-
case "date":
|
|
1383
|
-
case "datetime":
|
|
1384
|
-
case "time":
|
|
1385
|
-
return "date";
|
|
1386
|
-
case "string":
|
|
1387
|
-
case "id":
|
|
1388
|
-
case "reference":
|
|
1389
|
-
case "textarea":
|
|
1390
|
-
case "url":
|
|
1391
|
-
case "email":
|
|
1392
|
-
case "phone":
|
|
1393
|
-
case "picklist":
|
|
1394
|
-
case "multipicklist":
|
|
1395
|
-
case "combobox":
|
|
1396
|
-
case "encryptedstring":
|
|
1397
|
-
case "base64":
|
|
1398
|
-
return "text";
|
|
1399
|
-
default:
|
|
1400
|
-
return "text";
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
export async function listSalesforceObjects(connectionString: string): Promise<DatabaseObject[]> {
|
|
787
|
+
async function listSalesforceObjects(connectionString: string): Promise<DatabaseObject[]> {
|
|
1405
788
|
const { parseSalesforceURL, createSalesforceConnection } = await import("../../../plugins/salesforce/src/connection");
|
|
1406
789
|
const config = parseSalesforceURL(connectionString);
|
|
1407
|
-
|
|
1408
|
-
const source: any = createSalesforceConnection(config);
|
|
790
|
+
const source = createSalesforceConnection(config);
|
|
1409
791
|
try {
|
|
1410
792
|
const objects = await source.listObjects();
|
|
1411
793
|
return objects.map((obj: { name: string }) => ({
|
|
@@ -1417,18 +799,18 @@ export async function listSalesforceObjects(connectionString: string): Promise<D
|
|
|
1417
799
|
}
|
|
1418
800
|
}
|
|
1419
801
|
|
|
1420
|
-
|
|
802
|
+
async function profileSalesforce(
|
|
1421
803
|
connectionString: string,
|
|
1422
804
|
filterTables?: string[],
|
|
1423
805
|
prefetchedObjects?: DatabaseObject[],
|
|
1424
|
-
|
|
806
|
+
progress?: ProfileProgressCallbacks
|
|
807
|
+
): Promise<ProfilingResult> {
|
|
1425
808
|
const { parseSalesforceURL, createSalesforceConnection } = await import("../../../plugins/salesforce/src/connection");
|
|
1426
809
|
const config = parseSalesforceURL(connectionString);
|
|
1427
|
-
|
|
1428
|
-
const source: any = createSalesforceConnection(config);
|
|
810
|
+
const source = createSalesforceConnection(config);
|
|
1429
811
|
|
|
1430
812
|
const profiles: TableProfile[] = [];
|
|
1431
|
-
const errors:
|
|
813
|
+
const errors: ProfileError[] = [];
|
|
1432
814
|
|
|
1433
815
|
try {
|
|
1434
816
|
let allObjects: DatabaseObject[];
|
|
@@ -1446,9 +828,15 @@ export async function profileSalesforce(
|
|
|
1446
828
|
? allObjects.filter((o) => filterTables.includes(o.name))
|
|
1447
829
|
: allObjects;
|
|
1448
830
|
|
|
831
|
+
progress?.onStart(objectsToProfile.length);
|
|
832
|
+
|
|
1449
833
|
for (const [i, obj] of objectsToProfile.entries()) {
|
|
1450
834
|
const objectName = obj.name;
|
|
1451
|
-
|
|
835
|
+
if (progress) {
|
|
836
|
+
progress.onTableStart(objectName, i, objectsToProfile.length);
|
|
837
|
+
} else {
|
|
838
|
+
console.log(` [${i + 1}/${objectsToProfile.length}] Profiling ${objectName}...`);
|
|
839
|
+
}
|
|
1452
840
|
|
|
1453
841
|
try {
|
|
1454
842
|
const desc = await source.describe(objectName);
|
|
@@ -1464,14 +852,14 @@ export async function profileSalesforce(
|
|
|
1464
852
|
rowCount = parseInt(String(countVal ?? "0"), 10);
|
|
1465
853
|
}
|
|
1466
854
|
} catch (countErr) {
|
|
855
|
+
if (isFatalConnectionError(countErr)) throw countErr;
|
|
1467
856
|
console.warn(` Warning: Could not get row count for ${objectName}: ${countErr instanceof Error ? countErr.message : String(countErr)}`);
|
|
1468
857
|
}
|
|
1469
858
|
|
|
1470
859
|
const foreignKeys: ForeignKey[] = [];
|
|
1471
860
|
const primaryKeyColumns: string[] = [];
|
|
1472
861
|
|
|
1473
|
-
|
|
1474
|
-
const columns: ColumnProfile[] = desc.fields.map((field: any) => {
|
|
862
|
+
const columns: ColumnProfile[] = desc.fields.map((field: SObjectField) => {
|
|
1475
863
|
const isPK = field.name === "Id";
|
|
1476
864
|
if (isPK) primaryKeyColumns.push(field.name);
|
|
1477
865
|
|
|
@@ -1489,8 +877,7 @@ export async function profileSalesforce(
|
|
|
1489
877
|
|
|
1490
878
|
// For picklist fields, extract active values as sample_values
|
|
1491
879
|
const sampleValues = isEnumLike
|
|
1492
|
-
|
|
1493
|
-
? field.picklistValues.filter((pv: any) => pv.active).map((pv: any) => pv.value)
|
|
880
|
+
? field.picklistValues.filter((pv) => pv.active).map((pv) => pv.value)
|
|
1494
881
|
: [];
|
|
1495
882
|
|
|
1496
883
|
return {
|
|
@@ -1501,953 +888,57 @@ export async function profileSalesforce(
|
|
|
1501
888
|
null_count: null,
|
|
1502
889
|
sample_values: sampleValues,
|
|
1503
890
|
is_primary_key: isPK,
|
|
1504
|
-
is_foreign_key: isFK,
|
|
1505
|
-
fk_target_table: isFK ? field.referenceTo[0] : null,
|
|
1506
|
-
fk_target_column: isFK ? "Id" : null,
|
|
1507
|
-
is_enum_like: isEnumLike,
|
|
1508
|
-
profiler_notes: [],
|
|
1509
|
-
};
|
|
1510
|
-
});
|
|
1511
|
-
|
|
1512
|
-
profiles.push({
|
|
1513
|
-
table_name: objectName,
|
|
1514
|
-
object_type: "table",
|
|
1515
|
-
row_count: rowCount,
|
|
1516
|
-
columns,
|
|
1517
|
-
primary_key_columns: primaryKeyColumns,
|
|
1518
|
-
foreign_keys: foreignKeys,
|
|
1519
|
-
inferred_foreign_keys: [],
|
|
1520
|
-
profiler_notes: [],
|
|
1521
|
-
table_flags: { possibly_abandoned: false, possibly_denormalized: false },
|
|
1522
|
-
});
|
|
1523
|
-
} catch (err) {
|
|
1524
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1525
|
-
if (/ECONNRESET|ECONNREFUSED|EHOSTUNREACH|ENOTFOUND|EPIPE|ETIMEDOUT/i.test(msg)) {
|
|
1526
|
-
throw new Error(`Fatal Salesforce error while profiling ${objectName}: ${msg}`, { cause: err });
|
|
1527
|
-
}
|
|
1528
|
-
console.error(` Warning: Failed to profile ${objectName}: ${msg}`);
|
|
1529
|
-
errors.push({ table: objectName, error: msg });
|
|
1530
|
-
continue;
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
} finally {
|
|
1534
|
-
await source.close();
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
if (errors.length > 0) {
|
|
1538
|
-
console.log(`\nWarning: ${errors.length} object(s) failed to profile:`);
|
|
1539
|
-
for (const e of errors) {
|
|
1540
|
-
console.log(` - ${e.table}: ${e.error}`);
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
return profiles;
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
// --- Pluralization / singularization (shared by heuristics + YAML generation) ---
|
|
1548
|
-
|
|
1549
|
-
/**
|
|
1550
|
-
* Plural → singular lookup for irregular English words.
|
|
1551
|
-
* Also used by YAML generation (singularize) and heuristics (inferForeignKeys).
|
|
1552
|
-
*/
|
|
1553
|
-
const IRREGULAR_PLURALS: Record<string, string> = {
|
|
1554
|
-
people: "person",
|
|
1555
|
-
children: "child",
|
|
1556
|
-
men: "man",
|
|
1557
|
-
women: "woman",
|
|
1558
|
-
mice: "mouse",
|
|
1559
|
-
data: "datum",
|
|
1560
|
-
criteria: "criterion",
|
|
1561
|
-
analyses: "analysis",
|
|
1562
|
-
};
|
|
1563
|
-
|
|
1564
|
-
/** Derived inverse: singular → plural, built once from IRREGULAR_PLURALS. */
|
|
1565
|
-
const IRREGULAR_SINGULARS_TO_PLURALS: Record<string, string> = Object.fromEntries(
|
|
1566
|
-
Object.entries(IRREGULAR_PLURALS).map(([plural, singular]) => [singular, plural])
|
|
1567
|
-
);
|
|
1568
|
-
|
|
1569
|
-
export function pluralize(word: string): string {
|
|
1570
|
-
const lower = word.toLowerCase();
|
|
1571
|
-
if (IRREGULAR_SINGULARS_TO_PLURALS[lower]) return IRREGULAR_SINGULARS_TO_PLURALS[lower];
|
|
1572
|
-
if (lower.endsWith("y") && !/[aeiou]y$/i.test(lower))
|
|
1573
|
-
return word.slice(0, -1) + "ies";
|
|
1574
|
-
if (lower.endsWith("s") || lower.endsWith("x") || lower.endsWith("z") || lower.endsWith("sh") || lower.endsWith("ch"))
|
|
1575
|
-
return word + "es";
|
|
1576
|
-
return word + "s";
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
export function singularize(word: string): string {
|
|
1580
|
-
const lower = word.toLowerCase();
|
|
1581
|
-
if (IRREGULAR_PLURALS[lower]) return IRREGULAR_PLURALS[lower];
|
|
1582
|
-
if (lower.endsWith("ies")) return word.slice(0, -3) + "y";
|
|
1583
|
-
if (lower.endsWith("ses") || lower.endsWith("xes") || lower.endsWith("zes"))
|
|
1584
|
-
return word.slice(0, -2);
|
|
1585
|
-
if (lower.endsWith("s") && !lower.endsWith("ss") && !lower.endsWith("us") && !lower.endsWith("is")) return word.slice(0, -1);
|
|
1586
|
-
return word;
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
// --- Profiler heuristics (pure functions on TableProfile[]) ---
|
|
1590
|
-
|
|
1591
|
-
/**
|
|
1592
|
-
* For each `*_id` column without an existing FK constraint, try to match
|
|
1593
|
-
* the prefix to a table name (singular or plural). Only infer when the
|
|
1594
|
-
* target table has a PK column named `id`.
|
|
1595
|
-
*/
|
|
1596
|
-
export function inferForeignKeys(profiles: TableProfile[]): void {
|
|
1597
|
-
// Only tables (not views/matviews) can be FK targets — views have no PKs
|
|
1598
|
-
const tableMap = new Map(
|
|
1599
|
-
profiles.filter((p) => !isViewLike(p)).map((p) => [p.table_name, p])
|
|
1600
|
-
);
|
|
1601
|
-
|
|
1602
|
-
for (const profile of profiles) {
|
|
1603
|
-
if (isViewLike(profile)) continue;
|
|
1604
|
-
|
|
1605
|
-
const constrainedCols = new Set(profile.foreign_keys.map((fk) => fk.from_column));
|
|
1606
|
-
|
|
1607
|
-
for (const col of profile.columns) {
|
|
1608
|
-
if (!col.name.endsWith("_id")) continue;
|
|
1609
|
-
if (constrainedCols.has(col.name)) continue;
|
|
1610
|
-
if (col.is_primary_key) continue;
|
|
1611
|
-
|
|
1612
|
-
const prefix = col.name.slice(0, -3); // strip "_id"
|
|
1613
|
-
if (!prefix) continue;
|
|
1614
|
-
|
|
1615
|
-
// Try direct match, plural, singular
|
|
1616
|
-
const candidates = [prefix, pluralize(prefix), singularize(prefix)];
|
|
1617
|
-
let targetTable: TableProfile | undefined;
|
|
1618
|
-
for (const candidate of candidates) {
|
|
1619
|
-
targetTable = tableMap.get(candidate);
|
|
1620
|
-
if (targetTable) break;
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
if (!targetTable) continue;
|
|
1624
|
-
|
|
1625
|
-
// Only infer when target has PK column named "id"
|
|
1626
|
-
const hasPkId = targetTable.primary_key_columns.includes("id");
|
|
1627
|
-
if (!hasPkId) continue;
|
|
1628
|
-
|
|
1629
|
-
const inferredFK: ForeignKey = {
|
|
1630
|
-
from_column: col.name,
|
|
1631
|
-
to_table: targetTable.table_name,
|
|
1632
|
-
to_column: "id",
|
|
1633
|
-
source: "inferred",
|
|
1634
|
-
};
|
|
1635
|
-
|
|
1636
|
-
profile.inferred_foreign_keys.push(inferredFK);
|
|
1637
|
-
|
|
1638
|
-
col.profiler_notes.push(
|
|
1639
|
-
`Likely FK to ${targetTable.table_name}.id (inferred from column name, no constraint exists)`
|
|
1640
|
-
);
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
const ABANDONED_NAME_PATTERNS = [
|
|
1646
|
-
/^old_/,
|
|
1647
|
-
/^temp_/,
|
|
1648
|
-
/^legacy_/,
|
|
1649
|
-
/_legacy$/,
|
|
1650
|
-
/_backup$/,
|
|
1651
|
-
/_archive$/,
|
|
1652
|
-
/_v\d+$/,
|
|
1653
|
-
];
|
|
1654
|
-
|
|
1655
|
-
/**
|
|
1656
|
-
* Flag tables whose names match legacy/temp patterns AND have zero inbound FKs
|
|
1657
|
-
* (both constraint and inferred). Views and matviews are excluded — they cannot be abandoned.
|
|
1658
|
-
*/
|
|
1659
|
-
export function detectAbandonedTables(profiles: TableProfile[]): void {
|
|
1660
|
-
// Build set of tables referenced by any FK (constraint or inferred)
|
|
1661
|
-
const referencedTables = new Set<string>();
|
|
1662
|
-
for (const p of profiles) {
|
|
1663
|
-
for (const fk of p.foreign_keys) referencedTables.add(fk.to_table);
|
|
1664
|
-
for (const fk of p.inferred_foreign_keys) referencedTables.add(fk.to_table);
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
for (const profile of profiles) {
|
|
1668
|
-
if (isViewLike(profile)) continue;
|
|
1669
|
-
|
|
1670
|
-
const nameMatches = ABANDONED_NAME_PATTERNS.some((pat) =>
|
|
1671
|
-
pat.test(profile.table_name)
|
|
1672
|
-
);
|
|
1673
|
-
if (!nameMatches) continue;
|
|
1674
|
-
|
|
1675
|
-
const hasInboundFKs = referencedTables.has(profile.table_name);
|
|
1676
|
-
if (hasInboundFKs) continue;
|
|
1677
|
-
|
|
1678
|
-
profile.table_flags.possibly_abandoned = true;
|
|
1679
|
-
profile.profiler_notes.push(
|
|
1680
|
-
`Possibly abandoned: name matches legacy/temp pattern and no other tables reference it`
|
|
1681
|
-
);
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
/**
|
|
1686
|
-
* For enum-like columns, detect case-inconsistent values
|
|
1687
|
-
* (e.g., 'Technology', 'tech', 'TECHNOLOGY' all map to the same lowercase form).
|
|
1688
|
-
*/
|
|
1689
|
-
export function detectEnumInconsistency(profiles: TableProfile[]): void {
|
|
1690
|
-
for (const profile of profiles) {
|
|
1691
|
-
for (const col of profile.columns) {
|
|
1692
|
-
if (!col.is_enum_like) continue;
|
|
1693
|
-
if (col.sample_values.length === 0) continue;
|
|
1694
|
-
|
|
1695
|
-
// Group by lowercase form
|
|
1696
|
-
const groups = new Map<string, string[]>();
|
|
1697
|
-
for (const val of col.sample_values) {
|
|
1698
|
-
const lower = val.toLowerCase();
|
|
1699
|
-
const existing = groups.get(lower) ?? [];
|
|
1700
|
-
existing.push(val);
|
|
1701
|
-
groups.set(lower, existing);
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
// Find groups with multiple original forms
|
|
1705
|
-
const inconsistencies: string[] = [];
|
|
1706
|
-
for (const [, originals] of groups) {
|
|
1707
|
-
if (originals.length > 1) {
|
|
1708
|
-
inconsistencies.push(originals.join(", "));
|
|
1709
|
-
}
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
if (inconsistencies.length > 0) {
|
|
1713
|
-
col.profiler_notes.push(
|
|
1714
|
-
`Case-inconsistent enum values: [${inconsistencies.join("; ")}]. Consider using LOWER() for grouping`
|
|
1715
|
-
);
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
const DENORMALIZED_NAME_PATTERNS = [
|
|
1722
|
-
/_denormalized$/,
|
|
1723
|
-
/_cache$/,
|
|
1724
|
-
/_summary$/,
|
|
1725
|
-
/_stats$/,
|
|
1726
|
-
/_rollup$/,
|
|
1727
|
-
];
|
|
1728
|
-
|
|
1729
|
-
/**
|
|
1730
|
-
* Flag tables whose names match denormalization patterns. Views and matviews are excluded.
|
|
1731
|
-
*/
|
|
1732
|
-
export function detectDenormalizedTables(profiles: TableProfile[]): void {
|
|
1733
|
-
for (const profile of profiles) {
|
|
1734
|
-
if (isViewLike(profile)) continue;
|
|
1735
|
-
|
|
1736
|
-
const nameMatches = DENORMALIZED_NAME_PATTERNS.some((pat) =>
|
|
1737
|
-
pat.test(profile.table_name)
|
|
1738
|
-
);
|
|
1739
|
-
if (!nameMatches) continue;
|
|
1740
|
-
|
|
1741
|
-
profile.table_flags.possibly_denormalized = true;
|
|
1742
|
-
profile.profiler_notes.push(
|
|
1743
|
-
`Possibly denormalized/materialized table: name matches reporting pattern. Data may duplicate other tables`
|
|
1744
|
-
);
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
/**
|
|
1749
|
-
* Orchestrate all profiler heuristics. Initializes empty arrays/flags,
|
|
1750
|
-
* then runs all detectors in sequence.
|
|
1751
|
-
*/
|
|
1752
|
-
export function analyzeTableProfiles(profiles: TableProfile[]): void {
|
|
1753
|
-
// Reset containers on all profiles (clear any prior run)
|
|
1754
|
-
for (const p of profiles) {
|
|
1755
|
-
p.inferred_foreign_keys = [];
|
|
1756
|
-
p.profiler_notes = [];
|
|
1757
|
-
p.table_flags = { possibly_abandoned: false, possibly_denormalized: false };
|
|
1758
|
-
for (const col of p.columns) {
|
|
1759
|
-
col.profiler_notes = [];
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
inferForeignKeys(profiles);
|
|
1764
|
-
detectAbandonedTables(profiles);
|
|
1765
|
-
detectEnumInconsistency(profiles);
|
|
1766
|
-
detectDenormalizedTables(profiles);
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
// --- Generate YAML from profile ---
|
|
1770
|
-
|
|
1771
|
-
function entityName(tableName: string): string {
|
|
1772
|
-
return tableName
|
|
1773
|
-
.split("_")
|
|
1774
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
1775
|
-
.join("");
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
export function generateEntityYAML(
|
|
1779
|
-
profile: TableProfile,
|
|
1780
|
-
allProfiles: TableProfile[],
|
|
1781
|
-
dbType: DBType,
|
|
1782
|
-
schema: string = "public",
|
|
1783
|
-
source?: string,
|
|
1784
|
-
): string {
|
|
1785
|
-
const name = entityName(profile.table_name);
|
|
1786
|
-
// DuckDB's default schema is "main" — don't qualify with it (same as Postgres "public")
|
|
1787
|
-
const qualifiedTable = schema !== "public" && schema !== "main" ? `${schema}.${profile.table_name}` : profile.table_name;
|
|
1788
|
-
|
|
1789
|
-
// Build dimensions
|
|
1790
|
-
const dimensions: Record<string, unknown>[] = profile.columns.map((col) => {
|
|
1791
|
-
const dim: Record<string, unknown> = {
|
|
1792
|
-
name: col.name,
|
|
1793
|
-
sql: col.name,
|
|
1794
|
-
type: dbType === "salesforce" ? mapSalesforceFieldType(col.type) : mapSQLType(col.type),
|
|
1795
|
-
};
|
|
1796
|
-
|
|
1797
|
-
// Description
|
|
1798
|
-
if (col.is_primary_key) {
|
|
1799
|
-
dim.description = `Primary key`;
|
|
1800
|
-
dim.primary_key = true;
|
|
1801
|
-
} else if (col.is_foreign_key) {
|
|
1802
|
-
dim.description = `Foreign key to ${col.fk_target_table}`;
|
|
1803
|
-
}
|
|
1804
|
-
|
|
1805
|
-
if (col.unique_count !== null) dim.unique_count = col.unique_count;
|
|
1806
|
-
if (col.null_count !== null && col.null_count > 0)
|
|
1807
|
-
dim.null_count = col.null_count;
|
|
1808
|
-
if (col.sample_values.length > 0) {
|
|
1809
|
-
dim.sample_values = col.is_enum_like
|
|
1810
|
-
? col.sample_values
|
|
1811
|
-
: col.sample_values.slice(0, 8);
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
return dim;
|
|
1815
|
-
});
|
|
1816
|
-
|
|
1817
|
-
// Build virtual dimensions — dialect-aware CASE bucketing and date extractions
|
|
1818
|
-
const virtualDims: Record<string, unknown>[] = [];
|
|
1819
|
-
for (const col of profile.columns) {
|
|
1820
|
-
if (col.is_primary_key || col.is_foreign_key) continue;
|
|
1821
|
-
const mappedType = dbType === "salesforce" ? mapSalesforceFieldType(col.type) : mapSQLType(col.type);
|
|
1822
|
-
|
|
1823
|
-
if (mappedType === "number" && !col.name.endsWith("_id") && dbType !== "salesforce") {
|
|
1824
|
-
const label = col.name.replace(/_/g, " ");
|
|
1825
|
-
if (dbType === "mysql") {
|
|
1826
|
-
// MySQL: simple fixed-boundary bucketing (no PERCENTILE_CONT)
|
|
1827
|
-
virtualDims.push({
|
|
1828
|
-
name: `${col.name}_bucket`,
|
|
1829
|
-
sql: `CASE\n WHEN ${col.name} IS NULL THEN 'Unknown'\n WHEN ${col.name} < (SELECT AVG(${col.name}) * 0.5 FROM ${qualifiedTable}) THEN 'Low'\n WHEN ${col.name} < (SELECT AVG(${col.name}) * 1.5 FROM ${qualifiedTable}) THEN 'Medium'\n ELSE 'High'\nEND`,
|
|
1830
|
-
type: "string",
|
|
1831
|
-
description: `${label} bucketed into Low/Medium/High`,
|
|
1832
|
-
virtual: true,
|
|
1833
|
-
sample_values: ["Low", "Medium", "High"],
|
|
1834
|
-
});
|
|
1835
|
-
} else if (dbType === "clickhouse") {
|
|
1836
|
-
// ClickHouse: quantile function for tercile bucketing
|
|
1837
|
-
virtualDims.push({
|
|
1838
|
-
name: `${col.name}_bucket`,
|
|
1839
|
-
sql: `CASE\n WHEN ${col.name} < (SELECT quantile(0.33)(${col.name}) FROM ${qualifiedTable}) THEN 'Low'\n WHEN ${col.name} < (SELECT quantile(0.66)(${col.name}) FROM ${qualifiedTable}) THEN 'Medium'\n ELSE 'High'\nEND`,
|
|
1840
|
-
type: "string",
|
|
1841
|
-
description: `${label} bucketed into Low/Medium/High terciles`,
|
|
1842
|
-
virtual: true,
|
|
1843
|
-
sample_values: ["Low", "Medium", "High"],
|
|
1844
|
-
});
|
|
1845
|
-
} else {
|
|
1846
|
-
virtualDims.push({
|
|
1847
|
-
name: `${col.name}_bucket`,
|
|
1848
|
-
sql: `CASE\n WHEN ${col.name} < (SELECT PERCENTILE_CONT(0.33) WITHIN GROUP (ORDER BY ${col.name}) FROM ${qualifiedTable}) THEN 'Low'\n WHEN ${col.name} < (SELECT PERCENTILE_CONT(0.66) WITHIN GROUP (ORDER BY ${col.name}) FROM ${qualifiedTable}) THEN 'Medium'\n ELSE 'High'\nEND`,
|
|
1849
|
-
type: "string",
|
|
1850
|
-
description: `${label} bucketed into Low/Medium/High terciles`,
|
|
1851
|
-
virtual: true,
|
|
1852
|
-
sample_values: ["Low", "Medium", "High"],
|
|
1853
|
-
});
|
|
1854
|
-
}
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
if (mappedType === "date") {
|
|
1858
|
-
if (dbType === "mysql") {
|
|
1859
|
-
virtualDims.push({
|
|
1860
|
-
name: `${col.name}_year`,
|
|
1861
|
-
sql: `YEAR(${col.name})`,
|
|
1862
|
-
type: "number",
|
|
1863
|
-
description: `Year extracted from ${col.name}`,
|
|
1864
|
-
virtual: true,
|
|
1865
|
-
});
|
|
1866
|
-
virtualDims.push({
|
|
1867
|
-
name: `${col.name}_month`,
|
|
1868
|
-
sql: `DATE_FORMAT(${col.name}, '%Y-%m')`,
|
|
1869
|
-
type: "string",
|
|
1870
|
-
description: `Year-month extracted from ${col.name}`,
|
|
1871
|
-
virtual: true,
|
|
1872
|
-
});
|
|
1873
|
-
} else if (dbType === "clickhouse") {
|
|
1874
|
-
virtualDims.push({
|
|
1875
|
-
name: `${col.name}_year`,
|
|
1876
|
-
sql: `toYear(${col.name})`,
|
|
1877
|
-
type: "number",
|
|
1878
|
-
description: `Year extracted from ${col.name}`,
|
|
1879
|
-
virtual: true,
|
|
1880
|
-
});
|
|
1881
|
-
virtualDims.push({
|
|
1882
|
-
name: `${col.name}_month`,
|
|
1883
|
-
sql: `formatDateTime(${col.name}, '%Y-%m')`,
|
|
1884
|
-
type: "string",
|
|
1885
|
-
description: `Year-month extracted from ${col.name}`,
|
|
1886
|
-
virtual: true,
|
|
1887
|
-
});
|
|
1888
|
-
} else if (dbType === "salesforce") {
|
|
1889
|
-
virtualDims.push({
|
|
1890
|
-
name: `${col.name}_year`,
|
|
1891
|
-
sql: `CALENDAR_YEAR(${col.name})`,
|
|
1892
|
-
type: "number",
|
|
1893
|
-
description: `Year extracted from ${col.name}`,
|
|
1894
|
-
virtual: true,
|
|
1895
|
-
});
|
|
1896
|
-
virtualDims.push({
|
|
1897
|
-
name: `${col.name}_month`,
|
|
1898
|
-
sql: `CALENDAR_MONTH(${col.name})`,
|
|
1899
|
-
type: "number",
|
|
1900
|
-
description: `Month extracted from ${col.name}`,
|
|
1901
|
-
virtual: true,
|
|
1902
|
-
});
|
|
1903
|
-
} else {
|
|
1904
|
-
virtualDims.push({
|
|
1905
|
-
name: `${col.name}_year`,
|
|
1906
|
-
sql: `EXTRACT(YEAR FROM ${col.name})`,
|
|
1907
|
-
type: "number",
|
|
1908
|
-
description: `Year extracted from ${col.name}`,
|
|
1909
|
-
virtual: true,
|
|
1910
|
-
});
|
|
1911
|
-
virtualDims.push({
|
|
1912
|
-
name: `${col.name}_month`,
|
|
1913
|
-
sql: `TO_CHAR(${col.name}, 'YYYY-MM')`,
|
|
1914
|
-
type: "string",
|
|
1915
|
-
description: `Year-month extracted from ${col.name}`,
|
|
1916
|
-
virtual: true,
|
|
1917
|
-
});
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
|
-
// Emit profiler_notes on dimensions
|
|
1923
|
-
for (const dim of dimensions) {
|
|
1924
|
-
const col = profile.columns.find((c) => c.name === dim.name);
|
|
1925
|
-
if (col?.profiler_notes && col.profiler_notes.length > 0) {
|
|
1926
|
-
dim.profiler_notes = col.profiler_notes;
|
|
1927
|
-
}
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
// Build joins from constraint FKs
|
|
1931
|
-
const joins: Record<string, unknown>[] = profile.foreign_keys.map((fk) => ({
|
|
1932
|
-
target_entity: entityName(fk.to_table),
|
|
1933
|
-
relationship: "many_to_one",
|
|
1934
|
-
join_columns: {
|
|
1935
|
-
from: fk.from_column,
|
|
1936
|
-
to: fk.to_column,
|
|
1937
|
-
},
|
|
1938
|
-
description: `Each ${singularize(profile.table_name)} belongs to one ${singularize(fk.to_table)}`,
|
|
1939
|
-
}));
|
|
1940
|
-
|
|
1941
|
-
// Add inferred joins
|
|
1942
|
-
for (const fk of profile.inferred_foreign_keys) {
|
|
1943
|
-
joins.push({
|
|
1944
|
-
target_entity: entityName(fk.to_table),
|
|
1945
|
-
relationship: "many_to_one",
|
|
1946
|
-
join_columns: {
|
|
1947
|
-
from: fk.from_column,
|
|
1948
|
-
to: fk.to_column,
|
|
1949
|
-
},
|
|
1950
|
-
inferred: true,
|
|
1951
|
-
note: `No FK constraint exists — inferred from column name ${fk.from_column}`,
|
|
1952
|
-
description: `Each ${singularize(profile.table_name)} likely belongs to one ${singularize(fk.to_table)}`,
|
|
1953
|
-
});
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
|
-
// Build measures (skip for views/matviews — they are pre-aggregated or derived; measures should reference source tables instead)
|
|
1957
|
-
const measures: Record<string, unknown>[] = [];
|
|
1958
|
-
|
|
1959
|
-
if (!isViewLike(profile)) {
|
|
1960
|
-
// count_distinct on PK
|
|
1961
|
-
const pkCol = profile.columns.find((c) => c.is_primary_key);
|
|
1962
|
-
if (pkCol) {
|
|
1963
|
-
measures.push({
|
|
1964
|
-
name: `${singularize(profile.table_name)}_count`,
|
|
1965
|
-
sql: pkCol.name,
|
|
1966
|
-
type: "count_distinct",
|
|
1967
|
-
});
|
|
1968
|
-
}
|
|
1969
|
-
|
|
1970
|
-
// sum/avg on numeric non-FK non-PK columns
|
|
1971
|
-
for (const col of profile.columns) {
|
|
1972
|
-
if (col.is_primary_key || col.is_foreign_key) continue;
|
|
1973
|
-
if (col.name.endsWith("_id")) continue;
|
|
1974
|
-
const mappedType = mapSQLType(col.type);
|
|
1975
|
-
if (mappedType !== "number") continue;
|
|
1976
|
-
|
|
1977
|
-
measures.push({
|
|
1978
|
-
name: `total_${col.name}`,
|
|
1979
|
-
sql: col.name,
|
|
1980
|
-
type: "sum",
|
|
1981
|
-
description: `Sum of ${col.name.replace(/_/g, " ")}`,
|
|
1982
|
-
});
|
|
1983
|
-
measures.push({
|
|
1984
|
-
name: `avg_${col.name}`,
|
|
1985
|
-
sql: col.name,
|
|
1986
|
-
type: "avg",
|
|
1987
|
-
description: `Average ${col.name.replace(/_/g, " ")}`,
|
|
1988
|
-
});
|
|
1989
|
-
}
|
|
1990
|
-
}
|
|
1991
|
-
|
|
1992
|
-
// Build use_cases
|
|
1993
|
-
const useCases: string[] = [];
|
|
1994
|
-
|
|
1995
|
-
// Note for views
|
|
1996
|
-
if (isView(profile)) {
|
|
1997
|
-
useCases.push(`This is a database view — it may encapsulate complex joins or aggregations. Query it directly rather than recreating its logic`);
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
// Notes for materialized views
|
|
2001
|
-
if (isMatView(profile)) {
|
|
2002
|
-
useCases.push(`WARNING: This is a materialized view — data may be stale. Check with the user about refresh frequency before relying on real-time accuracy`);
|
|
2003
|
-
if (profile.matview_populated === false) {
|
|
2004
|
-
useCases.push(`WARNING: This materialized view has never been refreshed and contains no data`);
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
|
-
// Note for partitioned tables
|
|
2009
|
-
if (profile.partition_info) {
|
|
2010
|
-
useCases.push(`This table is partitioned by ${profile.partition_info.strategy} on (${profile.partition_info.key}). Always include ${profile.partition_info.key} in WHERE clauses for optimal query performance`);
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
|
-
// Prepend warnings for flagged tables
|
|
2014
|
-
if (profile.table_flags.possibly_abandoned) {
|
|
2015
|
-
useCases.push(`WARNING: This table appears to be abandoned/legacy. Verify with the user before querying`);
|
|
2016
|
-
}
|
|
2017
|
-
if (profile.table_flags.possibly_denormalized) {
|
|
2018
|
-
useCases.push(`WARNING: This is a denormalized/materialized table. Data may be stale or duplicate other tables`);
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
|
-
const enumCols = profile.columns.filter((c) => c.is_enum_like);
|
|
2022
|
-
const numericCols = profile.columns.filter(
|
|
2023
|
-
(c) =>
|
|
2024
|
-
mapSQLType(c.type) === "number" && !c.is_primary_key && !c.is_foreign_key && !c.name.endsWith("_id")
|
|
2025
|
-
);
|
|
2026
|
-
const dateCols = profile.columns.filter(
|
|
2027
|
-
(c) => mapSQLType(c.type) === "date"
|
|
2028
|
-
);
|
|
2029
|
-
|
|
2030
|
-
if (enumCols.length > 0)
|
|
2031
|
-
useCases.push(
|
|
2032
|
-
`Use for segmentation analysis by ${enumCols.map((c) => c.name).join(", ")}`
|
|
2033
|
-
);
|
|
2034
|
-
if (numericCols.length > 0)
|
|
2035
|
-
useCases.push(
|
|
2036
|
-
`Use for aggregation and trends on ${numericCols.map((c) => c.name).join(", ")}`
|
|
2037
|
-
);
|
|
2038
|
-
if (dateCols.length > 0)
|
|
2039
|
-
useCases.push(`Use for time-series analysis using ${dateCols.map((c) => c.name).join(", ")}`);
|
|
2040
|
-
|
|
2041
|
-
// Combined FK list for use_cases
|
|
2042
|
-
const allFKs = [...profile.foreign_keys, ...profile.inferred_foreign_keys];
|
|
2043
|
-
if (joins.length > 0) {
|
|
2044
|
-
const targets = allFKs.map((fk) => fk.to_table);
|
|
2045
|
-
const uniqueTargets = [...new Set(targets)];
|
|
2046
|
-
useCases.push(
|
|
2047
|
-
`Join with ${uniqueTargets.join(", ")} for cross-entity analysis`
|
|
2048
|
-
);
|
|
2049
|
-
}
|
|
2050
|
-
// Add "avoid" guidance for related tables (constraint + inferred)
|
|
2051
|
-
const tablesPointingHere = allProfiles.filter((p) =>
|
|
2052
|
-
[...p.foreign_keys, ...p.inferred_foreign_keys].some((fk) => fk.to_table === profile.table_name)
|
|
2053
|
-
);
|
|
2054
|
-
if (tablesPointingHere.length > 0) {
|
|
2055
|
-
useCases.push(
|
|
2056
|
-
`Avoid for row-level ${tablesPointingHere.map((p) => p.table_name).join("/")} queries — use those entities directly`
|
|
2057
|
-
);
|
|
2058
|
-
}
|
|
2059
|
-
if (useCases.length === 0) {
|
|
2060
|
-
useCases.push(`Use for querying ${profile.table_name} data`);
|
|
2061
|
-
}
|
|
2062
|
-
|
|
2063
|
-
// Build query patterns (skip for views/matviews — the view IS the pattern)
|
|
2064
|
-
const queryPatterns: Record<string, unknown>[] = [];
|
|
2065
|
-
|
|
2066
|
-
if (!isViewLike(profile)) {
|
|
2067
|
-
// Pattern: count by enum column
|
|
2068
|
-
for (const col of enumCols.slice(0, 2)) {
|
|
2069
|
-
queryPatterns.push({
|
|
2070
|
-
description: `${entityName(profile.table_name)} by ${col.name}`,
|
|
2071
|
-
sql: `SELECT ${col.name}, COUNT(*) as count\nFROM ${qualifiedTable}\nGROUP BY ${col.name}\nORDER BY count DESC`,
|
|
2072
|
-
});
|
|
2073
|
-
}
|
|
2074
|
-
|
|
2075
|
-
// Pattern: aggregate numeric by enum
|
|
2076
|
-
if (numericCols.length > 0 && enumCols.length > 0) {
|
|
2077
|
-
const numCol = numericCols[0];
|
|
2078
|
-
const enumCol = enumCols[0];
|
|
2079
|
-
queryPatterns.push({
|
|
2080
|
-
description: `Total ${numCol.name} by ${enumCol.name}`,
|
|
2081
|
-
sql: `SELECT ${enumCol.name}, SUM(${numCol.name}) as total_${numCol.name}, COUNT(*) as count\nFROM ${qualifiedTable}\nGROUP BY ${enumCol.name}\nORDER BY total_${numCol.name} DESC`,
|
|
2082
|
-
});
|
|
2083
|
-
}
|
|
2084
|
-
}
|
|
2085
|
-
|
|
2086
|
-
// Build description with optional suffix for flagged tables
|
|
2087
|
-
const profileIsViewLike = isViewLike(profile);
|
|
2088
|
-
const profileIsMatView = isMatView(profile);
|
|
2089
|
-
let description: string;
|
|
2090
|
-
if (profileIsMatView) {
|
|
2091
|
-
description = `Materialized view: ${profile.table_name} (${profile.row_count.toLocaleString()} rows). Contains ${profile.columns.length} columns.`;
|
|
2092
|
-
} else if (isView(profile)) {
|
|
2093
|
-
description = `Database view: ${profile.table_name} (${profile.row_count.toLocaleString()} rows). Contains ${profile.columns.length} columns.`;
|
|
2094
|
-
} else {
|
|
2095
|
-
description = `Auto-profiled schema for ${profile.table_name} (${profile.row_count.toLocaleString()} rows). Contains ${profile.columns.length} columns${allFKs.length > 0 ? `, linked to ${[...new Set(allFKs.map((fk) => fk.to_table))].join(", ")}` : ""}.`;
|
|
2096
|
-
}
|
|
2097
|
-
if (profile.table_flags.possibly_abandoned) {
|
|
2098
|
-
description += ` POSSIBLY ABANDONED — name matches legacy/temp pattern and no tables reference it.`;
|
|
2099
|
-
}
|
|
2100
|
-
if (profile.table_flags.possibly_denormalized) {
|
|
2101
|
-
description += ` DENORMALIZED — data may duplicate other tables.`;
|
|
2102
|
-
}
|
|
2103
|
-
|
|
2104
|
-
// Determine entity type
|
|
2105
|
-
let entityType: string;
|
|
2106
|
-
if (profileIsMatView) {
|
|
2107
|
-
entityType = "materialized_view";
|
|
2108
|
-
} else if (isView(profile)) {
|
|
2109
|
-
entityType = "view";
|
|
2110
|
-
} else {
|
|
2111
|
-
entityType = "fact_table";
|
|
2112
|
-
}
|
|
2113
|
-
|
|
2114
|
-
// Assemble entity
|
|
2115
|
-
const entity: Record<string, unknown> = {
|
|
2116
|
-
name,
|
|
2117
|
-
type: entityType,
|
|
2118
|
-
table: qualifiedTable,
|
|
2119
|
-
...(source ? { connection: source } : {}),
|
|
2120
|
-
grain: profileIsMatView
|
|
2121
|
-
? `one row per result from ${profile.table_name} materialized view`
|
|
2122
|
-
: profileIsViewLike
|
|
2123
|
-
? `one row per result from ${profile.table_name} view`
|
|
2124
|
-
: `one row per ${singularize(profile.table_name).replace(/_/g, " ")} record`,
|
|
2125
|
-
description,
|
|
2126
|
-
dimensions: [...dimensions, ...virtualDims],
|
|
2127
|
-
};
|
|
2128
|
-
|
|
2129
|
-
// Partition metadata
|
|
2130
|
-
if (profile.partition_info) {
|
|
2131
|
-
entity.partitioned = true;
|
|
2132
|
-
entity.partition_strategy = profile.partition_info.strategy;
|
|
2133
|
-
entity.partition_key = profile.partition_info.key;
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2136
|
-
if (measures.length > 0) entity.measures = measures;
|
|
2137
|
-
if (joins.length > 0) entity.joins = joins;
|
|
2138
|
-
entity.use_cases = useCases;
|
|
2139
|
-
if (queryPatterns.length > 0) entity.query_patterns = queryPatterns;
|
|
2140
|
-
|
|
2141
|
-
// Emit table-level profiler notes
|
|
2142
|
-
if (profile.profiler_notes.length > 0) {
|
|
2143
|
-
entity.profiler_notes = profile.profiler_notes;
|
|
2144
|
-
}
|
|
2145
|
-
|
|
2146
|
-
return yaml.dump(entity, { lineWidth: 120, noRefs: true });
|
|
2147
|
-
}
|
|
2148
|
-
|
|
2149
|
-
export function generateCatalogYAML(profiles: TableProfile[]): string {
|
|
2150
|
-
const catalog: Record<string, unknown> = {
|
|
2151
|
-
version: "1.0",
|
|
2152
|
-
entities: profiles.map((p) => {
|
|
2153
|
-
const enumCols = p.columns.filter((c) => c.is_enum_like);
|
|
2154
|
-
const numericCols = p.columns.filter(
|
|
2155
|
-
(c) =>
|
|
2156
|
-
mapSQLType(c.type) === "number" && !c.is_primary_key && !c.is_foreign_key && !c.name.endsWith("_id")
|
|
2157
|
-
);
|
|
2158
|
-
|
|
2159
|
-
// Generate use_for from table characteristics
|
|
2160
|
-
const useFor: string[] = [];
|
|
2161
|
-
if (enumCols.length > 0) {
|
|
2162
|
-
useFor.push(
|
|
2163
|
-
`Segmentation by ${enumCols.map((c) => c.name).join(", ")}`
|
|
2164
|
-
);
|
|
2165
|
-
}
|
|
2166
|
-
if (numericCols.length > 0) {
|
|
2167
|
-
useFor.push(
|
|
2168
|
-
`Aggregation on ${numericCols.map((c) => c.name).join(", ")}`
|
|
2169
|
-
);
|
|
2170
|
-
}
|
|
2171
|
-
const allFKs = [...p.foreign_keys, ...p.inferred_foreign_keys];
|
|
2172
|
-
if (allFKs.length > 0) {
|
|
2173
|
-
useFor.push(
|
|
2174
|
-
`Cross-entity analysis via ${[...new Set(allFKs.map((fk) => fk.to_table))].join(", ")}`
|
|
2175
|
-
);
|
|
2176
|
-
}
|
|
2177
|
-
if (useFor.length === 0) {
|
|
2178
|
-
useFor.push(`General queries on ${p.table_name}`);
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
|
-
// Generate common_questions from column types
|
|
2182
|
-
const questions: string[] = [];
|
|
2183
|
-
for (const col of enumCols.slice(0, 2)) {
|
|
2184
|
-
questions.push(
|
|
2185
|
-
`How many ${p.table_name} by ${col.name}?`
|
|
2186
|
-
);
|
|
2187
|
-
}
|
|
2188
|
-
if (numericCols.length > 0) {
|
|
2189
|
-
questions.push(
|
|
2190
|
-
`What is the average ${numericCols[0].name} across ${p.table_name}?`
|
|
2191
|
-
);
|
|
2192
|
-
}
|
|
2193
|
-
if (allFKs.length > 0) {
|
|
2194
|
-
const fk = allFKs[0];
|
|
2195
|
-
questions.push(
|
|
2196
|
-
`How are ${p.table_name} distributed across ${fk.to_table}?`
|
|
2197
|
-
);
|
|
2198
|
-
}
|
|
2199
|
-
if (questions.length === 0) {
|
|
2200
|
-
questions.push(`What data is in ${p.table_name}?`);
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
|
-
const entryIsMatView = isMatView(p);
|
|
2204
|
-
const entryIsViewLike = isViewLike(p);
|
|
2205
|
-
|
|
2206
|
-
let catalogDesc: string;
|
|
2207
|
-
if (entryIsMatView) {
|
|
2208
|
-
catalogDesc = `${p.table_name} [materialized view] (${p.row_count.toLocaleString()} rows, ${p.columns.length} columns)`;
|
|
2209
|
-
} else if (isView(p)) {
|
|
2210
|
-
catalogDesc = `${p.table_name} [view] (${p.row_count.toLocaleString()} rows, ${p.columns.length} columns)`;
|
|
2211
|
-
} else {
|
|
2212
|
-
catalogDesc = `${p.table_name} (${p.row_count.toLocaleString()} rows, ${p.columns.length} columns)`;
|
|
2213
|
-
}
|
|
2214
|
-
if (p.partition_info) {
|
|
2215
|
-
catalogDesc += ` [partitioned by ${p.partition_info.strategy}]`;
|
|
2216
|
-
}
|
|
2217
|
-
|
|
2218
|
-
return {
|
|
2219
|
-
name: entityName(p.table_name),
|
|
2220
|
-
file: `entities/${p.table_name}.yml`,
|
|
2221
|
-
grain: entryIsMatView
|
|
2222
|
-
? `one row per result from ${p.table_name} materialized view`
|
|
2223
|
-
: entryIsViewLike
|
|
2224
|
-
? `one row per result from ${p.table_name} view`
|
|
2225
|
-
: `one row per ${singularize(p.table_name).replace(/_/g, " ")} record`,
|
|
2226
|
-
description: catalogDesc,
|
|
2227
|
-
use_for: useFor,
|
|
2228
|
-
common_questions: questions,
|
|
2229
|
-
};
|
|
2230
|
-
}),
|
|
2231
|
-
glossary: "glossary.yml",
|
|
2232
|
-
};
|
|
2233
|
-
|
|
2234
|
-
// Add metrics section if we'll be generating metric files (exclude views/matviews)
|
|
2235
|
-
const tablesWithNumericCols = profiles.filter((p) =>
|
|
2236
|
-
!isViewLike(p) &&
|
|
2237
|
-
p.columns.some(
|
|
2238
|
-
(c) =>
|
|
2239
|
-
mapSQLType(c.type) === "number" && !c.is_primary_key && !c.is_foreign_key && !c.name.endsWith("_id")
|
|
2240
|
-
)
|
|
2241
|
-
);
|
|
2242
|
-
if (tablesWithNumericCols.length > 0) {
|
|
2243
|
-
catalog.metrics = tablesWithNumericCols.map((p) => ({
|
|
2244
|
-
file: `metrics/${p.table_name}.yml`,
|
|
2245
|
-
description: `Auto-generated metrics for ${p.table_name}`,
|
|
2246
|
-
}));
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
|
-
// Add tech_debt section for flagged tables
|
|
2250
|
-
const flaggedTables: { table: string; issues: string[] }[] = [];
|
|
2251
|
-
for (const p of profiles) {
|
|
2252
|
-
const issues: string[] = [];
|
|
2253
|
-
if (p.table_flags.possibly_abandoned) issues.push("possibly_abandoned");
|
|
2254
|
-
if (p.table_flags.possibly_denormalized) issues.push("possibly_denormalized");
|
|
2255
|
-
if (p.inferred_foreign_keys.length > 0) issues.push("missing_fk_constraints");
|
|
2256
|
-
const hasEnumIssues = p.columns.some((c) =>
|
|
2257
|
-
c.profiler_notes.some((n) => n.startsWith("Case-inconsistent"))
|
|
2258
|
-
);
|
|
2259
|
-
if (hasEnumIssues) issues.push("inconsistent_enums");
|
|
2260
|
-
if (issues.length > 0) flaggedTables.push({ table: p.table_name, issues });
|
|
2261
|
-
}
|
|
2262
|
-
if (flaggedTables.length > 0) {
|
|
2263
|
-
catalog.tech_debt = flaggedTables;
|
|
2264
|
-
}
|
|
2265
|
-
|
|
2266
|
-
return yaml.dump(catalog, { lineWidth: 120, noRefs: true });
|
|
2267
|
-
}
|
|
2268
|
-
|
|
2269
|
-
export function generateMetricYAML(profile: TableProfile, schema: string = "public"): string | null {
|
|
2270
|
-
if (isViewLike(profile)) return null;
|
|
2271
|
-
|
|
2272
|
-
const numericCols = profile.columns.filter(
|
|
2273
|
-
(c) =>
|
|
2274
|
-
mapSQLType(c.type) === "number" &&
|
|
2275
|
-
!c.is_primary_key &&
|
|
2276
|
-
!c.is_foreign_key &&
|
|
2277
|
-
!c.name.endsWith("_id")
|
|
2278
|
-
);
|
|
2279
|
-
|
|
2280
|
-
if (numericCols.length === 0) return null;
|
|
2281
|
-
|
|
2282
|
-
const pkCol = profile.columns.find((c) => c.is_primary_key);
|
|
2283
|
-
const enumCols = profile.columns.filter((c) => c.is_enum_like);
|
|
2284
|
-
const qualifiedTable = schema !== "public" ? `${schema}.${profile.table_name}` : profile.table_name;
|
|
2285
|
-
|
|
2286
|
-
const metrics: Record<string, unknown>[] = [];
|
|
2287
|
-
|
|
2288
|
-
// Count metric
|
|
2289
|
-
if (pkCol) {
|
|
2290
|
-
metrics.push({
|
|
2291
|
-
id: `${profile.table_name}_count`,
|
|
2292
|
-
label: `Total ${entityName(profile.table_name)}`,
|
|
2293
|
-
description: `Count of distinct ${profile.table_name} records.`,
|
|
2294
|
-
type: "atomic",
|
|
2295
|
-
sql: `SELECT COUNT(DISTINCT ${pkCol.name}) as count\nFROM ${qualifiedTable}`,
|
|
2296
|
-
aggregation: "count_distinct",
|
|
2297
|
-
});
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
// Sum and average for each numeric column
|
|
2301
|
-
for (const col of numericCols) {
|
|
2302
|
-
metrics.push({
|
|
2303
|
-
id: `total_${col.name}`,
|
|
2304
|
-
label: `Total ${col.name.replace(/_/g, " ")}`,
|
|
2305
|
-
description: `Sum of ${col.name} across all ${profile.table_name}.`,
|
|
2306
|
-
type: "atomic",
|
|
2307
|
-
source: {
|
|
2308
|
-
entity: entityName(profile.table_name),
|
|
2309
|
-
measure: `total_${col.name}`,
|
|
2310
|
-
},
|
|
2311
|
-
sql: `SELECT SUM(${col.name}) as total_${col.name}\nFROM ${qualifiedTable}`,
|
|
2312
|
-
aggregation: "sum",
|
|
2313
|
-
objective: "maximize",
|
|
2314
|
-
});
|
|
2315
|
-
|
|
2316
|
-
metrics.push({
|
|
2317
|
-
id: `avg_${col.name}`,
|
|
2318
|
-
label: `Average ${col.name.replace(/_/g, " ")}`,
|
|
2319
|
-
description: `Average ${col.name} per ${singularize(profile.table_name)}.`,
|
|
2320
|
-
type: "atomic",
|
|
2321
|
-
sql: `SELECT AVG(${col.name}) as avg_${col.name}\nFROM ${qualifiedTable}`,
|
|
2322
|
-
aggregation: "avg",
|
|
2323
|
-
});
|
|
2324
|
-
|
|
2325
|
-
// Breakdown by first enum column if available
|
|
2326
|
-
if (enumCols.length > 0) {
|
|
2327
|
-
const enumCol = enumCols[0];
|
|
2328
|
-
metrics.push({
|
|
2329
|
-
id: `${col.name}_by_${enumCol.name}`,
|
|
2330
|
-
label: `${col.name.replace(/_/g, " ")} by ${enumCol.name}`,
|
|
2331
|
-
description: `${col.name} broken down by ${enumCol.name}.`,
|
|
2332
|
-
type: "atomic",
|
|
2333
|
-
sql: `SELECT ${enumCol.name}, SUM(${col.name}) as total_${col.name}, AVG(${col.name}) as avg_${col.name}, COUNT(*) as count\nFROM ${qualifiedTable}\nGROUP BY ${enumCol.name}\nORDER BY total_${col.name} DESC`,
|
|
2334
|
-
});
|
|
2335
|
-
}
|
|
2336
|
-
}
|
|
2337
|
-
|
|
2338
|
-
return yaml.dump({ metrics }, { lineWidth: 120, noRefs: true });
|
|
2339
|
-
}
|
|
2340
|
-
|
|
2341
|
-
export function generateGlossaryYAML(profiles: TableProfile[]): string {
|
|
2342
|
-
const terms: Record<string, unknown> = {};
|
|
2343
|
-
|
|
2344
|
-
// Find columns that appear in multiple tables (ambiguous terms)
|
|
2345
|
-
const columnToTables = new Map<string, string[]>();
|
|
2346
|
-
for (const p of profiles) {
|
|
2347
|
-
for (const col of p.columns) {
|
|
2348
|
-
if (col.is_primary_key || col.is_foreign_key) continue;
|
|
2349
|
-
const existing = columnToTables.get(col.name) ?? [];
|
|
2350
|
-
existing.push(p.table_name);
|
|
2351
|
-
columnToTables.set(col.name, existing);
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
|
-
for (const [colName, tables] of columnToTables) {
|
|
2356
|
-
if (tables.length > 1) {
|
|
2357
|
-
terms[colName] = {
|
|
2358
|
-
status: "ambiguous",
|
|
2359
|
-
note: `"${colName}" appears in multiple tables: ${tables.join(", ")}. ASK the user which table they mean.`,
|
|
2360
|
-
possible_mappings: tables.map((t) => `${t}.${colName}`),
|
|
2361
|
-
};
|
|
2362
|
-
}
|
|
2363
|
-
}
|
|
2364
|
-
|
|
2365
|
-
// Add FK relationship terms
|
|
2366
|
-
for (const p of profiles) {
|
|
2367
|
-
for (const fk of p.foreign_keys) {
|
|
2368
|
-
const termName = fk.from_column.replace(/_id$/, "");
|
|
2369
|
-
if (!terms[termName]) {
|
|
2370
|
-
terms[termName] = {
|
|
2371
|
-
status: "defined",
|
|
2372
|
-
definition: `Refers to the ${fk.to_table} entity. Linked via ${p.table_name}.${fk.from_column} → ${fk.to_table}.${fk.to_column}.`,
|
|
2373
|
-
};
|
|
2374
|
-
}
|
|
2375
|
-
}
|
|
2376
|
-
}
|
|
891
|
+
is_foreign_key: isFK,
|
|
892
|
+
fk_target_table: isFK ? field.referenceTo[0] : null,
|
|
893
|
+
fk_target_column: isFK ? "Id" : null,
|
|
894
|
+
is_enum_like: isEnumLike,
|
|
895
|
+
profiler_notes: [],
|
|
896
|
+
};
|
|
897
|
+
});
|
|
2377
898
|
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
899
|
+
profiles.push({
|
|
900
|
+
table_name: objectName,
|
|
901
|
+
object_type: "table",
|
|
902
|
+
row_count: rowCount,
|
|
903
|
+
columns,
|
|
904
|
+
primary_key_columns: primaryKeyColumns,
|
|
905
|
+
foreign_keys: foreignKeys,
|
|
906
|
+
inferred_foreign_keys: [],
|
|
907
|
+
profiler_notes: [],
|
|
908
|
+
table_flags: { possibly_abandoned: false, possibly_denormalized: false },
|
|
909
|
+
});
|
|
910
|
+
progress?.onTableDone(objectName, i, objectsToProfile.length);
|
|
911
|
+
} catch (err) {
|
|
912
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
913
|
+
// Fail fast on connection-level errors that will affect all remaining objects
|
|
914
|
+
if (isFatalConnectionError(err)) {
|
|
915
|
+
throw new Error(`Fatal Salesforce error while profiling ${objectName}: ${msg}`, { cause: err });
|
|
916
|
+
}
|
|
917
|
+
if (progress) {
|
|
918
|
+
progress.onTableError(objectName, msg, i, objectsToProfile.length);
|
|
919
|
+
} else {
|
|
920
|
+
console.error(` Warning: Failed to profile ${objectName}: ${msg}`);
|
|
921
|
+
}
|
|
922
|
+
errors.push({ table: objectName, error: msg });
|
|
923
|
+
continue;
|
|
2386
924
|
}
|
|
2387
925
|
}
|
|
926
|
+
} finally {
|
|
927
|
+
await source.close();
|
|
2388
928
|
}
|
|
2389
929
|
|
|
2390
|
-
|
|
2391
|
-
for (const p of profiles) {
|
|
2392
|
-
for (const col of p.columns) {
|
|
2393
|
-
if (!col.is_enum_like) continue;
|
|
2394
|
-
const inconsistencyNote = col.profiler_notes.find((n) =>
|
|
2395
|
-
n.startsWith("Case-inconsistent")
|
|
2396
|
-
);
|
|
2397
|
-
if (!inconsistencyNote) continue;
|
|
2398
|
-
|
|
2399
|
-
const termKey = `${p.table_name}.${col.name}`;
|
|
2400
|
-
terms[termKey] = {
|
|
2401
|
-
status: "ambiguous",
|
|
2402
|
-
note: `${col.name} on ${p.table_name} has case-inconsistent values. Use LOWER(${col.name}) when grouping or filtering.`,
|
|
2403
|
-
guidance: `Always wrap in LOWER() for reliable aggregation: GROUP BY LOWER(${col.name})`,
|
|
2404
|
-
};
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2407
|
-
|
|
2408
|
-
if (Object.keys(terms).length === 0) {
|
|
2409
|
-
terms["example_term"] = {
|
|
2410
|
-
status: "defined",
|
|
2411
|
-
definition: "Replace this with your own business terms",
|
|
2412
|
-
};
|
|
2413
|
-
}
|
|
2414
|
-
|
|
2415
|
-
return yaml.dump({ terms }, { lineWidth: 120, noRefs: true });
|
|
930
|
+
return { profiles, errors };
|
|
2416
931
|
}
|
|
2417
932
|
|
|
2418
|
-
export function mapSQLType(sqlType: string): string {
|
|
2419
|
-
// Strip ClickHouse wrappers (Nullable, LowCardinality) before matching
|
|
2420
|
-
const unwrapped = sqlType.replace(/Nullable\((.+)\)/g, "$1").replace(/LowCardinality\((.+)\)/g, "$1");
|
|
2421
|
-
const t = unwrapped.toLowerCase();
|
|
2422
|
-
// interval and money look like they contain "int" — handle before the numeric check
|
|
2423
|
-
if (t.includes("interval") || t.includes("money")) return "string";
|
|
2424
|
-
if (
|
|
2425
|
-
t.includes("int") ||
|
|
2426
|
-
t.includes("float") ||
|
|
2427
|
-
t.includes("real") ||
|
|
2428
|
-
t.includes("numeric") ||
|
|
2429
|
-
t.includes("decimal") ||
|
|
2430
|
-
t.includes("double") ||
|
|
2431
|
-
t === "currency" ||
|
|
2432
|
-
t === "percent" ||
|
|
2433
|
-
t === "long"
|
|
2434
|
-
)
|
|
2435
|
-
return "number";
|
|
2436
|
-
if (t.startsWith("bool")) return "boolean";
|
|
2437
|
-
if (t.includes("date") || t.includes("time") || t.includes("timestamp"))
|
|
2438
|
-
return "date";
|
|
2439
|
-
return "string";
|
|
2440
|
-
}
|
|
2441
933
|
|
|
2442
934
|
// --- DuckDB profiler (CSV/Parquet files) ---
|
|
2443
935
|
|
|
2444
936
|
/** Helper to run a DuckDB query and return typed rows. */
|
|
2445
937
|
async function duckdbQuery<T = Record<string, unknown>>(
|
|
2446
|
-
conn:
|
|
938
|
+
conn: DuckDBConnection,
|
|
2447
939
|
sql: string,
|
|
2448
940
|
): Promise<T[]> {
|
|
2449
|
-
|
|
2450
|
-
const reader = await (conn as any).runAndReadAll(sql);
|
|
941
|
+
const reader = await conn.runAndReadAll(sql);
|
|
2451
942
|
return reader.getRowObjects() as T[];
|
|
2452
943
|
}
|
|
2453
944
|
|
|
@@ -2488,18 +979,15 @@ export async function ingestIntoDuckDB(
|
|
|
2488
979
|
? `read_csv_auto('${absPath.replace(/'/g, "''")}')`
|
|
2489
980
|
: `read_parquet('${absPath.replace(/'/g, "''")}')`
|
|
2490
981
|
|
|
2491
|
-
|
|
2492
|
-
await (conn as any).run(`CREATE TABLE "${stem}" AS SELECT * FROM ${readFn}`);
|
|
982
|
+
await conn.run(`CREATE TABLE "${stem}" AS SELECT * FROM ${readFn}`);
|
|
2493
983
|
tableNames.push(stem);
|
|
2494
984
|
console.log(` Loaded ${file.format.toUpperCase()} → table "${stem}" from ${file.path}`);
|
|
2495
985
|
}
|
|
2496
986
|
return tableNames;
|
|
2497
987
|
} finally {
|
|
2498
988
|
// DuckDB Neo API uses synchronous cleanup methods
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2502
|
-
(instance as any).closeSync();
|
|
989
|
+
conn.disconnectSync();
|
|
990
|
+
instance.closeSync();
|
|
2503
991
|
}
|
|
2504
992
|
}
|
|
2505
993
|
|
|
@@ -2523,10 +1011,8 @@ export async function listDuckDBObjects(dbPath: string): Promise<DatabaseObject[
|
|
|
2523
1011
|
}));
|
|
2524
1012
|
} finally {
|
|
2525
1013
|
// DuckDB Neo API uses synchronous cleanup methods
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2529
|
-
(instance as any).closeSync();
|
|
1014
|
+
conn.disconnectSync();
|
|
1015
|
+
instance.closeSync();
|
|
2530
1016
|
}
|
|
2531
1017
|
}
|
|
2532
1018
|
|
|
@@ -2548,11 +1034,13 @@ export async function profileDuckDB(
|
|
|
2548
1034
|
dbPath: string,
|
|
2549
1035
|
filterTables?: string[],
|
|
2550
1036
|
prefetchedObjects?: DatabaseObject[],
|
|
2551
|
-
|
|
1037
|
+
progress?: ProfileProgressCallbacks
|
|
1038
|
+
): Promise<ProfilingResult> {
|
|
2552
1039
|
const DuckDBInstance = await loadDuckDB();
|
|
2553
1040
|
const instance = await DuckDBInstance.create(dbPath, { access_mode: "READ_ONLY" });
|
|
2554
1041
|
const conn = await instance.connect();
|
|
2555
1042
|
const profiles: TableProfile[] = [];
|
|
1043
|
+
const errors: ProfileError[] = [];
|
|
2556
1044
|
|
|
2557
1045
|
try {
|
|
2558
1046
|
let allObjects: DatabaseObject[];
|
|
@@ -2566,10 +1054,17 @@ export async function profileDuckDB(
|
|
|
2566
1054
|
? allObjects.filter((o) => filterTables.includes(o.name))
|
|
2567
1055
|
: allObjects;
|
|
2568
1056
|
|
|
1057
|
+
progress?.onStart(objectsToProfile.length);
|
|
1058
|
+
|
|
2569
1059
|
for (const [i, obj] of objectsToProfile.entries()) {
|
|
2570
1060
|
const tableName = obj.name;
|
|
2571
1061
|
const objectType = obj.type;
|
|
2572
|
-
|
|
1062
|
+
const objectLabel = objectType === "view" ? " [view]" : "";
|
|
1063
|
+
if (progress) {
|
|
1064
|
+
progress.onTableStart(tableName + objectLabel, i, objectsToProfile.length);
|
|
1065
|
+
} else {
|
|
1066
|
+
console.log(` [${i + 1}/${objectsToProfile.length}] Profiling ${tableName}${objectLabel}...`);
|
|
1067
|
+
}
|
|
2573
1068
|
|
|
2574
1069
|
try {
|
|
2575
1070
|
const countRows = await duckdbQuery<{ c: number | bigint }>(conn, `SELECT COUNT(*) as c FROM "${tableName}"`);
|
|
@@ -2622,6 +1117,7 @@ export async function profileDuckDB(
|
|
|
2622
1117
|
sampleValues = sampleRows.map((r) => String(r.v));
|
|
2623
1118
|
}
|
|
2624
1119
|
} catch (colErr) {
|
|
1120
|
+
if (isFatalConnectionError(colErr)) throw colErr;
|
|
2625
1121
|
console.warn(
|
|
2626
1122
|
` Warning: Could not profile column ${tableName}.${col.column_name}: ${colErr instanceof Error ? colErr.message : String(colErr)}`
|
|
2627
1123
|
);
|
|
@@ -2657,19 +1153,28 @@ export async function profileDuckDB(
|
|
|
2657
1153
|
possibly_denormalized: false,
|
|
2658
1154
|
},
|
|
2659
1155
|
});
|
|
1156
|
+
progress?.onTableDone(tableName, i, objectsToProfile.length);
|
|
2660
1157
|
} catch (err) {
|
|
2661
|
-
|
|
1158
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1159
|
+
// Fail fast on connection-level errors that will affect all remaining tables
|
|
1160
|
+
if (isFatalConnectionError(err)) {
|
|
1161
|
+
throw new Error(`Fatal database error while profiling ${tableName}: ${msg}`, { cause: err });
|
|
1162
|
+
}
|
|
1163
|
+
if (progress) {
|
|
1164
|
+
progress.onTableError(tableName, msg, i, objectsToProfile.length);
|
|
1165
|
+
} else {
|
|
1166
|
+
console.error(` Warning: Failed to profile ${tableName}: ${msg}`);
|
|
1167
|
+
}
|
|
1168
|
+
errors.push({ table: tableName, error: msg });
|
|
2662
1169
|
}
|
|
2663
1170
|
}
|
|
2664
1171
|
} finally {
|
|
2665
1172
|
// DuckDB Neo API uses synchronous cleanup methods
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2669
|
-
(instance as any).closeSync();
|
|
1173
|
+
conn.disconnectSync();
|
|
1174
|
+
instance.closeSync();
|
|
2670
1175
|
}
|
|
2671
1176
|
|
|
2672
|
-
return profiles;
|
|
1177
|
+
return { profiles, errors };
|
|
2673
1178
|
}
|
|
2674
1179
|
|
|
2675
1180
|
// --- Demo datasets ---
|
|
@@ -2916,7 +1421,13 @@ export function computeDiff(
|
|
|
2916
1421
|
|
|
2917
1422
|
// Metadata differences
|
|
2918
1423
|
const metadataChanges: string[] = [];
|
|
2919
|
-
|
|
1424
|
+
// Only flag type changes that indicate real schema drift (e.g. table↔view).
|
|
1425
|
+
// Semantic classifications like dimension_table vs fact_table are enrichment
|
|
1426
|
+
// metadata — the profiler always assigns "fact_table" to non-views, so comparing
|
|
1427
|
+
// it against enriched YAML produces false positives.
|
|
1428
|
+
const semanticTypes = new Set(["fact_table", "dimension_table"]);
|
|
1429
|
+
if (db.objectType && yml.objectType && db.objectType !== yml.objectType
|
|
1430
|
+
&& !(semanticTypes.has(db.objectType) && semanticTypes.has(yml.objectType))) {
|
|
2920
1431
|
metadataChanges.push(`type changed: ${yml.objectType} → ${db.objectType}`);
|
|
2921
1432
|
}
|
|
2922
1433
|
if (db.partitionStrategy !== yml.partitionStrategy) {
|
|
@@ -3147,7 +1658,10 @@ export async function handleActionApproval(
|
|
|
3147
1658
|
signal: AbortSignal.timeout(30_000),
|
|
3148
1659
|
});
|
|
3149
1660
|
if (!res.ok) {
|
|
3150
|
-
const body = (await res.json().catch(() =>
|
|
1661
|
+
const body = (await res.json().catch(() => {
|
|
1662
|
+
// intentionally ignored: error response may not be JSON; fall back to status code
|
|
1663
|
+
return {};
|
|
1664
|
+
})) as Record<string, unknown>;
|
|
3151
1665
|
return { ok: false, error: (body.message as string) ?? `HTTP ${res.status}` };
|
|
3152
1666
|
}
|
|
3153
1667
|
const body = (await res.json()) as Record<string, unknown>;
|
|
@@ -3834,6 +2348,183 @@ async function handleQuery(args: string[]): Promise<void> {
|
|
|
3834
2348
|
}
|
|
3835
2349
|
}
|
|
3836
2350
|
|
|
2351
|
+
// --- Index CLI handler ---
|
|
2352
|
+
|
|
2353
|
+
async function handleIndex(args: string[]): Promise<void> {
|
|
2354
|
+
const statsOnly = args.includes("--stats");
|
|
2355
|
+
|
|
2356
|
+
if (!fs.existsSync(SEMANTIC_DIR)) {
|
|
2357
|
+
console.error(pc.red("No semantic/ directory found. Run 'atlas init' first."));
|
|
2358
|
+
process.exit(1);
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
try {
|
|
2362
|
+
const { getSemanticIndexStats, buildSemanticIndex } = await import("@atlas/api/lib/semantic/search");
|
|
2363
|
+
|
|
2364
|
+
// Use stats-based validation — works for both default and per-source layouts
|
|
2365
|
+
const stats = getSemanticIndexStats(SEMANTIC_DIR);
|
|
2366
|
+
|
|
2367
|
+
if (stats.entities === 0) {
|
|
2368
|
+
console.error(pc.red("No valid entity YAML files found in semantic/. Run 'atlas init' first."));
|
|
2369
|
+
process.exit(1);
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
if (statsOnly) {
|
|
2373
|
+
console.log(
|
|
2374
|
+
`${pc.bold("Semantic index stats:")} ` +
|
|
2375
|
+
`${stats.entities} entities, ${stats.dimensions} dimensions, ` +
|
|
2376
|
+
`${stats.measures} measures, ${stats.metrics} metrics, ` +
|
|
2377
|
+
`${stats.glossaryTerms} glossary terms (${stats.keywords} keywords)`
|
|
2378
|
+
);
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
// Full rebuild — buildSemanticIndex does its own loading; stats above are for validation + display
|
|
2383
|
+
const start = Date.now();
|
|
2384
|
+
buildSemanticIndex(SEMANTIC_DIR);
|
|
2385
|
+
const elapsed = Date.now() - start;
|
|
2386
|
+
|
|
2387
|
+
console.log(
|
|
2388
|
+
pc.green("✓") + ` Indexed ${stats.entities} entities, ` +
|
|
2389
|
+
`${stats.dimensions} dimensions, ${stats.measures} measures ` +
|
|
2390
|
+
`(${stats.keywords} keywords) in ${elapsed}ms`
|
|
2391
|
+
);
|
|
2392
|
+
} catch (err) {
|
|
2393
|
+
console.error(pc.red("Failed to build semantic index."));
|
|
2394
|
+
console.error(` ${err instanceof Error ? err.message : String(err)}`);
|
|
2395
|
+
process.exit(1);
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
// --- Learn CLI handler ---
|
|
2400
|
+
|
|
2401
|
+
async function handleLearn(args: string[]): Promise<void> {
|
|
2402
|
+
const applyMode = args.includes("--apply");
|
|
2403
|
+
const runSuggestions = args.includes("--suggestions");
|
|
2404
|
+
const limitArg = getFlag(args, "--limit");
|
|
2405
|
+
const sinceArg = getFlag(args, "--since");
|
|
2406
|
+
const sourceArg = requireFlagIdentifier(args, "--source", "source name");
|
|
2407
|
+
|
|
2408
|
+
// Resolve semantic directories
|
|
2409
|
+
const semanticRoot = sourceArg
|
|
2410
|
+
? path.join(SEMANTIC_DIR, sourceArg)
|
|
2411
|
+
: SEMANTIC_DIR;
|
|
2412
|
+
const entitiesDir = sourceArg
|
|
2413
|
+
? path.join(semanticRoot, "entities")
|
|
2414
|
+
: ENTITIES_DIR;
|
|
2415
|
+
|
|
2416
|
+
// Validate semantic layer exists
|
|
2417
|
+
if (!fs.existsSync(entitiesDir)) {
|
|
2418
|
+
console.error(pc.red(`No entities found at ${entitiesDir}. Run 'atlas init' first.`));
|
|
2419
|
+
process.exit(1);
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
// Validate internal DB is configured
|
|
2423
|
+
if (!process.env.DATABASE_URL) {
|
|
2424
|
+
console.error(pc.red("DATABASE_URL is required for atlas learn."));
|
|
2425
|
+
console.error(" The audit log is stored in the internal database.");
|
|
2426
|
+
console.error(" Set DATABASE_URL=postgresql://... to enable audit log analysis.");
|
|
2427
|
+
process.exit(1);
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
// Validate --limit
|
|
2431
|
+
const limit = limitArg ? parseInt(limitArg, 10) : 1000;
|
|
2432
|
+
if (Number.isNaN(limit) || limit <= 0) {
|
|
2433
|
+
console.error(pc.red(`Invalid value for --limit: "${limitArg}". Expected a positive integer.`));
|
|
2434
|
+
process.exit(1);
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
// Validate --since
|
|
2438
|
+
if (sinceArg) {
|
|
2439
|
+
const sinceDate = new Date(sinceArg);
|
|
2440
|
+
if (Number.isNaN(sinceDate.getTime())) {
|
|
2441
|
+
console.error(pc.red(`Invalid value for --since: "${sinceArg}". Expected ISO 8601 format (e.g., 2026-03-01).`));
|
|
2442
|
+
process.exit(1);
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
console.log(`\nAtlas Learn — analyzing audit log for YAML improvements...\n`);
|
|
2447
|
+
|
|
2448
|
+
const { getInternalDB, closeInternalDB } = await import("@atlas/api/lib/db/internal");
|
|
2449
|
+
try {
|
|
2450
|
+
const { fetchAuditLog, analyzeQueries } = await import("../lib/learn/analyze");
|
|
2451
|
+
const { loadEntities, loadGlossary, generateProposals, applyProposals } = await import("../lib/learn/propose");
|
|
2452
|
+
const { formatDiff, formatSummary } = await import("../lib/learn/diff");
|
|
2453
|
+
|
|
2454
|
+
// 1. Fetch audit log
|
|
2455
|
+
const pool = getInternalDB();
|
|
2456
|
+
const rows = await fetchAuditLog(pool, { limit, since: sinceArg });
|
|
2457
|
+
|
|
2458
|
+
if (rows.length === 0) {
|
|
2459
|
+
console.log(pc.yellow("No successful queries found in the audit log."));
|
|
2460
|
+
console.log(" Run some queries first, then try again.");
|
|
2461
|
+
return;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
console.log(` Analyzed ${pc.bold(String(rows.length))} successful queries`);
|
|
2465
|
+
|
|
2466
|
+
// 2. Analyze patterns
|
|
2467
|
+
const analysis = analyzeQueries(rows);
|
|
2468
|
+
console.log(` Found ${pc.bold(String(analysis.patterns.length))} recurring patterns, ` +
|
|
2469
|
+
`${pc.bold(String(analysis.joins.size))} join pairs, ` +
|
|
2470
|
+
`${pc.bold(String(analysis.aliases.length))} column aliases`);
|
|
2471
|
+
|
|
2472
|
+
// 3. Load existing YAML
|
|
2473
|
+
const entities = loadEntities(entitiesDir);
|
|
2474
|
+
const glossaryData = loadGlossary(semanticRoot);
|
|
2475
|
+
|
|
2476
|
+
if (entities.size === 0) {
|
|
2477
|
+
console.error(pc.red(`No valid entity YAML files found in ${entitiesDir}.`));
|
|
2478
|
+
process.exit(1);
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
console.log(` Comparing against ${pc.bold(String(entities.size))} entities\n`);
|
|
2482
|
+
|
|
2483
|
+
// 4. Generate proposals
|
|
2484
|
+
const proposalSet = generateProposals(analysis, entities, glossaryData);
|
|
2485
|
+
|
|
2486
|
+
// 5. Output results
|
|
2487
|
+
console.log(formatSummary(proposalSet));
|
|
2488
|
+
|
|
2489
|
+
if (proposalSet.proposals.length > 0) {
|
|
2490
|
+
console.log(formatDiff(proposalSet));
|
|
2491
|
+
|
|
2492
|
+
if (applyMode) {
|
|
2493
|
+
const { written, failed } = applyProposals(proposalSet);
|
|
2494
|
+
if (written.length > 0) {
|
|
2495
|
+
console.log(pc.green(`\n✓ Applied changes to ${written.length} file(s):`));
|
|
2496
|
+
for (const f of written) {
|
|
2497
|
+
console.log(` ${f.replace(process.cwd() + "/", "")}`);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
if (failed.length > 0) {
|
|
2501
|
+
console.error(pc.red(`\n✗ Failed to write ${failed.length} file(s):`));
|
|
2502
|
+
for (const f of failed) {
|
|
2503
|
+
console.error(` ${f.path.replace(process.cwd() + "/", "")}: ${f.error}`);
|
|
2504
|
+
}
|
|
2505
|
+
process.exit(1);
|
|
2506
|
+
}
|
|
2507
|
+
} else {
|
|
2508
|
+
console.log(pc.dim("\nDry run — no files modified. Use --apply to write changes."));
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
if (runSuggestions) {
|
|
2513
|
+
console.log("\n📊 Generating query suggestions from audit log...");
|
|
2514
|
+
const { generateSuggestions } = await import("@atlas/api/lib/learn/suggestions");
|
|
2515
|
+
const result = await generateSuggestions(null); // CLI runs in single-org mode
|
|
2516
|
+
console.log(` Created: ${pc.bold(String(result.created))} suggestions`);
|
|
2517
|
+
console.log(` Updated: ${pc.bold(String(result.updated))} suggestions`);
|
|
2518
|
+
}
|
|
2519
|
+
} catch (err) {
|
|
2520
|
+
console.error(pc.red("Failed to analyze audit log."));
|
|
2521
|
+
console.error(` ${err instanceof Error ? err.message : String(err)}`);
|
|
2522
|
+
process.exit(1);
|
|
2523
|
+
} finally {
|
|
2524
|
+
await closeInternalDB();
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
|
|
3837
2528
|
// --- Diff CLI handler ---
|
|
3838
2529
|
|
|
3839
2530
|
async function handleDiff(args: string[]): Promise<void> {
|
|
@@ -3927,8 +2618,7 @@ async function handleDiff(args: string[]): Promise<void> {
|
|
|
3927
2618
|
} else if (dbType === "salesforce") {
|
|
3928
2619
|
const { parseSalesforceURL, createSalesforceConnection } = await import("../../../plugins/salesforce/src/connection");
|
|
3929
2620
|
const config = parseSalesforceURL(connStr);
|
|
3930
|
-
|
|
3931
|
-
const source: any = createSalesforceConnection(config);
|
|
2621
|
+
const source = createSalesforceConnection(config);
|
|
3932
2622
|
try {
|
|
3933
2623
|
const objects = await source.listObjects();
|
|
3934
2624
|
console.log(`Connected: Salesforce (${objects.length} queryable objects)`);
|
|
@@ -3971,32 +2661,39 @@ async function handleDiff(args: string[]): Promise<void> {
|
|
|
3971
2661
|
console.log(`\nProfiling ${dbType} database...\n`);
|
|
3972
2662
|
let profiles: TableProfile[];
|
|
3973
2663
|
try {
|
|
2664
|
+
let result: ProfilingResult;
|
|
3974
2665
|
switch (dbType) {
|
|
3975
2666
|
case "mysql":
|
|
3976
|
-
|
|
2667
|
+
result = await profileMySQL(connStr, filterTables, undefined, undefined, cliProfileLogger);
|
|
3977
2668
|
break;
|
|
3978
2669
|
case "postgres":
|
|
3979
|
-
|
|
2670
|
+
result = await profilePostgres(connStr, filterTables, undefined, schemaArg, undefined, cliProfileLogger);
|
|
3980
2671
|
break;
|
|
3981
2672
|
case "clickhouse":
|
|
3982
|
-
|
|
2673
|
+
result = await profileClickHouse(connStr, filterTables);
|
|
3983
2674
|
break;
|
|
3984
2675
|
case "snowflake":
|
|
3985
|
-
|
|
2676
|
+
result = await profileSnowflake(connStr, filterTables);
|
|
3986
2677
|
break;
|
|
3987
2678
|
case "duckdb": {
|
|
3988
2679
|
const { parseDuckDBUrl } = await import("../../../plugins/duckdb/src/connection");
|
|
3989
2680
|
const duckConfig = parseDuckDBUrl(connStr);
|
|
3990
|
-
|
|
2681
|
+
result = await profileDuckDB(duckConfig.path, filterTables);
|
|
3991
2682
|
break;
|
|
3992
2683
|
}
|
|
3993
2684
|
case "salesforce":
|
|
3994
|
-
|
|
2685
|
+
result = await profileSalesforce(connStr, filterTables);
|
|
3995
2686
|
break;
|
|
3996
2687
|
default: {
|
|
3997
2688
|
throw new Error(`Unknown database type: ${dbType}`);
|
|
3998
2689
|
}
|
|
3999
2690
|
}
|
|
2691
|
+
profiles = result.profiles;
|
|
2692
|
+
if (result.errors.length > 0) {
|
|
2693
|
+
const total = result.profiles.length + result.errors.length;
|
|
2694
|
+
logProfilingErrors(result.errors, total);
|
|
2695
|
+
console.warn(`Continuing diff with ${profiles.length} successfully profiled tables.\n`);
|
|
2696
|
+
}
|
|
4000
2697
|
} catch (err) {
|
|
4001
2698
|
console.error(`\nError: Failed to profile database.`);
|
|
4002
2699
|
console.error(err instanceof Error ? err.message : String(err));
|
|
@@ -4009,7 +2706,7 @@ async function handleDiff(args: string[]): Promise<void> {
|
|
|
4009
2706
|
}
|
|
4010
2707
|
|
|
4011
2708
|
// Run FK inference so inferred FKs are comparable
|
|
4012
|
-
analyzeTableProfiles(profiles);
|
|
2709
|
+
profiles = analyzeTableProfiles(profiles);
|
|
4013
2710
|
|
|
4014
2711
|
// Build DB snapshots
|
|
4015
2712
|
const dbSnapshots = new Map<string, EntitySnapshot>();
|
|
@@ -4058,7 +2755,7 @@ async function handleDiff(args: string[]): Promise<void> {
|
|
|
4058
2755
|
|
|
4059
2756
|
// --- Profile a single datasource ---
|
|
4060
2757
|
|
|
4061
|
-
|
|
2758
|
+
interface ProfileDatasourceOpts {
|
|
4062
2759
|
id: string; // "default", "warehouse", etc.
|
|
4063
2760
|
url: string;
|
|
4064
2761
|
dbType: DBType;
|
|
@@ -4067,25 +2764,18 @@ export interface ProfileDatasourceOpts {
|
|
|
4067
2764
|
shouldEnrich: boolean;
|
|
4068
2765
|
explicitEnrich: boolean;
|
|
4069
2766
|
demoDataset: DemoDataset | null; // null for multi-source runs (--demo is single-datasource only)
|
|
2767
|
+
force: boolean; // skip failure threshold check
|
|
2768
|
+
orgId?: string; // org-scoped mode: write to semantic/.orgs/{orgId}/
|
|
4070
2769
|
}
|
|
4071
2770
|
|
|
4072
|
-
|
|
4073
|
-
* Compute the output base directory for a datasource.
|
|
4074
|
-
* "default" → `semantic/`, anything else → `semantic/{id}/`.
|
|
4075
|
-
* Returns an absolute path resolved from the process working directory.
|
|
4076
|
-
*/
|
|
4077
|
-
export function outputDirForDatasource(id: string): string {
|
|
4078
|
-
return id === "default" ? SEMANTIC_DIR : path.join(SEMANTIC_DIR, id);
|
|
4079
|
-
}
|
|
4080
|
-
|
|
4081
|
-
export interface DatasourceEntry {
|
|
2771
|
+
interface DatasourceEntry {
|
|
4082
2772
|
id: string;
|
|
4083
2773
|
url: string;
|
|
4084
2774
|
schema: string;
|
|
4085
2775
|
}
|
|
4086
2776
|
|
|
4087
2777
|
async function profileDatasource(opts: ProfileDatasourceOpts): Promise<void> {
|
|
4088
|
-
const { id, url: connStr, dbType, filterTables, shouldEnrich, explicitEnrich, demoDataset } = opts;
|
|
2778
|
+
const { id, url: connStr, dbType, filterTables, shouldEnrich, explicitEnrich, demoDataset, force, orgId } = opts;
|
|
4089
2779
|
let { schema: schemaArg } = opts;
|
|
4090
2780
|
|
|
4091
2781
|
validateSchemaName(schemaArg);
|
|
@@ -4176,14 +2866,11 @@ async function profileDatasource(opts: ProfileDatasourceOpts): Promise<void> {
|
|
|
4176
2866
|
const DuckDBInstance = await loadDuckDB();
|
|
4177
2867
|
const testInstance = await DuckDBInstance.create(duckConfig.path, { access_mode: "READ_ONLY" });
|
|
4178
2868
|
const testConn = await testInstance.connect();
|
|
4179
|
-
|
|
4180
|
-
const reader = await (testConn as any).runAndReadAll("SELECT version() as v");
|
|
2869
|
+
const reader = await testConn.runAndReadAll("SELECT version() as v");
|
|
4181
2870
|
const version = reader.getRowObjects()[0]?.v ?? "unknown";
|
|
4182
2871
|
console.log(`Connected: DuckDB ${version}`);
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4186
|
-
(testInstance as any).closeSync();
|
|
2872
|
+
testConn.disconnectSync();
|
|
2873
|
+
testInstance.closeSync();
|
|
4187
2874
|
} catch (err) {
|
|
4188
2875
|
console.error(`\nError: Cannot open DuckDB database.`);
|
|
4189
2876
|
console.error(err instanceof Error ? err.message : String(err));
|
|
@@ -4193,8 +2880,7 @@ async function profileDatasource(opts: ProfileDatasourceOpts): Promise<void> {
|
|
|
4193
2880
|
} else if (dbType === "salesforce") {
|
|
4194
2881
|
const { parseSalesforceURL, createSalesforceConnection } = await import("../../../plugins/salesforce/src/connection");
|
|
4195
2882
|
const config = parseSalesforceURL(connStr);
|
|
4196
|
-
|
|
4197
|
-
const source: any = createSalesforceConnection(config);
|
|
2883
|
+
const source = createSalesforceConnection(config);
|
|
4198
2884
|
try {
|
|
4199
2885
|
const objects = await source.listObjects();
|
|
4200
2886
|
console.log(`Connected: Salesforce (${objects.length} queryable objects)`);
|
|
@@ -4232,10 +2918,10 @@ async function profileDatasource(opts: ProfileDatasourceOpts): Promise<void> {
|
|
|
4232
2918
|
try {
|
|
4233
2919
|
switch (dbType) {
|
|
4234
2920
|
case "mysql":
|
|
4235
|
-
allObjects = await listMySQLObjects(connStr);
|
|
2921
|
+
allObjects = await listMySQLObjects(connStr, cliProfileLogger);
|
|
4236
2922
|
break;
|
|
4237
2923
|
case "postgres":
|
|
4238
|
-
allObjects = await listPostgresObjects(connStr, schemaArg);
|
|
2924
|
+
allObjects = await listPostgresObjects(connStr, schemaArg, cliProfileLogger);
|
|
4239
2925
|
break;
|
|
4240
2926
|
case "clickhouse":
|
|
4241
2927
|
allObjects = await listClickHouseObjects(connStr);
|
|
@@ -4298,40 +2984,66 @@ async function profileDatasource(opts: ProfileDatasourceOpts): Promise<void> {
|
|
|
4298
2984
|
|
|
4299
2985
|
console.log(`\nAtlas Init — profiling ${dbType} database...\n`);
|
|
4300
2986
|
|
|
4301
|
-
|
|
2987
|
+
const progress = createProgressTracker();
|
|
2988
|
+
const profilingStart = Date.now();
|
|
2989
|
+
|
|
2990
|
+
let result: ProfilingResult;
|
|
4302
2991
|
switch (dbType) {
|
|
4303
2992
|
case "mysql":
|
|
4304
|
-
|
|
2993
|
+
result = await profileMySQL(connStr, selectedTables, prefetchedObjects, progress, cliProfileLogger);
|
|
4305
2994
|
break;
|
|
4306
2995
|
case "postgres":
|
|
4307
|
-
|
|
2996
|
+
result = await profilePostgres(connStr, selectedTables, prefetchedObjects, schemaArg, progress, cliProfileLogger);
|
|
4308
2997
|
break;
|
|
4309
2998
|
case "clickhouse":
|
|
4310
|
-
|
|
2999
|
+
result = await profileClickHouse(connStr, selectedTables, prefetchedObjects, progress);
|
|
4311
3000
|
break;
|
|
4312
3001
|
case "snowflake":
|
|
4313
|
-
|
|
3002
|
+
result = await profileSnowflake(connStr, selectedTables, prefetchedObjects, progress);
|
|
4314
3003
|
break;
|
|
4315
3004
|
case "duckdb": {
|
|
4316
3005
|
const { parseDuckDBUrl } = await import("../../../plugins/duckdb/src/connection");
|
|
4317
3006
|
const duckConfig = parseDuckDBUrl(connStr);
|
|
4318
|
-
|
|
3007
|
+
result = await profileDuckDB(duckConfig.path, selectedTables, prefetchedObjects, progress);
|
|
4319
3008
|
break;
|
|
4320
3009
|
}
|
|
4321
3010
|
case "salesforce":
|
|
4322
|
-
|
|
3011
|
+
result = await profileSalesforce(connStr, selectedTables, prefetchedObjects, progress);
|
|
4323
3012
|
break;
|
|
4324
3013
|
default: {
|
|
4325
3014
|
throw new Error(`Unknown database type: ${dbType}`);
|
|
4326
3015
|
}
|
|
4327
3016
|
}
|
|
4328
3017
|
|
|
3018
|
+
let { profiles } = result;
|
|
3019
|
+
const { errors: profilingErrors } = result;
|
|
3020
|
+
const profilingElapsed = Date.now() - profilingStart;
|
|
3021
|
+
progress.onComplete(profiles.length, profilingElapsed);
|
|
3022
|
+
|
|
4329
3023
|
if (profiles.length === 0) {
|
|
4330
3024
|
throw new Error("No tables or views were successfully profiled. Check the warnings above and verify your database permissions.");
|
|
4331
3025
|
}
|
|
4332
3026
|
|
|
3027
|
+
// Always warn about profiling errors
|
|
3028
|
+
if (profilingErrors.length > 0) {
|
|
3029
|
+
const totalAttempted = profiles.length + profilingErrors.length;
|
|
3030
|
+
logProfilingErrors(profilingErrors, totalAttempted);
|
|
3031
|
+
|
|
3032
|
+
const { shouldAbort } = checkFailureThreshold(result, force);
|
|
3033
|
+
if (shouldAbort) {
|
|
3034
|
+
console.error(`\nThis usually indicates a connection or permission issue.`);
|
|
3035
|
+
console.error(`Run \`atlas doctor\` to diagnose. Use \`--force\` to continue anyway.`);
|
|
3036
|
+
throw new Error(
|
|
3037
|
+
`Profiling failed for ${profilingErrors.length}/${totalAttempted} tables ` +
|
|
3038
|
+
`(${Math.round((profilingErrors.length / totalAttempted) * 100)}%). ` +
|
|
3039
|
+
`Use --force to continue anyway.`
|
|
3040
|
+
);
|
|
3041
|
+
}
|
|
3042
|
+
console.warn(`Continuing with ${profiles.length} successfully profiled tables.\n`);
|
|
3043
|
+
}
|
|
3044
|
+
|
|
4333
3045
|
// Run profiler heuristics
|
|
4334
|
-
analyzeTableProfiles(profiles);
|
|
3046
|
+
profiles = analyzeTableProfiles(profiles);
|
|
4335
3047
|
|
|
4336
3048
|
const tableCount = profiles.filter((p) => !isViewLike(p)).length;
|
|
4337
3049
|
const viewCount = profiles.filter((p) => isView(p)).length;
|
|
@@ -4371,7 +3083,7 @@ async function profileDatasource(opts: ProfileDatasourceOpts): Promise<void> {
|
|
|
4371
3083
|
}
|
|
4372
3084
|
|
|
4373
3085
|
// Compute output directories
|
|
4374
|
-
const outputBase = outputDirForDatasource(id);
|
|
3086
|
+
const outputBase = outputDirForDatasource(id, orgId);
|
|
4375
3087
|
const entitiesOutDir = path.join(outputBase, "entities");
|
|
4376
3088
|
const metricsOutDir = path.join(outputBase, "metrics");
|
|
4377
3089
|
|
|
@@ -4379,6 +3091,15 @@ async function profileDatasource(opts: ProfileDatasourceOpts): Promise<void> {
|
|
|
4379
3091
|
fs.mkdirSync(entitiesOutDir, { recursive: true });
|
|
4380
3092
|
fs.mkdirSync(metricsOutDir, { recursive: true });
|
|
4381
3093
|
|
|
3094
|
+
// Clean stale entity/metric files from previous runs
|
|
3095
|
+
for (const dir of [entitiesOutDir, metricsOutDir]) {
|
|
3096
|
+
for (const file of fs.readdirSync(dir)) {
|
|
3097
|
+
if (file.endsWith(".yml") || file.endsWith(".yaml")) {
|
|
3098
|
+
fs.unlinkSync(path.join(dir, file));
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
|
|
4382
3103
|
// Generate entity YAMLs
|
|
4383
3104
|
console.log(`\nGenerating semantic layer...\n`);
|
|
4384
3105
|
|
|
@@ -4438,9 +3159,11 @@ async function profileDatasource(opts: ProfileDatasourceOpts): Promise<void> {
|
|
|
4438
3159
|
}
|
|
4439
3160
|
}
|
|
4440
3161
|
|
|
4441
|
-
const relativeOutput =
|
|
3162
|
+
const relativeOutput = orgId
|
|
3163
|
+
? `./semantic/.orgs/${orgId}/`
|
|
3164
|
+
: id === "default" ? "./semantic/" : `./semantic/${id}/`;
|
|
4442
3165
|
console.log(`
|
|
4443
|
-
Done! Semantic layer
|
|
3166
|
+
Done! Semantic layer written to ${relativeOutput} in ${formatDuration(profilingElapsed)}
|
|
4444
3167
|
|
|
4445
3168
|
Generated:
|
|
4446
3169
|
- ${profiles.length} entity YAMLs with dimensions, joins, measures, and query patterns${sourceId ? ` (connection: ${id})` : ""}
|
|
@@ -4454,6 +3177,82 @@ Next steps:
|
|
|
4454
3177
|
`);
|
|
4455
3178
|
}
|
|
4456
3179
|
|
|
3180
|
+
// --- Import ---
|
|
3181
|
+
|
|
3182
|
+
async function handleImport(args: string[]): Promise<void> {
|
|
3183
|
+
const connectionArg = getFlag(args, "--connection");
|
|
3184
|
+
|
|
3185
|
+
// Determine the API base URL
|
|
3186
|
+
const apiUrl = process.env.ATLAS_API_URL ?? "http://localhost:3001";
|
|
3187
|
+
|
|
3188
|
+
// Build the import request
|
|
3189
|
+
const importUrl = `${apiUrl}/api/v1/admin/semantic/org/import`;
|
|
3190
|
+
const body: Record<string, string> = {};
|
|
3191
|
+
if (connectionArg) body.connectionId = connectionArg;
|
|
3192
|
+
|
|
3193
|
+
// Determine auth header
|
|
3194
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
3195
|
+
if (process.env.ATLAS_API_KEY) headers.Authorization = `Bearer ${process.env.ATLAS_API_KEY}`;
|
|
3196
|
+
|
|
3197
|
+
console.log("Importing semantic layer from disk to DB...\n");
|
|
3198
|
+
|
|
3199
|
+
try {
|
|
3200
|
+
const resp = await fetch(importUrl, {
|
|
3201
|
+
method: "POST",
|
|
3202
|
+
headers,
|
|
3203
|
+
body: JSON.stringify(body),
|
|
3204
|
+
signal: AbortSignal.timeout(60_000),
|
|
3205
|
+
});
|
|
3206
|
+
|
|
3207
|
+
if (!resp.ok) {
|
|
3208
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
3209
|
+
console.error("Import failed: authentication required.");
|
|
3210
|
+
console.error(" Set ATLAS_API_KEY environment variable.");
|
|
3211
|
+
} else {
|
|
3212
|
+
let errorMsg = `HTTP ${resp.status}`;
|
|
3213
|
+
try {
|
|
3214
|
+
const json = await resp.json() as { message?: string; error?: string };
|
|
3215
|
+
errorMsg = json.message ?? json.error ?? errorMsg;
|
|
3216
|
+
} catch {
|
|
3217
|
+
// intentionally ignored: JSON parse failed, fall through to text() attempt
|
|
3218
|
+
errorMsg = await resp.text().catch(() => errorMsg);
|
|
3219
|
+
}
|
|
3220
|
+
console.error(`Import failed: ${errorMsg}`);
|
|
3221
|
+
}
|
|
3222
|
+
process.exit(1);
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
const result = await resp.json() as { imported: number; skipped: number; errors: Array<{ file: string; reason: string }>; total: number };
|
|
3226
|
+
|
|
3227
|
+
console.log(`Imported: ${result.imported}`);
|
|
3228
|
+
if (result.skipped > 0) {
|
|
3229
|
+
console.log(`Skipped: ${result.skipped}`);
|
|
3230
|
+
}
|
|
3231
|
+
console.log(`Total: ${result.total}`);
|
|
3232
|
+
|
|
3233
|
+
if (result.errors.length > 0) {
|
|
3234
|
+
console.log("\nErrors:");
|
|
3235
|
+
for (const e of result.errors) {
|
|
3236
|
+
console.log(` ${e.file}: ${e.reason}`);
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
if (result.imported > 0) {
|
|
3241
|
+
console.log("\nDone! Entities imported to DB. The explore tool and SQL validation will use the updated semantic layer.");
|
|
3242
|
+
}
|
|
3243
|
+
} catch (err) {
|
|
3244
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
3245
|
+
if (detail.includes("ECONNREFUSED") || detail.includes("fetch failed")) {
|
|
3246
|
+
console.error(`Cannot reach Atlas API at ${apiUrl}. Is the server running?`);
|
|
3247
|
+
console.error(" Start it with: bun run dev:api");
|
|
3248
|
+
console.error(" Set ATLAS_API_URL if the API is not on localhost:3001");
|
|
3249
|
+
} else {
|
|
3250
|
+
console.error(`Import failed: ${detail}`);
|
|
3251
|
+
}
|
|
3252
|
+
process.exit(1);
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
|
|
4457
3256
|
// --- Migrate ---
|
|
4458
3257
|
|
|
4459
3258
|
async function handleMigrate(args: string[]): Promise<void> {
|
|
@@ -4569,12 +3368,306 @@ async function handleMigrate(args: string[]): Promise<void> {
|
|
|
4569
3368
|
}
|
|
4570
3369
|
}
|
|
4571
3370
|
|
|
3371
|
+
// --- Help system ---
|
|
3372
|
+
|
|
3373
|
+
interface SubcommandHelp {
|
|
3374
|
+
description: string;
|
|
3375
|
+
usage: string;
|
|
3376
|
+
flags?: Array<{ flag: string; description: string }>;
|
|
3377
|
+
subcommands?: Array<{ name: string; description: string }>;
|
|
3378
|
+
examples?: string[];
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
function printSubcommandHelp(help: SubcommandHelp): void {
|
|
3382
|
+
console.log(`${help.description}\n`);
|
|
3383
|
+
console.log(`Usage: atlas ${help.usage}\n`);
|
|
3384
|
+
if (help.subcommands?.length) {
|
|
3385
|
+
console.log("Subcommands:");
|
|
3386
|
+
const maxLen = Math.max(...help.subcommands.map((s) => s.name.length));
|
|
3387
|
+
for (const s of help.subcommands) {
|
|
3388
|
+
console.log(` ${s.name.padEnd(maxLen + 2)}${s.description}`);
|
|
3389
|
+
}
|
|
3390
|
+
console.log();
|
|
3391
|
+
}
|
|
3392
|
+
if (help.flags?.length) {
|
|
3393
|
+
console.log("Options:");
|
|
3394
|
+
const maxLen = Math.max(...help.flags.map((f) => f.flag.length));
|
|
3395
|
+
for (const f of help.flags) {
|
|
3396
|
+
console.log(` ${f.flag.padEnd(maxLen + 2)}${f.description}`);
|
|
3397
|
+
}
|
|
3398
|
+
console.log();
|
|
3399
|
+
}
|
|
3400
|
+
if (help.examples?.length) {
|
|
3401
|
+
console.log("Examples:");
|
|
3402
|
+
for (const ex of help.examples) {
|
|
3403
|
+
console.log(` ${ex}`);
|
|
3404
|
+
}
|
|
3405
|
+
console.log();
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
const SUBCOMMAND_HELP: Record<string, SubcommandHelp> = {
|
|
3410
|
+
init: {
|
|
3411
|
+
description: "Profile a database and generate semantic layer YAML files.",
|
|
3412
|
+
usage: "init [options]",
|
|
3413
|
+
flags: [
|
|
3414
|
+
{ flag: "--tables <t1,t2>", description: "Profile only specific tables/views (comma-separated)" },
|
|
3415
|
+
{ flag: "--schema <name>", description: "PostgreSQL schema name (default: public)" },
|
|
3416
|
+
{ flag: "--source <name>", description: "Write to semantic/{name}/ subdirectory (mutually exclusive with --connection)" },
|
|
3417
|
+
{ flag: "--connection <name>", description: "Profile a named datasource from atlas.config.ts (mutually exclusive with --source)" },
|
|
3418
|
+
{ flag: "--csv <file1.csv,...>", description: "Load CSV files via DuckDB (no DB server needed, requires @duckdb/node-api)" },
|
|
3419
|
+
{ flag: "--parquet <f1.parquet,...>", description: "Load Parquet files via DuckDB (requires @duckdb/node-api)" },
|
|
3420
|
+
{ flag: "--enrich", description: "Add LLM-enriched descriptions and query patterns (requires API key)" },
|
|
3421
|
+
{ flag: "--no-enrich", description: "Explicitly skip LLM enrichment" },
|
|
3422
|
+
{ flag: "--force", description: "Continue even if more than 20% of tables fail to profile" },
|
|
3423
|
+
{ flag: "--demo [simple|cybersec|ecommerce]", description: "Load a demo dataset then profile (default: simple)" },
|
|
3424
|
+
{ flag: "--org <orgId>", description: "Write to semantic/.orgs/{orgId}/ and auto-import to DB (org-scoped mode)" },
|
|
3425
|
+
{ flag: "--no-import", description: "Skip auto-import to DB in org-scoped mode (write disk only)" },
|
|
3426
|
+
],
|
|
3427
|
+
examples: [
|
|
3428
|
+
"atlas init",
|
|
3429
|
+
"atlas init --tables users,orders,products",
|
|
3430
|
+
"atlas init --enrich",
|
|
3431
|
+
"atlas init --demo cybersec",
|
|
3432
|
+
"atlas init --csv sales.csv,products.csv",
|
|
3433
|
+
"atlas init --org org-123",
|
|
3434
|
+
],
|
|
3435
|
+
},
|
|
3436
|
+
diff: {
|
|
3437
|
+
description: "Compare the database schema against the existing semantic layer. Exits with code 1 if drift is detected.",
|
|
3438
|
+
usage: "diff [options]",
|
|
3439
|
+
flags: [
|
|
3440
|
+
{ flag: "--tables <t1,t2>", description: "Diff only specific tables/views" },
|
|
3441
|
+
{ flag: "--schema <name>", description: "PostgreSQL schema (falls back to ATLAS_SCHEMA, then public)" },
|
|
3442
|
+
{ flag: "--source <name>", description: "Read from semantic/{name}/ subdirectory" },
|
|
3443
|
+
],
|
|
3444
|
+
examples: [
|
|
3445
|
+
"atlas diff",
|
|
3446
|
+
"atlas diff --tables users,orders",
|
|
3447
|
+
'atlas diff || echo "Schema drift detected!"',
|
|
3448
|
+
],
|
|
3449
|
+
},
|
|
3450
|
+
query: {
|
|
3451
|
+
description: "Ask a natural language question and get an answer. Requires a running Atlas API server.",
|
|
3452
|
+
usage: 'query "your question" [options]',
|
|
3453
|
+
flags: [
|
|
3454
|
+
{ flag: "--json", description: "Raw JSON output (pipe-friendly)" },
|
|
3455
|
+
{ flag: "--csv", description: "CSV output (headers + rows, no narrative)" },
|
|
3456
|
+
{ flag: "--quiet", description: "Data only — no narrative, SQL, or stats" },
|
|
3457
|
+
{ flag: "--auto-approve", description: "Auto-approve any pending actions" },
|
|
3458
|
+
{ flag: "--connection <id>", description: "Query a specific datasource" },
|
|
3459
|
+
],
|
|
3460
|
+
examples: [
|
|
3461
|
+
'atlas query "How many users signed up last month?"',
|
|
3462
|
+
'atlas query "top 10 customers by revenue" --json',
|
|
3463
|
+
'atlas query "monthly revenue by product" --csv > report.csv',
|
|
3464
|
+
],
|
|
3465
|
+
},
|
|
3466
|
+
doctor: {
|
|
3467
|
+
description: "Alias for 'atlas validate' — validate config, semantic layer, and connectivity.",
|
|
3468
|
+
usage: "doctor",
|
|
3469
|
+
examples: [
|
|
3470
|
+
"atlas doctor",
|
|
3471
|
+
],
|
|
3472
|
+
},
|
|
3473
|
+
validate: {
|
|
3474
|
+
description: "Validate config, semantic layer, and connectivity. Use --offline to skip connectivity checks.",
|
|
3475
|
+
usage: "validate [options]",
|
|
3476
|
+
flags: [
|
|
3477
|
+
{ flag: "--offline", description: "Skip connectivity checks (datasource, provider, internal DB)" },
|
|
3478
|
+
],
|
|
3479
|
+
examples: [
|
|
3480
|
+
"atlas validate",
|
|
3481
|
+
"atlas validate --offline",
|
|
3482
|
+
],
|
|
3483
|
+
},
|
|
3484
|
+
mcp: {
|
|
3485
|
+
description: "Start an MCP (Model Context Protocol) server for Claude Desktop, Cursor, and other MCP clients.",
|
|
3486
|
+
usage: "mcp [options]",
|
|
3487
|
+
flags: [
|
|
3488
|
+
{ flag: "--transport <stdio|sse>", description: "Transport type (default: stdio)" },
|
|
3489
|
+
{ flag: "--port <n>", description: "Port for SSE transport (default: 8080)" },
|
|
3490
|
+
],
|
|
3491
|
+
examples: [
|
|
3492
|
+
"atlas mcp",
|
|
3493
|
+
"atlas mcp --transport sse --port 9090",
|
|
3494
|
+
],
|
|
3495
|
+
},
|
|
3496
|
+
import: {
|
|
3497
|
+
description: "Import semantic layer YAML files from disk into the internal DB for the active org.",
|
|
3498
|
+
usage: "import [options]",
|
|
3499
|
+
flags: [
|
|
3500
|
+
{ flag: "--connection <name>", description: "Associate imported entities with a named datasource" },
|
|
3501
|
+
],
|
|
3502
|
+
examples: [
|
|
3503
|
+
"atlas import",
|
|
3504
|
+
"atlas import --connection warehouse",
|
|
3505
|
+
],
|
|
3506
|
+
},
|
|
3507
|
+
index: {
|
|
3508
|
+
description: "Rebuild the semantic index from current YAML files, or print index statistics.",
|
|
3509
|
+
usage: "index [options]",
|
|
3510
|
+
flags: [
|
|
3511
|
+
{ flag: "--stats", description: "Print current index statistics without rebuilding" },
|
|
3512
|
+
],
|
|
3513
|
+
examples: [
|
|
3514
|
+
"atlas index",
|
|
3515
|
+
"atlas index --stats",
|
|
3516
|
+
],
|
|
3517
|
+
},
|
|
3518
|
+
learn: {
|
|
3519
|
+
description: "Analyze audit log and propose semantic layer YAML improvements.",
|
|
3520
|
+
usage: "learn [options]",
|
|
3521
|
+
flags: [
|
|
3522
|
+
{ flag: "--apply", description: "Write proposed changes to YAML files (default: dry-run)" },
|
|
3523
|
+
{ flag: "--suggestions", description: "Generate query suggestions from the audit log" },
|
|
3524
|
+
{ flag: "--limit <n>", description: "Max audit log entries to analyze (default: 1000)" },
|
|
3525
|
+
{ flag: "--since <date>", description: "Only analyze queries after this date (ISO 8601)" },
|
|
3526
|
+
{ flag: "--source <name>", description: "Read from/write to semantic/{name}/ subdirectory" },
|
|
3527
|
+
],
|
|
3528
|
+
examples: [
|
|
3529
|
+
"atlas learn",
|
|
3530
|
+
"atlas learn --apply",
|
|
3531
|
+
"atlas learn --since 2026-03-01 --limit 500",
|
|
3532
|
+
"atlas learn --source warehouse",
|
|
3533
|
+
],
|
|
3534
|
+
},
|
|
3535
|
+
migrate: {
|
|
3536
|
+
description: "Generate or apply plugin schema migrations.",
|
|
3537
|
+
usage: "migrate [options]",
|
|
3538
|
+
flags: [
|
|
3539
|
+
{ flag: "--apply", description: "Execute migrations against internal database (default: dry-run)" },
|
|
3540
|
+
],
|
|
3541
|
+
examples: [
|
|
3542
|
+
"atlas migrate",
|
|
3543
|
+
"atlas migrate --apply",
|
|
3544
|
+
],
|
|
3545
|
+
},
|
|
3546
|
+
plugin: {
|
|
3547
|
+
description: "Manage Atlas plugins.",
|
|
3548
|
+
usage: "plugin <list|create|add>",
|
|
3549
|
+
subcommands: [
|
|
3550
|
+
{ name: "list", description: "List installed plugins from atlas.config.ts" },
|
|
3551
|
+
{ name: "create <name> --type <type>", description: "Scaffold a new plugin (datasource|context|interaction|action|sandbox)" },
|
|
3552
|
+
{ name: "add <package-name>", description: "Install a plugin package" },
|
|
3553
|
+
],
|
|
3554
|
+
examples: [
|
|
3555
|
+
"atlas plugin list",
|
|
3556
|
+
"atlas plugin create my-plugin --type datasource",
|
|
3557
|
+
"atlas plugin add @useatlas/plugin-bigquery",
|
|
3558
|
+
],
|
|
3559
|
+
},
|
|
3560
|
+
eval: {
|
|
3561
|
+
description: "Run the evaluation pipeline against demo schemas to measure text-to-SQL accuracy.",
|
|
3562
|
+
usage: "eval [options]",
|
|
3563
|
+
flags: [
|
|
3564
|
+
{ flag: "--schema <name>", description: "Filter by demo dataset (not a PostgreSQL schema; e.g. simple, cybersec, ecommerce)" },
|
|
3565
|
+
{ flag: "--category <name>", description: "Filter by category" },
|
|
3566
|
+
{ flag: "--difficulty <level>", description: "Filter by difficulty (simple|medium|complex)" },
|
|
3567
|
+
{ flag: "--id <case-id>", description: "Run a single case" },
|
|
3568
|
+
{ flag: "--limit <n>", description: "Max cases to evaluate" },
|
|
3569
|
+
{ flag: "--resume <file>", description: "Resume from existing JSONL results file" },
|
|
3570
|
+
{ flag: "--baseline", description: "Save results as new baseline" },
|
|
3571
|
+
{ flag: "--compare <file.jsonl>", description: "Diff against baseline (exit 1 on regression)" },
|
|
3572
|
+
{ flag: "--csv", description: "CSV output" },
|
|
3573
|
+
{ flag: "--json", description: "JSON summary output" },
|
|
3574
|
+
],
|
|
3575
|
+
examples: [
|
|
3576
|
+
"atlas eval",
|
|
3577
|
+
"atlas eval --schema cybersec --difficulty complex",
|
|
3578
|
+
"atlas eval --baseline",
|
|
3579
|
+
],
|
|
3580
|
+
},
|
|
3581
|
+
smoke: {
|
|
3582
|
+
description: "Run end-to-end smoke tests against a running Atlas deployment.",
|
|
3583
|
+
usage: "smoke [options]",
|
|
3584
|
+
flags: [
|
|
3585
|
+
{ flag: "--target <url>", description: "API base URL (default: http://localhost:3001)" },
|
|
3586
|
+
{ flag: "--api-key <key>", description: "Bearer auth token" },
|
|
3587
|
+
{ flag: "--timeout <ms>", description: "Per-check timeout (default: 30000)" },
|
|
3588
|
+
{ flag: "--verbose", description: "Show full response bodies on failure" },
|
|
3589
|
+
{ flag: "--json", description: "Machine-readable JSON output" },
|
|
3590
|
+
],
|
|
3591
|
+
examples: [
|
|
3592
|
+
"atlas smoke",
|
|
3593
|
+
"atlas smoke --target https://api.example.com --api-key sk-...",
|
|
3594
|
+
],
|
|
3595
|
+
},
|
|
3596
|
+
benchmark: {
|
|
3597
|
+
description: "Run the BIRD benchmark for text-to-SQL accuracy evaluation.",
|
|
3598
|
+
usage: "benchmark [options]",
|
|
3599
|
+
flags: [
|
|
3600
|
+
{ flag: "--bird-path <path>", description: "Path to the downloaded BIRD dev directory (required)" },
|
|
3601
|
+
{ flag: "--limit <n>", description: "Max questions to evaluate" },
|
|
3602
|
+
{ flag: "--db <name>", description: "Filter to a single database" },
|
|
3603
|
+
{ flag: "--csv", description: "CSV output" },
|
|
3604
|
+
{ flag: "--resume <file>", description: "Resume from existing JSONL results file" },
|
|
3605
|
+
],
|
|
3606
|
+
examples: [
|
|
3607
|
+
"atlas benchmark --bird-path ./bird-dev",
|
|
3608
|
+
"atlas benchmark --bird-path ./bird-dev --db california_schools --limit 50",
|
|
3609
|
+
],
|
|
3610
|
+
},
|
|
3611
|
+
completions: {
|
|
3612
|
+
description: "Output a shell completion script.",
|
|
3613
|
+
usage: "completions <bash|zsh|fish>",
|
|
3614
|
+
examples: [
|
|
3615
|
+
'eval "$(atlas completions bash)"',
|
|
3616
|
+
'eval "$(atlas completions zsh)"',
|
|
3617
|
+
"atlas completions fish > ~/.config/fish/completions/atlas.fish",
|
|
3618
|
+
],
|
|
3619
|
+
},
|
|
3620
|
+
};
|
|
3621
|
+
|
|
3622
|
+
function printOverviewHelp(): void {
|
|
3623
|
+
console.log(
|
|
3624
|
+
"Atlas CLI — profile databases, generate semantic layers, and query your data.\n\n" +
|
|
3625
|
+
"Usage: atlas <command> [options]\n\n" +
|
|
3626
|
+
"Commands:\n" +
|
|
3627
|
+
" init Profile DB and generate semantic layer\n" +
|
|
3628
|
+
" import Import semantic YAML files from disk into DB\n" +
|
|
3629
|
+
" index Rebuild or inspect the semantic index\n" +
|
|
3630
|
+
" learn Analyze audit log and propose YAML improvements\n" +
|
|
3631
|
+
" diff Compare DB schema against existing semantic layer\n" +
|
|
3632
|
+
" query Ask a question via the Atlas API\n" +
|
|
3633
|
+
" validate Validate config, semantic layer, and connectivity\n" +
|
|
3634
|
+
" doctor Alias for validate\n" +
|
|
3635
|
+
" eval Run eval pipeline against demo schemas\n" +
|
|
3636
|
+
" smoke Run E2E smoke tests against a running Atlas deployment\n" +
|
|
3637
|
+
" migrate Generate/apply plugin schema migrations\n" +
|
|
3638
|
+
" plugin Manage plugins (list, create, add)\n" +
|
|
3639
|
+
" benchmark Run BIRD benchmark for text-to-SQL accuracy\n" +
|
|
3640
|
+
" mcp Start MCP server (stdio or SSE transport)\n" +
|
|
3641
|
+
" completions Output shell completion script (bash, zsh, fish)\n\n" +
|
|
3642
|
+
"Run atlas <command> --help for detailed usage of any command."
|
|
3643
|
+
);
|
|
3644
|
+
}
|
|
3645
|
+
|
|
3646
|
+
/** Check if args contain --help or -h for a subcommand. */
|
|
3647
|
+
function wantsHelp(args: string[]): boolean {
|
|
3648
|
+
return args.includes("--help") || args.includes("-h");
|
|
3649
|
+
}
|
|
3650
|
+
|
|
4572
3651
|
// --- Main ---
|
|
4573
3652
|
|
|
4574
3653
|
async function main() {
|
|
4575
3654
|
const args = process.argv.slice(2);
|
|
4576
3655
|
const command = args[0];
|
|
4577
3656
|
|
|
3657
|
+
// Top-level help: atlas --help, atlas -h, or no command
|
|
3658
|
+
if (!command || command === "--help" || command === "-h") {
|
|
3659
|
+
printOverviewHelp();
|
|
3660
|
+
process.exit(0);
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
// Per-subcommand --help
|
|
3664
|
+
if (wantsHelp(args) && command in SUBCOMMAND_HELP) {
|
|
3665
|
+
printSubcommandHelp(SUBCOMMAND_HELP[command]);
|
|
3666
|
+
process.exit(0);
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
await checkEnvFile(command);
|
|
3670
|
+
|
|
4578
3671
|
if (command === "query") {
|
|
4579
3672
|
return handleQuery(args);
|
|
4580
3673
|
}
|
|
@@ -4601,17 +3694,28 @@ async function main() {
|
|
|
4601
3694
|
}
|
|
4602
3695
|
|
|
4603
3696
|
if (command === "doctor") {
|
|
4604
|
-
|
|
4605
|
-
|
|
3697
|
+
// doctor is an alias for validate with relaxed exit codes:
|
|
3698
|
+
// Sandbox and Internal DB failures don't contribute to exit 1
|
|
3699
|
+
const { runValidate } = await import("../src/validate");
|
|
3700
|
+
const exitCode = await runValidate({ mode: "doctor" });
|
|
4606
3701
|
process.exit(exitCode);
|
|
4607
3702
|
}
|
|
4608
3703
|
|
|
4609
3704
|
if (command === "validate") {
|
|
4610
3705
|
const { runValidate } = await import("../src/validate");
|
|
4611
|
-
const
|
|
3706
|
+
const offline = args.includes("--offline");
|
|
3707
|
+
const exitCode = await runValidate({ offline });
|
|
4612
3708
|
process.exit(exitCode);
|
|
4613
3709
|
}
|
|
4614
3710
|
|
|
3711
|
+
if (command === "index") {
|
|
3712
|
+
return handleIndex(args);
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3715
|
+
if (command === "learn") {
|
|
3716
|
+
return handleLearn(args);
|
|
3717
|
+
}
|
|
3718
|
+
|
|
4615
3719
|
if (command === "diff") {
|
|
4616
3720
|
return handleDiff(args);
|
|
4617
3721
|
}
|
|
@@ -4676,6 +3780,10 @@ async function main() {
|
|
|
4676
3780
|
return;
|
|
4677
3781
|
}
|
|
4678
3782
|
|
|
3783
|
+
if (command === "import") {
|
|
3784
|
+
return handleImport(args);
|
|
3785
|
+
}
|
|
3786
|
+
|
|
4679
3787
|
if (command === "migrate") {
|
|
4680
3788
|
return handleMigrate(args);
|
|
4681
3789
|
}
|
|
@@ -4685,56 +3793,8 @@ async function main() {
|
|
|
4685
3793
|
}
|
|
4686
3794
|
|
|
4687
3795
|
if (command !== "init") {
|
|
4688
|
-
console.
|
|
4689
|
-
|
|
4690
|
-
"Commands:\n" +
|
|
4691
|
-
" init Profile DB and generate semantic layer\n" +
|
|
4692
|
-
" diff Compare DB schema against existing semantic layer\n" +
|
|
4693
|
-
" query Ask a question via the Atlas API\n" +
|
|
4694
|
-
" doctor Validate environment, connectivity, and configuration\n" +
|
|
4695
|
-
" validate Check config and semantic layer YAML files (offline)\n" +
|
|
4696
|
-
" eval Run eval pipeline against demo schemas\n" +
|
|
4697
|
-
" smoke Run E2E smoke tests against a running Atlas deployment\n" +
|
|
4698
|
-
" migrate Generate/apply plugin schema migrations\n" +
|
|
4699
|
-
" plugin Manage plugins (list, create, add)\n" +
|
|
4700
|
-
" benchmark Run BIRD benchmark for text-to-SQL accuracy\n" +
|
|
4701
|
-
" mcp Start MCP server (stdio default, --transport sse --port N for SSE)\n" +
|
|
4702
|
-
" completions Output shell completion script (bash, zsh, fish)\n\n" +
|
|
4703
|
-
"Options (init):\n" +
|
|
4704
|
-
" --tables t1,t2 Only specific tables/views\n" +
|
|
4705
|
-
" --schema <name> PostgreSQL schema (default: public)\n" +
|
|
4706
|
-
" --source <name> Write to semantic/{name}/ subdirectory (per-source layout)\n" +
|
|
4707
|
-
" --connection <name> Profile a datasource from atlas.config.ts\n" +
|
|
4708
|
-
" --csv file1.csv[,...] Load CSV files via DuckDB (no DB server needed)\n" +
|
|
4709
|
-
" --parquet f1.parquet[,...] Load Parquet files via DuckDB\n" +
|
|
4710
|
-
" --enrich Profile + LLM enrichment (needs API key)\n" +
|
|
4711
|
-
" --no-enrich Explicitly skip LLM enrichment\n" +
|
|
4712
|
-
" --demo [simple|cybersec|ecommerce] Load demo dataset then profile\n\n" +
|
|
4713
|
-
"Options (diff):\n" +
|
|
4714
|
-
" --tables t1,t2 Only diff specific tables/views\n" +
|
|
4715
|
-
" --schema <name> PostgreSQL schema (default: public)\n" +
|
|
4716
|
-
" --source <name> Read from semantic/{name}/ subdirectory\n\n" +
|
|
4717
|
-
"Options (query):\n" +
|
|
4718
|
-
' atlas query "question" Ask a question (requires running API server)\n' +
|
|
4719
|
-
" --json Raw JSON output (pipe-friendly)\n" +
|
|
4720
|
-
" --csv CSV output (headers + rows, no narrative)\n" +
|
|
4721
|
-
" --quiet Data only — no narrative, SQL, or stats\n" +
|
|
4722
|
-
" --auto-approve Auto-approve any pending actions\n" +
|
|
4723
|
-
" --connection <id> Query a specific datasource\n\n" +
|
|
4724
|
-
"Options (migrate):\n" +
|
|
4725
|
-
" atlas migrate Dry run — show SQL that would be executed\n" +
|
|
4726
|
-
" --apply Execute migrations against internal DB\n\n" +
|
|
4727
|
-
"Options (smoke):\n" +
|
|
4728
|
-
" --target <url> API base URL (default: http://localhost:3001)\n" +
|
|
4729
|
-
" --api-key <key> Bearer auth token\n" +
|
|
4730
|
-
" --timeout <ms> Per-check timeout (default: 30000)\n" +
|
|
4731
|
-
" --verbose Show full response bodies on failure\n" +
|
|
4732
|
-
" --json Machine-readable JSON output\n\n" +
|
|
4733
|
-
"Options (plugin):\n" +
|
|
4734
|
-
" atlas plugin list List installed plugins\n" +
|
|
4735
|
-
" atlas plugin create <name> --type <type> Scaffold a new plugin\n" +
|
|
4736
|
-
" atlas plugin add <package-name> Install a plugin package"
|
|
4737
|
-
);
|
|
3796
|
+
console.error(`Unknown command: ${command}\n`);
|
|
3797
|
+
printOverviewHelp();
|
|
4738
3798
|
process.exit(1);
|
|
4739
3799
|
}
|
|
4740
3800
|
|
|
@@ -4744,6 +3804,7 @@ async function main() {
|
|
|
4744
3804
|
const sourceArg = requireFlagIdentifier(args, "--source", "source name");
|
|
4745
3805
|
const connectionArg = requireFlagIdentifier(args, "--connection", "connection name");
|
|
4746
3806
|
const demoDataset = parseDemoArg(args);
|
|
3807
|
+
const forceInit = args.includes("--force");
|
|
4747
3808
|
const csvArg = getFlag(args, "--csv");
|
|
4748
3809
|
const parquetArg = getFlag(args, "--parquet");
|
|
4749
3810
|
const hasDocumentFiles = !!(csvArg || parquetArg);
|
|
@@ -4792,15 +3853,31 @@ async function main() {
|
|
|
4792
3853
|
// Profile the DuckDB database
|
|
4793
3854
|
console.log("Profiling DuckDB tables...\n");
|
|
4794
3855
|
const duckFilterTables = filterTables ?? tableNames;
|
|
4795
|
-
const
|
|
3856
|
+
const duckProgress = createProgressTracker();
|
|
3857
|
+
const duckStart = Date.now();
|
|
3858
|
+
const duckResult = await profileDuckDB(dbPath, duckFilterTables, undefined, duckProgress);
|
|
3859
|
+
let { profiles } = duckResult;
|
|
3860
|
+
duckProgress.onComplete(profiles.length, Date.now() - duckStart);
|
|
4796
3861
|
|
|
4797
3862
|
if (profiles.length === 0) {
|
|
4798
3863
|
console.error("\nError: No tables were successfully profiled.");
|
|
4799
3864
|
process.exit(1);
|
|
4800
3865
|
}
|
|
4801
3866
|
|
|
3867
|
+
// Warn about any profiling errors
|
|
3868
|
+
if (duckResult.errors.length > 0) {
|
|
3869
|
+
const total = profiles.length + duckResult.errors.length;
|
|
3870
|
+
logProfilingErrors(duckResult.errors, total);
|
|
3871
|
+
const { shouldAbort } = checkFailureThreshold(duckResult, forceInit);
|
|
3872
|
+
if (shouldAbort) {
|
|
3873
|
+
console.error(`\nUse \`--force\` to continue anyway.`);
|
|
3874
|
+
process.exit(1);
|
|
3875
|
+
}
|
|
3876
|
+
console.warn(`Continuing with ${profiles.length} successfully profiled tables.\n`);
|
|
3877
|
+
}
|
|
3878
|
+
|
|
4802
3879
|
// Run profiler heuristics
|
|
4803
|
-
analyzeTableProfiles(profiles);
|
|
3880
|
+
profiles = analyzeTableProfiles(profiles);
|
|
4804
3881
|
|
|
4805
3882
|
console.log(`\nFound ${profiles.length} table(s):\n`);
|
|
4806
3883
|
for (const p of profiles) {
|
|
@@ -4811,6 +3888,15 @@ async function main() {
|
|
|
4811
3888
|
fs.mkdirSync(entitiesOutDir, { recursive: true });
|
|
4812
3889
|
fs.mkdirSync(metricsOutDir, { recursive: true });
|
|
4813
3890
|
|
|
3891
|
+
// Clean stale entity/metric files from previous runs
|
|
3892
|
+
for (const dir of [entitiesOutDir, metricsOutDir]) {
|
|
3893
|
+
for (const file of fs.readdirSync(dir)) {
|
|
3894
|
+
if (file.endsWith(".yml") || file.endsWith(".yaml")) {
|
|
3895
|
+
fs.unlinkSync(path.join(dir, file));
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
|
|
4814
3900
|
console.log(`\nGenerating semantic layer...\n`);
|
|
4815
3901
|
|
|
4816
3902
|
// DuckDB uses PostgreSQL-compatible SQL — "public" schema is not meaningful
|
|
@@ -4889,6 +3975,20 @@ Next steps:
|
|
|
4889
3975
|
shouldEnrich = providerConfigured;
|
|
4890
3976
|
}
|
|
4891
3977
|
|
|
3978
|
+
// --- Detect org-scoped mode ---
|
|
3979
|
+
// When DATABASE_URL is set and managed auth is active, atlas init writes
|
|
3980
|
+
// to semantic/.orgs/{orgId}/ and auto-imports to the internal DB.
|
|
3981
|
+
const noImport = args.includes("--no-import");
|
|
3982
|
+
let orgId: string | undefined;
|
|
3983
|
+
if (process.env.DATABASE_URL && process.env.BETTER_AUTH_SECRET) {
|
|
3984
|
+
// Org-scoped mode is available. The orgId comes from the active session.
|
|
3985
|
+
// For CLI use, accept ATLAS_ORG_ID env var or --org flag.
|
|
3986
|
+
orgId = getFlag(args, "--org") ?? process.env.ATLAS_ORG_ID;
|
|
3987
|
+
if (orgId) {
|
|
3988
|
+
console.log(`Org-scoped mode: writing to semantic/.orgs/${orgId}/\n`);
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
|
|
4892
3992
|
// --- Resolve datasource list ---
|
|
4893
3993
|
|
|
4894
3994
|
// Try loading atlas.config.ts
|
|
@@ -5048,6 +4148,8 @@ Next steps:
|
|
|
5048
4148
|
shouldEnrich,
|
|
5049
4149
|
explicitEnrich,
|
|
5050
4150
|
demoDataset: isMultiSource ? null : demoDataset,
|
|
4151
|
+
force: forceInit,
|
|
4152
|
+
orgId,
|
|
5051
4153
|
});
|
|
5052
4154
|
} catch (err) {
|
|
5053
4155
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -5071,6 +4173,66 @@ Next steps:
|
|
|
5071
4173
|
console.error(`${"=".repeat(60)}`);
|
|
5072
4174
|
process.exit(1);
|
|
5073
4175
|
}
|
|
4176
|
+
|
|
4177
|
+
// --- Auto-import to DB in org-scoped mode ---
|
|
4178
|
+
if (orgId && !noImport) {
|
|
4179
|
+
console.log("\nImporting entities to internal DB...\n");
|
|
4180
|
+
|
|
4181
|
+
const apiUrl = process.env.ATLAS_API_URL ?? "http://localhost:3001";
|
|
4182
|
+
const importUrl = `${apiUrl}/api/v1/admin/semantic/org/import`;
|
|
4183
|
+
const importHeaders: Record<string, string> = { "Content-Type": "application/json" };
|
|
4184
|
+
if (process.env.ATLAS_API_KEY) importHeaders.Authorization = `Bearer ${process.env.ATLAS_API_KEY}`;
|
|
4185
|
+
|
|
4186
|
+
// For each datasource, import with its connection ID
|
|
4187
|
+
let anyImported = false;
|
|
4188
|
+
for (const ds of datasources) {
|
|
4189
|
+
const importBody: Record<string, string> = {};
|
|
4190
|
+
if (ds.id !== "default") importBody.connectionId = ds.id;
|
|
4191
|
+
|
|
4192
|
+
try {
|
|
4193
|
+
const resp = await fetch(importUrl, {
|
|
4194
|
+
method: "POST",
|
|
4195
|
+
headers: importHeaders,
|
|
4196
|
+
body: JSON.stringify(importBody),
|
|
4197
|
+
signal: AbortSignal.timeout(60_000),
|
|
4198
|
+
});
|
|
4199
|
+
|
|
4200
|
+
if (resp.ok) {
|
|
4201
|
+
const result = await resp.json() as { imported: number; skipped: number; total: number };
|
|
4202
|
+
console.log(` Imported ${result.imported} entities${ds.id !== "default" ? ` (connection: ${ds.id})` : ""}`);
|
|
4203
|
+
if (result.imported > 0) anyImported = true;
|
|
4204
|
+
} else {
|
|
4205
|
+
let errorMsg = `HTTP ${resp.status}`;
|
|
4206
|
+
try {
|
|
4207
|
+
const json = await resp.json() as { message?: string; error?: string };
|
|
4208
|
+
errorMsg = json.message ?? json.error ?? errorMsg;
|
|
4209
|
+
} catch {
|
|
4210
|
+
// intentionally ignored: JSON parse failed, fall through to text() attempt
|
|
4211
|
+
errorMsg = await resp.text().catch(() => errorMsg);
|
|
4212
|
+
}
|
|
4213
|
+
console.warn(` Warning: Import failed for ${ds.id}: ${errorMsg}`);
|
|
4214
|
+
console.warn(" Run 'atlas import' later to retry.\n");
|
|
4215
|
+
}
|
|
4216
|
+
} catch (err) {
|
|
4217
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
4218
|
+
if (detail.includes("ECONNREFUSED") || detail.includes("fetch failed")) {
|
|
4219
|
+
console.warn(" Warning: Atlas API not reachable — skipping auto-import.");
|
|
4220
|
+
console.warn(" Set ATLAS_API_URL if the API is not on localhost:3001");
|
|
4221
|
+
console.warn(" Start the API server and run 'atlas import' to import manually.\n");
|
|
4222
|
+
break; // Don't try remaining datasources
|
|
4223
|
+
}
|
|
4224
|
+
console.warn(` Warning: Import failed for ${ds.id}: ${detail}`);
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
if (!anyImported && datasources.length > 0) {
|
|
4229
|
+
console.warn("\nNo entities were imported to the DB. Files were written to disk successfully.");
|
|
4230
|
+
console.warn("Run 'atlas import' once the API server is available to complete the import.");
|
|
4231
|
+
if (!process.env.ATLAS_API_KEY) {
|
|
4232
|
+
console.warn("Hint: set ATLAS_API_KEY for CLI authentication.\n");
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
5074
4236
|
}
|
|
5075
4237
|
|
|
5076
4238
|
export function getFlag(args: string[], flag: string): string | undefined {
|