Repository: innoverio/vscode-dbt-power-user Branch: master Commit: d73424bc0792 Files: 654 Total size: 6.3 MB Directory structure: gitextract_u70y3h9w/ ├── .eslintrc.json ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yaml │ │ ├── config.yml │ │ ├── feature_request.yml │ │ └── sweep-template.yml │ ├── actions/ │ │ └── common-build/ │ │ └── action.yml │ ├── dependabot.yml │ ├── pull_request_template.md │ ├── scripts/ │ │ ├── analyze-vsix.py │ │ └── generate-bundle-report.py │ └── workflows/ │ ├── ci.yml │ ├── deploy-docs-to-s3.yaml │ └── tests.yml ├── .gitignore ├── .gitpod.yml ├── .husky/ │ └── pre-commit ├── .mcp.json ├── .nycrc.json ├── .prettierignore ├── .prettierrc ├── .vscodeignore ├── CLAUDE.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── altimate_notebook_kernel.py ├── codecov.yml ├── docker-setup/ │ ├── Dockerfile │ ├── README.md │ ├── deploy.sh │ ├── docker-compose.yml │ └── start-code-server.sh ├── documentation/ │ ├── docs/ │ │ ├── arch/ │ │ │ ├── beta.md │ │ │ ├── faq.md │ │ │ ├── llm-gateway.md │ │ │ └── pricingfaq.md │ │ ├── develop/ │ │ │ ├── autocomplete.md │ │ │ ├── clicktorun.md │ │ │ ├── compiledCode.md │ │ │ ├── explanation.md │ │ │ ├── genmodelSQL.md │ │ │ ├── genmodelSource.md │ │ │ ├── translateSQL.md │ │ │ └── updatemodel.md │ │ ├── discover/ │ │ │ ├── setupui.md │ │ │ ├── viewdocs.md │ │ │ └── viewlineage.md │ │ ├── document/ │ │ │ ├── docblocks.md │ │ │ ├── generatedoc.md │ │ │ └── write.md │ │ ├── govern/ │ │ │ ├── collaboration.md │ │ │ ├── governance.md │ │ │ ├── multiproject.md │ │ │ ├── notebooks.md │ │ │ └── querybookmarks.md │ │ ├── index.md │ │ ├── javascripts/ │ │ │ └── feedback,js │ │ ├── overrides/ │ │ │ ├── .gitkeep │ │ │ └── main.html │ │ ├── setup/ │ │ │ ├── configuration.md │ │ │ ├── cursor_installation_workaround.md │ │ │ ├── faq.md │ │ │ ├── installation.md │ │ │ ├── optConfig.md │ │ │ ├── reqdConfig.md │ │ │ ├── reqdConfigCloud.md │ │ │ ├── reqdConfigFusion.md │ │ │ └── sso.md │ │ ├── studio.md │ │ ├── stylesheets/ │ │ │ └── extra.css │ │ ├── teammates/ │ │ │ ├── altimate-code.md │ │ │ ├── coach.md │ │ │ └── introduction.md │ │ ├── test/ │ │ │ ├── adhocquery.md │ │ │ ├── bigquerycost.md │ │ │ ├── defertoprod.md │ │ │ ├── lineage.md │ │ │ ├── queryResults.md │ │ │ ├── runctes.md │ │ │ ├── runtests.md │ │ │ ├── sqlvalidation.md │ │ │ ├── sqlvisualizer.md │ │ │ ├── utilities.md │ │ │ └── writetests.md │ │ └── troubleshooting.md │ ├── mkdocs.yml │ ├── readme.md │ └── requirements.txt ├── jest.config.js ├── monitoring/ │ ├── README.md │ ├── app.py │ ├── github_issues/ │ │ ├── app.py │ │ ├── requirements.txt │ │ └── templates/ │ │ └── index.html │ ├── requirements.txt │ └── templates/ │ └── index.html ├── package.json ├── package.nls.json ├── postInstall.js ├── prepareBuild.js ├── rsbuild.config.ts ├── snippets/ │ ├── snippets_markdown.json │ ├── snippets_sql.json │ └── snippets_yaml.json ├── src/ │ ├── altimate.ts │ ├── autocompletion_provider/ │ │ ├── docAutocompletionProvider.ts │ │ ├── index.ts │ │ ├── macroAutocompletionProvider.ts │ │ ├── modelAutocompletionProvider.ts │ │ ├── sourceAutocompletionProvider.ts │ │ └── usercompletion_provider.ts │ ├── code_lens_provider/ │ │ ├── cteCodeLensProvider.ts │ │ ├── index.ts │ │ ├── sourceModelCreationCodeLensProvider.ts │ │ ├── sqlActionsCodeLensProvider.ts │ │ └── virtualSqlCodeLensProvider.ts │ ├── commands/ │ │ ├── altimateScan.ts │ │ ├── bigQueryCostEstimate.ts │ │ ├── index.ts │ │ ├── runModel.ts │ │ ├── runTest.ts │ │ ├── sqlToModel.ts │ │ ├── tests/ │ │ │ ├── initCatalog.ts │ │ │ ├── missingSchemaTest.ts │ │ │ ├── scanContext.ts │ │ │ ├── staleModelColumnTest.ts │ │ │ ├── step.ts │ │ │ ├── undocumentedModelColumnTest.ts │ │ │ └── unmaterializedModelTest.ts │ │ ├── validateSql.ts │ │ └── walkthroughCommands.ts │ ├── comment_provider/ │ │ ├── conversationProvider.ts │ │ └── index.ts │ ├── content_provider/ │ │ ├── index.ts │ │ └── sqlPreviewContentProvider.ts │ ├── cte_profiler/ │ │ ├── cteProfilerDecorationProvider.ts │ │ ├── cteProfilerService.ts │ │ └── cteProfilerTypes.ts │ ├── dbtPowerUserExtension.ts │ ├── dbt_client/ │ │ ├── datapilot.ts │ │ ├── dbtProject.ts │ │ ├── dbtProjectContainer.ts │ │ ├── dbtProjectLog.ts │ │ ├── dbtVersionEvent.ts │ │ ├── dbtWorkspaceFolder.ts │ │ ├── event/ │ │ │ ├── manifestCacheChangedEvent.ts │ │ │ ├── projectConfigChangedEvent.ts │ │ │ └── runResultsEvent.ts │ │ ├── index.ts │ │ ├── pythonEnvironment.ts │ │ ├── runtimePythonEnvironmentProvider.ts │ │ ├── vscodeConfiguration.ts │ │ └── vscodeTerminal.ts │ ├── definition_provider/ │ │ ├── docDefinitionProvider.ts │ │ ├── index.ts │ │ ├── macroDefinitionProvider.ts │ │ ├── modelDefinitionProvider.ts │ │ └── sourceDefinitionProvider.ts │ ├── document_formatting_edit_provider/ │ │ ├── dbtDocumentFormattingEditProvider.ts │ │ └── index.ts │ ├── extension.ts │ ├── hover_provider/ │ │ ├── depthDecorationProvider.ts │ │ ├── index.ts │ │ ├── macroHoverProvider.ts │ │ ├── modelHoverProvider.ts │ │ ├── sourceHoverProvider.ts │ │ ├── utils.ts │ │ └── yamlModelHoverProvider.ts │ ├── inversify.config.ts │ ├── lib/ │ │ ├── index.d.ts │ │ └── index.js │ ├── mcp/ │ │ ├── index.ts │ │ ├── server.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── modules.ts │ ├── quickpick/ │ │ ├── actionsQuickPick.ts │ │ ├── index.ts │ │ ├── notebookQuickPick.ts │ │ ├── projectQuickPick.ts │ │ └── sqlQuickPick.ts │ ├── services/ │ │ ├── altimateAuthService.ts │ │ ├── altimateCodeChatService.ts │ │ ├── conversationService.ts │ │ ├── dbtLineageService.ts │ │ ├── dbtTestService.ts │ │ ├── diagnosticsOutputChannel.ts │ │ ├── docGenService.ts │ │ ├── fileService.ts │ │ ├── queryAnalysisService.ts │ │ ├── queryManifestService.ts │ │ ├── runHistoryService.ts │ │ ├── sharedStateService.ts │ │ ├── streamingService.ts │ │ └── usersService.ts │ ├── statusbar/ │ │ ├── deferToProductionStatusBar.ts │ │ ├── index.ts │ │ ├── targetStatusBar.ts │ │ └── versionStatusBar.ts │ ├── telemetry/ │ │ ├── events.ts │ │ └── index.ts │ ├── test/ │ │ ├── common.ts │ │ ├── fixtures/ │ │ │ ├── formatter/ │ │ │ │ ├── README.md │ │ │ │ ├── add-trailing-newline.sql │ │ │ │ ├── basic-select-reformat.sql │ │ │ │ ├── delete-blank-lines.sql │ │ │ │ ├── issue-1717-exact-reproducer.sql │ │ │ │ ├── issue-1717-long-lines.sql │ │ │ │ ├── jinja-source-long-line.sql │ │ │ │ ├── long-line-split-to-multiple.sql │ │ │ │ ├── multi-chunk-changes.sql │ │ │ │ └── no-changes-needed.sql │ │ │ └── runHistory.ts │ │ ├── integration/ │ │ │ ├── formatter.test.ts │ │ │ ├── helpers/ │ │ │ │ ├── fixtureLoader.ts │ │ │ │ └── sqlfmtRunner.ts │ │ │ ├── index.ts │ │ │ ├── runHistoryTreeview.test.ts │ │ │ └── runTests.ts │ │ ├── mock/ │ │ │ ├── lib.ts │ │ │ ├── node-fetch.ts │ │ │ └── vscode.ts │ │ ├── setup.ts │ │ └── suite/ │ │ ├── altimate.test.ts │ │ ├── commandProcessExecution.test.ts │ │ ├── conversationProvider.test.ts │ │ ├── coverage.ts │ │ ├── cteCodeLensProvider.test.ts │ │ ├── dbtCloudDetection.test.ts │ │ ├── dbtCoreDetection.test.ts │ │ ├── dbtCoreIntegration.test.ts │ │ ├── dbtIntegration.test.ts │ │ ├── dbtProject.test.ts │ │ ├── dbtProjectContainer.test.ts │ │ ├── dbtTerminal.test.ts │ │ ├── dbtWorkspaceFolder.test.ts │ │ ├── diagnosticsOutputChannel.test.ts │ │ ├── extension.test.ts │ │ ├── index.ts │ │ ├── jinjaGrammar.test.ts │ │ ├── jupyterlabServicesCompat.test.ts │ │ ├── macroDefinitionProvider.test.ts │ │ ├── mcpSchemaCompat.test.ts │ │ ├── newLineagePanel.test.ts │ │ ├── resolveSettingsVariables.test.ts │ │ ├── runHistoryService.test.ts │ │ ├── runHistoryTreeItems.test.ts │ │ ├── runHistoryTreeviewProvider.test.ts │ │ ├── runTest.test.ts │ │ ├── runTest.ts │ │ ├── runtimePythonEnvironmentProvider.test.ts │ │ ├── testParser.test.ts │ │ └── utils.test.ts │ ├── treeview_provider/ │ │ ├── index.ts │ │ ├── modelTreeviewProvider.ts │ │ ├── runHistoryTreeItems.ts │ │ └── runHistoryTreeviewProvider.ts │ ├── types/ │ │ └── istanbul-lib-instrument.d.ts │ ├── types.ts │ ├── utils.ts │ ├── validation_provider/ │ │ └── index.ts │ └── webview_provider/ │ ├── DbtDocsView.ts │ ├── altimateWebviewProvider.ts │ ├── datapilotPanel.ts │ ├── docsEditPanel.ts │ ├── index.ts │ ├── insightsPanel.ts │ ├── lineagePanel.ts │ ├── newDocsGenPanel.ts │ ├── newLineagePanel.ts │ ├── onboardingPanel.ts │ ├── queryResultPanel.ts │ └── sqlLineagePanel.ts ├── sweep.yaml ├── syntaxes/ │ ├── jinja-sql.tmLanguage.json │ └── jinja-yaml.tmLanguage.json ├── test-fixtures/ │ ├── .gitignore │ ├── dbt-core-sample-duckdb/ │ │ ├── .coveragerc │ │ ├── .github/ │ │ │ ├── dependabot.yml │ │ │ └── workflows/ │ │ │ ├── gosales_ci_pylint.yml │ │ │ ├── gosales_ci_sonarcloud.yml │ │ │ ├── gosales_ci_sqlfluff.yml │ │ │ └── static.yml │ │ ├── .sqlfluff │ │ ├── .user.yml │ │ ├── README.md │ │ ├── SECURITY.md │ │ ├── assets/ │ │ │ ├── go-sales-erd.drawio │ │ │ └── hld-duckdb-dbt-sample.drawio │ │ ├── coverage.xml │ │ ├── dbt_project.yml │ │ ├── macros/ │ │ │ ├── .gitkeep │ │ │ ├── custom_schema.sql │ │ │ ├── scd2_hash.sql │ │ │ └── scd2_ts.sql │ │ ├── models/ │ │ │ ├── 01-raw/ │ │ │ │ ├── t_raw_go_1k.py │ │ │ │ ├── t_raw_go_1k.yml │ │ │ │ ├── t_raw_go_daily_sales.py │ │ │ │ ├── t_raw_go_daily_sales.yml │ │ │ │ ├── t_raw_go_products.py │ │ │ │ ├── t_raw_go_products.yml │ │ │ │ ├── t_raw_go_retailers.py │ │ │ │ └── t_raw_go_retailers.yml │ │ │ ├── 02-stg/ │ │ │ │ ├── t_stg_go_1k.sql │ │ │ │ ├── t_stg_go_1k.yml │ │ │ │ ├── t_stg_go_daily_sales.sql │ │ │ │ ├── t_stg_go_daily_sales.yml │ │ │ │ ├── t_stg_go_methods.sql │ │ │ │ ├── t_stg_go_methods.yml │ │ │ │ ├── t_stg_go_products.sql │ │ │ │ ├── t_stg_go_products.yml │ │ │ │ ├── t_stg_go_retailers.sql │ │ │ │ └── t_stg_go_retailers.yml │ │ │ ├── 03-det/ │ │ │ │ ├── t_dim_dates.sql │ │ │ │ ├── t_dim_dates.yml │ │ │ │ ├── t_dim_order_methods.sql │ │ │ │ ├── t_dim_order_methods.yml │ │ │ │ ├── t_dim_products.sql │ │ │ │ ├── t_dim_products.yml │ │ │ │ ├── t_dim_retailers.sql │ │ │ │ ├── t_dim_retailers.yml │ │ │ │ ├── t_fct_sales.sql │ │ │ │ └── t_fct_sales.yml │ │ │ └── 04-mrt/ │ │ │ ├── t_mrt_sales.sql │ │ │ └── t_mrt_sales.yml │ │ ├── packages.yml │ │ ├── profiles.yml │ │ ├── requirements.txt │ │ ├── seeds/ │ │ │ ├── ref_go_methods.csv │ │ │ └── seeds.yml │ │ ├── shared_utils/ │ │ │ ├── __init__.py │ │ │ ├── config.py │ │ │ └── db_utils.py │ │ ├── snapshots/ │ │ │ └── .gitkeep │ │ ├── sonar-project.properites │ │ └── tests/ │ │ ├── .gitkeep │ │ ├── test_config.py │ │ ├── test_data.py │ │ └── test_db_utils.py │ └── jaffle-shop-duckdb/ │ ├── .devcontainer.json │ ├── .github/ │ │ ├── CODEOWNERS │ │ └── workflows/ │ │ └── validate_on_platforms.yml │ ├── .gitignore │ ├── .python-version │ ├── .sqlfluff │ ├── .sqlfluffignore │ ├── Dockerfile │ ├── LICENSE │ ├── README.md │ ├── RELEASE.md │ ├── dbt-completion.bash │ ├── dbt_project.yml │ ├── etc/ │ │ └── dbdiagram_definition.txt │ ├── models/ │ │ ├── customers.sql │ │ ├── docs.md │ │ ├── orders.sql │ │ ├── overview.md │ │ ├── schema.yml │ │ └── staging/ │ │ ├── schema.yml │ │ ├── stg_customers.sql │ │ ├── stg_orders.sql │ │ └── stg_payments.sql │ ├── profiles.yml │ ├── pyproject.toml │ ├── requirements.txt │ └── seeds/ │ ├── .gitkeep │ ├── raw_customers.csv │ ├── raw_orders.csv │ └── raw_payments.csv ├── tsconfig.json ├── typings.d.ts └── webview_panels/ ├── .eslintrc.cjs ├── .gitignore ├── .storybook/ │ ├── __mocks__/ │ │ ├── crypto.ts │ │ └── vscode.ts │ ├── main.ts │ ├── preview-head.html │ └── preview.tsx ├── README.md ├── eslint/ │ └── typescript.cjs ├── index.html ├── package.json ├── postcss.config.mjs ├── public/ │ └── .gitkeep ├── src/ │ ├── App.tsx │ ├── AppConstants.tsx │ ├── AppRoutes.tsx │ ├── NoMatch.tsx │ ├── _variables.scss │ ├── assets/ │ │ └── icons/ │ │ ├── Images.stories.tsx │ │ ├── index.tsx │ │ └── styles.css │ ├── lib/ │ │ ├── altimate/ │ │ │ ├── DbtDocsRenderer.js │ │ │ ├── altimate-components.d.ts │ │ │ ├── altimate-components.js │ │ │ ├── main.css │ │ │ └── main.js │ │ └── index.ts │ ├── main.scss │ ├── main.tsx │ ├── modules/ │ │ ├── AutoCollapsingNotification/ │ │ │ └── AutoCollapsingNotification.tsx │ │ ├── app/ │ │ │ ├── AppProvider.tsx │ │ │ ├── appSlice.ts │ │ │ ├── indexedDb.ts │ │ │ ├── requestExecutor.ts │ │ │ ├── types.ts │ │ │ ├── useAppContext.ts │ │ │ └── useListeners.ts │ │ ├── bigQuery/ │ │ │ └── CostEstimator.tsx │ │ ├── commonActionButtons/ │ │ │ ├── CommonActionButtons.tsx │ │ │ ├── FeedbackButton.tsx │ │ │ └── HelpButton.tsx │ │ ├── dataPilot/ │ │ │ ├── DataPilotHelp.tsx │ │ │ ├── DataPilotProvider.tsx │ │ │ ├── Datapilot.stories.tsx │ │ │ ├── DefaultDatapilotView.tsx │ │ │ ├── components/ │ │ │ │ ├── common/ │ │ │ │ │ ├── AskDatapilotInput.tsx │ │ │ │ │ ├── DatapilotChatFollowup.tsx │ │ │ │ │ ├── Header.tsx │ │ │ │ │ ├── UserQuery.tsx │ │ │ │ │ └── useAiGenerationUtils.ts │ │ │ │ ├── docGen/ │ │ │ │ │ ├── AiDocActionButton.tsx │ │ │ │ │ ├── AiDocChat.tsx │ │ │ │ │ ├── DataPilotDocGen.stories.tsx │ │ │ │ │ ├── NewGenerationResults.tsx │ │ │ │ │ └── types.ts │ │ │ │ ├── queryAnalysis/ │ │ │ │ │ ├── QueryAnalysis.stories.tsx │ │ │ │ │ ├── QueryAnalysis.tsx │ │ │ │ │ ├── QueryAnalysisActionButton.tsx │ │ │ │ │ ├── commands.ts │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── QueryTranslateDialectSelects.tsx │ │ │ │ │ │ └── constants.ts │ │ │ │ │ ├── provider/ │ │ │ │ │ │ ├── QueryAnalysisProvider.tsx │ │ │ │ │ │ ├── queryAnalysisSlice.ts │ │ │ │ │ │ ├── types.ts │ │ │ │ │ │ └── useQueryAnalysisContext.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── useQueryAnalysisAction.ts │ │ │ │ └── test/ │ │ │ │ ├── AddCustomTest.tsx │ │ │ │ └── Tests.stories.tsx │ │ │ ├── constants.tsx │ │ │ ├── dataPilotSlice.ts │ │ │ ├── datapilot.module.scss │ │ │ ├── index.tsx │ │ │ ├── types.ts │ │ │ └── useDataPilotContext.ts │ │ ├── dbtDocs/ │ │ │ ├── DbtDocsView.tsx │ │ │ └── ShareDbtDocsButton.tsx │ │ ├── defer/ │ │ │ ├── DeferToProduction.tsx │ │ │ ├── ManifestSelection.tsx │ │ │ ├── constants.ts │ │ │ ├── defer.module.scss │ │ │ └── types.ts │ │ ├── documentationEditor/ │ │ │ ├── DocumentationEditor.stories.tsx │ │ │ ├── DocumentationEditor.tsx │ │ │ ├── DocumentationProvider.tsx │ │ │ ├── components/ │ │ │ │ ├── conversation/ │ │ │ │ │ ├── AddCoversationButton.tsx │ │ │ │ │ ├── ConversationsRightPanel.tsx │ │ │ │ │ └── ShowConversationsButton.tsx │ │ │ │ ├── docGenerator/ │ │ │ │ │ ├── BulkGenerateButton.tsx │ │ │ │ │ ├── Citations.tsx │ │ │ │ │ ├── CoachAi.tsx │ │ │ │ │ ├── CoachAiIfModified.tsx │ │ │ │ │ ├── DocBlockInserter.module.scss │ │ │ │ │ ├── DocBlockInserter.tsx │ │ │ │ │ ├── DocGenSelectedColumns.tsx │ │ │ │ │ ├── DocGeneratorColumn.tsx │ │ │ │ │ ├── DocGeneratorColumnsList.tsx │ │ │ │ │ ├── DocGeneratorInput.tsx │ │ │ │ │ ├── GenerateButton.tsx │ │ │ │ │ ├── SyncWithDatabase.tsx │ │ │ │ │ ├── coachAi.module.scss │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── docGenInput.module.scss │ │ │ │ │ └── generateAll.module.scss │ │ │ │ ├── documentationPropagation/ │ │ │ │ │ ├── DocumentationPropagation.tsx │ │ │ │ │ └── styles.module.scss │ │ │ │ ├── help/ │ │ │ │ │ ├── DocumentationHelpContent.tsx │ │ │ │ │ └── TestsHelpContent.tsx │ │ │ │ ├── model/ │ │ │ │ │ └── Options.tsx │ │ │ │ ├── result/ │ │ │ │ │ ├── DocGenerationResult.tsx │ │ │ │ │ ├── DocumentationResult.stories.tsx │ │ │ │ │ └── DocumentationResult.tsx │ │ │ │ ├── saveDocumentation/ │ │ │ │ │ └── SaveDocumentation.tsx │ │ │ │ ├── score/ │ │ │ │ │ ├── Score.stories.tsx │ │ │ │ │ ├── Score.tsx │ │ │ │ │ └── styles.module.scss │ │ │ │ ├── search/ │ │ │ │ │ └── SearchColumnsInput.tsx │ │ │ │ ├── settings/ │ │ │ │ │ └── DocGeneratorSettings.tsx │ │ │ │ ├── telemetry/ │ │ │ │ │ └── index.ts │ │ │ │ └── tests/ │ │ │ │ ├── AddTest.tsx │ │ │ │ ├── CustomTestButton.tsx │ │ │ │ ├── DbtTestCode.tsx │ │ │ │ ├── DisplayTestDetails.tsx │ │ │ │ ├── EntityWithTests.tsx │ │ │ │ ├── Test.tsx │ │ │ │ ├── TestDetails.tsx │ │ │ │ ├── forms/ │ │ │ │ │ ├── AcceptedValues.tsx │ │ │ │ │ ├── Relationships.tsx │ │ │ │ │ └── TestForm.tsx │ │ │ │ ├── hooks/ │ │ │ │ │ └── useTestFormSave.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ ├── state/ │ │ │ │ ├── documentationSlice.ts │ │ │ │ ├── types.ts │ │ │ │ └── useDocumentationContext.ts │ │ │ ├── styles.module.scss │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── feedback/ │ │ │ ├── ResultFeedbackButtons.tsx │ │ │ └── types.ts │ │ ├── healthCheck/ │ │ │ ├── IssueDetail.tsx │ │ │ ├── IssueList.tsx │ │ │ ├── ProjectHealthChecker.tsx │ │ │ ├── ProjectHealthNewCheckButton.tsx │ │ │ ├── healthcheck.module.scss │ │ │ └── types.ts │ │ ├── home/ │ │ │ └── Home.tsx │ │ ├── insights/ │ │ │ ├── Insights.stories.tsx │ │ │ ├── Insights.tsx │ │ │ ├── components/ │ │ │ │ └── help/ │ │ │ │ ├── HelpButton.tsx │ │ │ │ └── HelpContent.tsx │ │ │ └── insights.module.scss │ │ ├── lineage/ │ │ │ ├── ActionWidget.tsx │ │ │ ├── Demo.tsx │ │ │ ├── LineageView.tsx │ │ │ ├── MissingLineageMessage.tsx │ │ │ ├── components/ │ │ │ │ ├── demo/ │ │ │ │ │ └── DemoButton.tsx │ │ │ │ └── help/ │ │ │ │ ├── HelpButton.tsx │ │ │ │ └── HelpContent.tsx │ │ │ ├── lineage.module.scss │ │ │ ├── tailwind-globals.css │ │ │ └── types.ts │ │ ├── logger/ │ │ │ └── index.ts │ │ ├── markdown/ │ │ │ ├── PreTag.tsx │ │ │ ├── Renderer.tsx │ │ │ └── markdown.module.scss │ │ ├── newFeature/ │ │ │ └── NewFeatureIndicator.tsx │ │ ├── notebooks/ │ │ │ ├── DeleteNotebookButton.tsx │ │ │ ├── NoNotebooks.tsx │ │ │ ├── NotebookPrivacySettingButton.tsx │ │ │ ├── Notebooks.tsx │ │ │ ├── NotebooksList.tsx │ │ │ ├── notebooklist.module.scss │ │ │ └── types.ts │ │ ├── onboarding/ │ │ │ ├── AltimateSetupStep.tsx │ │ │ ├── DbtIntegrationSetup.tsx │ │ │ ├── InstallDbtStep.tsx │ │ │ ├── Onboarding.tsx │ │ │ ├── PrerequisitesStep.tsx │ │ │ ├── ProjectSetupStep.tsx │ │ │ ├── SetupWizard.tsx │ │ │ ├── TutorialsStep.tsx │ │ │ └── onboarding.module.scss │ │ ├── previewFeature/ │ │ │ ├── PreviewFeatureIcon.tsx │ │ │ └── tooltip.module.scss │ │ ├── queryPanel/ │ │ │ ├── QueryPanel.stories.tsx │ │ │ ├── QueryPanel.tsx │ │ │ ├── QueryPanelDefaultView.tsx │ │ │ ├── QueryPanelProvider.tsx │ │ │ ├── components/ │ │ │ │ ├── QueryPanelContents/ │ │ │ │ │ ├── QueryPanelContent.tsx │ │ │ │ │ ├── QueryPanelError.tsx │ │ │ │ │ ├── QueryPanelLoader.tsx │ │ │ │ │ ├── QueryPanelTitle.tsx │ │ │ │ │ └── types.ts │ │ │ │ ├── clearResultsButton/ │ │ │ │ │ └── ClearResultsButton.tsx │ │ │ │ ├── filters/ │ │ │ │ │ └── Filters.tsx │ │ │ │ ├── help/ │ │ │ │ │ ├── HelpButton.tsx │ │ │ │ │ └── HelpContent.tsx │ │ │ │ ├── openInTabButton/ │ │ │ │ │ └── OpenInTabButton.tsx │ │ │ │ ├── perspective/ │ │ │ │ │ ├── PerspectiveErrorBoundary.tsx │ │ │ │ │ ├── PerspectivePlugins.ts │ │ │ │ │ ├── PerspectiveViewer.stories.tsx │ │ │ │ │ ├── PerspectiveViewer.tsx │ │ │ │ │ ├── perspective.d.ts │ │ │ │ │ ├── perspective.module.scss │ │ │ │ │ ├── perspective.scss │ │ │ │ │ └── themes.css │ │ │ │ ├── queryLimit/ │ │ │ │ │ ├── QueryLimit.tsx │ │ │ │ │ └── styles.module.scss │ │ │ │ ├── queryPanelBookmarks/ │ │ │ │ │ ├── BookmarkAccordion.tsx │ │ │ │ │ ├── BookmarkButton.tsx │ │ │ │ │ ├── BookmarkPrivacySettingButton.tsx │ │ │ │ │ ├── DeleteBookmarkButton.tsx │ │ │ │ │ ├── QueryBookmarkRow.tsx │ │ │ │ │ ├── QueryPanelBookmarks.tsx │ │ │ │ │ └── utils.ts │ │ │ │ ├── queryPanelQueryHistory/ │ │ │ │ │ ├── ExecuteQueryButton.tsx │ │ │ │ │ ├── QueryHistoryRow.tsx │ │ │ │ │ └── QueryPanelHistory.tsx │ │ │ │ └── runAdhocQueryButton/ │ │ │ │ ├── NewNotebook.tsx │ │ │ │ └── RunAdhocQueryButton.tsx │ │ │ ├── constants.ts │ │ │ ├── context/ │ │ │ │ ├── queryPanelSlice.ts │ │ │ │ └── types.ts │ │ │ ├── querypanel.module.scss │ │ │ ├── useQueryPanelCommonActions.ts │ │ │ ├── useQueryPanelListeners.ts │ │ │ └── useQueryPanelState.ts │ │ └── vscode/ │ │ └── index.ts │ ├── notebook/ │ │ ├── index.tsx │ │ ├── renderer.module.scss │ │ └── renderer.tsx │ ├── testUtils/ │ │ ├── conversations/ │ │ │ └── index.ts │ │ ├── datapilot/ │ │ │ ├── docGen.ts │ │ │ ├── index.ts │ │ │ ├── queryAnalysis.ts │ │ │ └── test.ts │ │ ├── documentation/ │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── queryResults/ │ │ │ └── index.ts │ │ └── users/ │ │ └── index.ts │ ├── uiCore/ │ │ ├── components/ │ │ │ ├── accordion/ │ │ │ │ ├── Accordion.tsx │ │ │ │ └── accordion.module.scss │ │ │ ├── avatar/ │ │ │ │ ├── Avatar.tsx │ │ │ │ └── styles.module.scss │ │ │ ├── button/ │ │ │ │ └── Button.tsx │ │ │ ├── codeblock/ │ │ │ │ ├── codeblock.module.scss │ │ │ │ └── index.tsx │ │ │ ├── drawer/ │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.scss │ │ │ ├── dropdown/ │ │ │ │ ├── Dropdown.tsx │ │ │ │ └── dropdown.module.scss │ │ │ ├── dropdownButton/ │ │ │ │ ├── DropdownButton.tsx │ │ │ │ └── styles.module.scss │ │ │ ├── iconButton/ │ │ │ │ ├── IconButton.tsx │ │ │ │ └── styles.module.scss │ │ │ ├── loader/ │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.scss │ │ │ ├── loadingButton/ │ │ │ │ ├── index.tsx │ │ │ │ └── loadingButton.module.scss │ │ │ ├── popoverWithButton/ │ │ │ │ ├── PopoverWithButton.tsx │ │ │ │ └── styles.module.scss │ │ │ ├── select/ │ │ │ │ ├── index.tsx │ │ │ │ └── select.module.scss │ │ │ ├── stack/ │ │ │ │ ├── Stack.tsx │ │ │ │ └── stack.module.scss │ │ │ ├── tabs/ │ │ │ │ ├── Tabs.tsx │ │ │ │ └── tabs.module.scss │ │ │ ├── tag/ │ │ │ │ ├── Tag.tsx │ │ │ │ └── tag.module.scss │ │ │ └── tooltip/ │ │ │ └── Tooltip.tsx │ │ ├── index.ts │ │ ├── theme.scss │ │ └── uiToolkitStories/ │ │ ├── Colors.stories.tsx │ │ ├── Components.stories.tsx │ │ └── Typography.stories.tsx │ └── vite-env.d.ts ├── tailwind.config.ts ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.renderer.ts └── vite.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "root": true, "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": 12, "sourceType": "module" }, "plugins": ["@typescript-eslint"], "extends": [ "plugin:@typescript-eslint/eslint-recommended", // "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended" ], "rules": { "@typescript-eslint/class-name-casing": "off", // TODO: why is this also triggered for namespaces and functions? "@typescript-eslint/semi": "warn", "curly": "warn", "eqeqeq": "warn", "no-throw-literal": "warn", "indent": "off" } } ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ --- name: Bug report description: Report a bug or an issue you've found with dbt Power User labels: bug body: - type: textarea attributes: label: Expected behavior description: What do you think should have happened placeholder: > A clear and concise description of what you expected to happen. validations: required: true - type: textarea attributes: label: Actual behavior description: Describe what actually happened placeholder: > A clear and concise description of what actually happened. validations: required: true - type: textarea attributes: label: Steps To Reproduce description: This will help us reproduce your issue placeholder: > In as much detail as possible, please provide steps to reproduce the issue. Sample code that triggers the issue, relevant server settings, etc is all very helpful here. validations: required: true - type: textarea attributes: label: Log output/Screenshots description: What do you think went wrong? placeholder: > If applicable, add log output and/or screenshots to help explain your problem. - type: input attributes: label: Operating System description: What Operating System are you using? placeholder: "You can get it via `cat /etc/os-release` for example" validations: required: true - type: input attributes: label: dbt version description: "Execute `dbt --version`" placeholder: Which version of dbt are you using? validations: required: true - type: input attributes: label: dbt Adapter description: "Which adapter are you using?" validations: required: true - type: input attributes: label: dbt Power User version description: "What dbt Power User version are you using?" validations: required: true - type: checkboxes attributes: label: Are you willing to submit PR? description: > This is absolutely not required, but we are happy to guide you in the contribution process especially if you already have a good understanding of how to implement the feature. options: - label: Yes I am willing to submit a PR! - type: markdown attributes: value: "Thanks for completing our form!" ================================================ FILE: .github/ISSUE_TEMPLATE/config.yaml ================================================ --- contact_links: - name: Ask a question or get help right here url: https://github.com/innoverio/vscode-dbt-power-user/discussions about: Ask a question or get help here on Github Discussions ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ --- contact_links: - name: Ask a question or get help right here url: https://github.com/innoverio/vscode-dbt-power-user/discussions about: Ask a question or get help here on Github Discussions ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ --- name: Feature request description: Suggest an idea for dbt Power User labels: enhancement body: - type: textarea attributes: label: Describe the feature description: What would you like to happen? placeholder: > A clear and concise description of what you want to happen and what problem it would solve. validations: required: true - type: textarea attributes: label: Describe alternatives you've considered description: What did you try to make it happen? placeholder: > A clear and concise description of any alternative solutions or features you've considered. - type: textarea attributes: label: Who will benefit? placeholder: > What kind of use case will this feature be useful for? Please be specific and provide examples, this will help us prioritize properly. - type: checkboxes attributes: label: Are you willing to submit PR? description: > This is absolutely not required, but we are happy to guide you in the contribution process especially if you already have a good understanding of how to implement the feature. options: - label: Yes I am willing to submit a PR! - type: markdown attributes: value: "Thanks for completing our form!" ================================================ FILE: .github/ISSUE_TEMPLATE/sweep-template.yml ================================================ name: Sweep Issue title: 'Sweep: ' description: For small bugs, features, refactors, and tests to be handled by Sweep, an AI-powered junior developer. labels: sweep body: - type: textarea id: description attributes: label: Details description: Tell Sweep where and what to edit and provide enough context for a new developer to the codebase placeholder: | Unit Tests: Write unit tests for . Test each function in the file. Make sure to test edge cases. Bugs: The bug might be in . Here are the logs: ... Features: the new endpoint should use the ... class from because it contains ... logic. Refactors: We are migrating this function to ... version because ... - type: input id: branch attributes: label: Branch description: The branch to work off of (optional) placeholder: | main ================================================ FILE: .github/actions/common-build/action.yml ================================================ # .github/actions/common-build/action.yml name: "Common Build" description: "Common build steps for the project" inputs: vsce-target: description: "VS Code target platform (e.g., darwin-arm64). When set, only the matching altimate-core native binary is installed." required: false default: "" runs: using: "composite" steps: - name: Install Node.js uses: actions/setup-node@v4 with: node-version: "20" cache: "npm" cache-dependency-path: | package-lock.json webview_panels/package-lock.json - run: npm ci shell: bash env: VSCE_TARGET: ${{ inputs.vsce-target }} - run: npm ci shell: bash working-directory: ./webview_panels ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: # python dependencies - package-ecosystem: "npm" directory: "/" schedule: interval: "daily" rebase-strategy: "disabled" labels: - "dependencies" ================================================ FILE: .github/pull_request_template.md ================================================ ## Overview ### Problem Describe the problem you are solving. Mention the ticket/issue if applicable. ### Solution Describe the implemented solution. Add external references if needed. ### Screenshot/Demo A picture is worth a thousand words. Please highlight the changes if applicable. ### How to test - Steps to be followed to verify the solution or code changes - Mention if there is any settings configuration added/changed/deleted ## Checklist - [ ] I have run this code and it appears to resolve the stated issue - [ ] `README.md` updated and added information about my change ================================================ FILE: .github/scripts/analyze-vsix.py ================================================ """Analyze a VSIX file and output size data as JSON. Reads the first .vsix file in the current directory, categorizes its contents, and writes bundle-baseline.json with the results. Also sets the vsix_size GitHub Actions output variable. """ import json import os import sys import zipfile CATEGORIES = [ (lambda n: "altimate-core" in n and n.endswith(".node"), "Native: altimate-core"), (lambda n: "zeromq/" in n and n.endswith(".node"), "Native: zeromq"), (lambda n: "dist/node_modules/" in n, "Native: other node_modules"), (lambda n: n.endswith("extension.js") and "dist/" in n, "Extension backend (JS)"), (lambda n: "webview_panels/dist/" in n and n.endswith(".js"), "Webview JS bundles"), (lambda n: "webview_panels/dist/" in n and n.endswith(".css"), "Webview CSS"), ( lambda n: "webview_panels/dist/" in n and (n.endswith(".gif") or n.endswith(".png")), "Webview images", ), (lambda n: "webview_panels/dist/" in n, "Webview other"), ( lambda n: "altimate_python_packages/" in n or (n.endswith(".py") and "dist/" in n), "Python packages", ), (lambda n: "media/" in n, "Media assets"), ] def categorize(filename): for predicate, category in CATEGORIES: if predicate(filename): return category return "Other" def main(): vsix_files = [f for f in os.listdir(".") if f.endswith(".vsix")] if not vsix_files: print("No .vsix file found", file=sys.stderr) sys.exit(1) vsix = vsix_files[0] z = zipfile.ZipFile(vsix) vsix_mb = os.path.getsize(vsix) / 1024 / 1024 categories = {} for info in z.infolist(): cat = categorize(info.filename) if cat not in categories: categories[cat] = {"raw": 0, "compressed": 0, "count": 0} categories[cat]["raw"] += info.file_size categories[cat]["compressed"] += info.compress_size categories[cat]["count"] += 1 baseline = {"vsix_mb": round(vsix_mb, 1), "categories": categories} with open("bundle-baseline.json", "w") as f: json.dump(baseline, f) gh_output = os.environ.get("GITHUB_OUTPUT") if gh_output: with open(gh_output, "a") as f: f.write(f"vsix_size={vsix_mb:.1f}\n") print(f"VSIX size: {vsix_mb:.1f} MB") if __name__ == "__main__": main() ================================================ FILE: .github/scripts/generate-bundle-report.py ================================================ """Generate a bundle size report comparing current build against master baseline. Reads bundle-baseline.json (current build) and optionally master-baseline.json (cached master baseline). Outputs: - bundle-report.md: collapsible markdown report for PR comments - GITHUB_OUTPUT: status_desc for the commit status API Expects VSCE_TARGET env var to label the platform. """ import json import os import sys def format_size(bytes_val): if bytes_val >= 100 * 1024: return f"{bytes_val / 1024 / 1024:.1f} MB" return f"{bytes_val / 1024:.0f} KB" def main(): target = os.environ.get("VSCE_TARGET", "unknown") current = json.load(open("bundle-baseline.json")) vsix_mb = current["vsix_mb"] categories = current["categories"] # Try to load master baseline for comparison baseline_mb = None baseline_cats = None if os.path.exists("master-baseline.json"): try: master = json.load(open("master-baseline.json")) baseline_mb = master["vsix_mb"] baseline_cats = master.get("categories", {}) except Exception: pass # Build delta string for commit status if baseline_mb is not None: delta = vsix_mb - baseline_mb sign = "+" if delta >= 0 else "" delta_str = f" ({sign}{delta:.1f} MB vs master)" else: delta_str = "" status_desc = f"VSIX: {vsix_mb:.1f} MB{delta_str}" # Build summary line for collapsible header if baseline_mb is not None: delta = vsix_mb - baseline_mb sign = "+" if delta >= 0 else "" icon = "\U0001f534" if delta > 1 else "\U0001f7e2" if delta < -1 else "\u26aa" summary = f"{target}: {vsix_mb:.1f} MB \u00b7 {icon} {sign}{delta:.1f} MB vs master" else: summary = f"{target}: {vsix_mb:.1f} MB" total_raw = sum(v["raw"] for v in categories.values()) total_compressed = sum(v["compressed"] for v in categories.values()) total_files = sum(v["count"] for v in categories.values()) # Build collapsible markdown report lines = [ "
", f"{summary}", "", "| Category | Size | Compressed | Files |", "|---|---:|---:|---:|", ] sorted_cats = sorted(categories.items(), key=lambda x: x[1]["raw"], reverse=True) for cat, v in sorted_cats: raw = v["raw"] if raw < 1024: continue raw_s = format_size(raw) comp_s = format_size(v["compressed"]) delta_s = "" if baseline_cats and cat in baseline_cats: d = (raw - baseline_cats[cat]["raw"]) / 1024 / 1024 if abs(d) >= 0.1: s = "+" if d >= 0 else "" delta_s = f" ({s}{d:.1f})" lines.append(f"| {cat} | {raw_s}{delta_s} | {comp_s} | {v['count']} |") lines.append( f"| **Total** | **{total_raw / 1024 / 1024:.1f} MB** " f"| **{total_compressed / 1024 / 1024:.1f} MB** | **{total_files}** |" ) lines.extend(["", "
"]) report = "\n".join(lines) with open("bundle-report.md", "w") as f: f.write(report) gh_output = os.environ.get("GITHUB_OUTPUT") if gh_output: with open(gh_output, "a") as f: f.write(f"status_desc={status_desc}\n") print(report) if __name__ == "__main__": main() ================================================ FILE: .github/workflows/ci.yml ================================================ on: push: branches: - master pull_request: branches: - master release: types: - created permissions: contents: read jobs: build: strategy: fail-fast: false matrix: include: - os: macos-latest vsce-target: darwin-arm64 - os: ubuntu-latest vsce-target: linux-x64 - os: windows-latest vsce-target: win32-x64 runs-on: ${{ matrix.os }} permissions: contents: read pull-requests: write statuses: write steps: - name: Checkout uses: actions/checkout@v4 - name: Run common build steps uses: ./.github/actions/common-build with: vsce-target: ${{ matrix.vsce-target }} - name: Build and package VSIX run: npx @vscode/vsce package --target ${{ matrix.vsce-target }} env: VSCE_TARGET: ${{ matrix.vsce-target }} NODE_OPTIONS: --max-old-space-size=8192 - name: Analyze bundle size id: analyze run: python3 .github/scripts/analyze-vsix.py - name: Restore master baseline if: github.event_name == 'pull_request' uses: actions/cache/restore@v4 with: path: master-baseline.json key: bundle-size-baseline-${{ matrix.vsce-target }}-will-not-match restore-keys: bundle-size-baseline-${{ matrix.vsce-target }}- - name: Save master baseline if: github.event_name == 'push' && github.ref == 'refs/heads/master' uses: actions/cache/save@v4 with: path: bundle-baseline.json key: bundle-size-baseline-${{ matrix.vsce-target }}-${{ github.sha }} - name: Generate report id: report run: python3 .github/scripts/generate-bundle-report.py env: VSCE_TARGET: ${{ matrix.vsce-target }} - name: Set commit status if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository uses: actions/github-script@v7 with: script: | const desc = '${{ steps.report.outputs.status_desc }}'; const target = '${{ matrix.vsce-target }}'; await github.rest.repos.createCommitStatus({ owner: context.repo.owner, repo: context.repo.repo, sha: context.payload.pull_request.head.sha, state: 'success', description: desc, context: `Bundle Size (${target})`, target_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, }); - name: Upload report if: github.event_name == 'pull_request' uses: actions/upload-artifact@v4 with: name: bundle-report-${{ matrix.vsce-target }} path: bundle-report.md retention-days: 1 - name: Job summary shell: bash run: cat bundle-report.md >> "$GITHUB_STEP_SUMMARY" bundle-size-report: needs: build runs-on: ubuntu-latest if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository permissions: pull-requests: write steps: - name: Download all reports uses: actions/download-artifact@v4 with: pattern: bundle-report-* - name: Combine reports run: | echo "## Bundle Size Report" > combined-report.md echo "" >> combined-report.md for dir in bundle-report-*/; do cat "$dir/bundle-report.md" >> combined-report.md echo "" >> combined-report.md done - name: Post PR comment uses: actions/github-script@v7 with: script: | const fs = require('fs'); const report = fs.readFileSync('combined-report.md', 'utf8'); const marker = ''; const body = marker + '\n' + report; const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, }); const existing = comments.find(c => c.body.includes(marker)); if (existing) { await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body, }); } else { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body, }); } release-vsstudio-marketplace: needs: build runs-on: ubuntu-latest if: success() && startsWith( github.ref, 'refs/tags/') strategy: fail-fast: false matrix: target: - darwin-arm64 - darwin-x64 - linux-arm64 - linux-x64 - win32-x64 steps: - name: Checkout uses: actions/checkout@v4 - name: Run common build steps uses: ./.github/actions/common-build with: vsce-target: ${{ matrix.target }} - name: Publish Visual Studio Marketplace run: | PRE_RELEASE="" if [ "${{ github.event_name }}" == "release" ] && [ "${{ github.event.release.prerelease }}" == "true" ]; then PRE_RELEASE="--pre-release" fi npx vsce publish --skip-duplicate --target ${{ matrix.target }} $PRE_RELEASE env: VSCE_PAT: ${{ secrets.VSCE_PAT }} VSCE_TARGET: ${{ matrix.target }} # Send notification after all platform builds (only on the last matrix entry) release-vsstudio-notify: runs-on: ubuntu-latest needs: release-vsstudio-marketplace if: always() && startsWith( github.ref, 'refs/tags/') steps: - name: Send Slack notification env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} uses: 8398a7/action-slack@v3 with: status: ${{ needs.release-vsstudio-marketplace.result }} text: "Tag: ${{ github.ref_name }} release to Visual Studio Marketplace status: ${{ needs.release-vsstudio-marketplace.result == 'success' && 'succeeded' || 'failed' }} :${{ needs.release-vsstudio-marketplace.result == 'success' && 'tada' || 'disappointed' }}:" release-openvsx-marketplace: needs: build runs-on: ubuntu-latest if: success() && startsWith( github.ref, 'refs/tags/') strategy: fail-fast: false matrix: target: - darwin-arm64 - darwin-x64 - linux-arm64 - linux-x64 - win32-x64 steps: - name: Checkout uses: actions/checkout@v4 - name: Run common build steps uses: ./.github/actions/common-build with: vsce-target: ${{ matrix.target }} - name: Publish OpenVSX Marketplace run: | PRE_RELEASE="" if [ "${{ github.event_name }}" == "release" ] && [ "${{ github.event.release.prerelease }}" == "true" ]; then PRE_RELEASE="--pre-release" fi npx ovsx publish --skip-duplicate --target ${{ matrix.target }} $PRE_RELEASE env: OVSX_PAT: ${{ secrets.OVSX_PAT }} VSCE_TARGET: ${{ matrix.target }} release-openvsx-notify: runs-on: ubuntu-latest needs: release-openvsx-marketplace if: always() && startsWith( github.ref, 'refs/tags/') steps: - name: Send Slack notification env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} uses: 8398a7/action-slack@v3 with: status: ${{ needs.release-openvsx-marketplace.result }} text: "Tag: ${{ github.ref_name }} release to openvsx marketplace status: ${{ needs.release-openvsx-marketplace.result == 'success' && 'succeeded' || 'failed' }} :${{ needs.release-openvsx-marketplace.result == 'success' && 'tada' || 'disappointed' }}:" ================================================ FILE: .github/workflows/deploy-docs-to-s3.yaml ================================================ name: Deploy Static Website to AWS # Defines when the action will run. # This example triggers the workflow on push events to the main branch. on: push: branches: [master] # Limit permissions for the GITHUB_TOKEN to adhere to the principle of least privilege. permissions: contents: read # The jobs that will be executed by the workflow. jobs: deploy: # The type of runner that the job will run on. runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job. steps: # Checks-out your repository under $GITHUB_WORKSPACE, so the job can access it. - uses: actions/checkout@v2 # Install MkDocs and any necessary plugins - name: Install MkDocs with plugins run: | pip install mkdocs pip install mkdocs-material pip install mkdocs-git-revision-date-localized-plugin # Build your site using MkDocs - name: Build site with MkDocs run: | cd documentation mkdocs build --clean # Deploy to AWS - name: Deploy to AWS uses: onramper/action-deploy-aws-static-site@v3.2.0 with: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} domain: docs.myaltimate.com publish_dir: ./documentation/site ================================================ FILE: .github/workflows/tests.yml ================================================ name: Tests on: push: branches: ["*"] pull_request: branches: ["*"] jobs: test: strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: "18" cache: "npm" cache-dependency-path: | package-lock.json webview_panels/package-lock.json - name: Install dependencies run: | npm ci npm run install:panels - name: Compile run: npm run compile - name: Install xvfb if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y xvfb - name: Run unit tests (Linux) if: runner.os == 'Linux' run: | xvfb-run --auto-servernum npm run compile && xvfb-run --auto-servernum npm run test:coverage - name: Run unit tests (macOS/Windows) if: runner.os != 'Linux' run: | npm run compile && npm run test:coverage - name: Setup Python uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install sqlfmt run: pip install "shandy-sqlfmt[jinjafmt]" - name: Run integration tests (Linux) if: runner.os == 'Linux' run: xvfb-run --auto-servernum npm run test:integration - name: Run integration tests (macOS/Windows) if: runner.os != 'Linux' run: npm run test:integration - name: Upload test results if: always() uses: actions/upload-artifact@v4 with: name: test-results-${{ matrix.os }} path: test-results/ - name: Upload coverage results if: always() uses: actions/upload-artifact@v4 with: name: coverage-${{ matrix.os }} path: | coverage/ .nyc_output/ *.lcov - name: Upload coverage to Codecov if: always() uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} directory: ./coverage/ flags: unittests name: codecov-${{ matrix.os }} fail_ci_if_error: false ================================================ FILE: .gitignore ================================================ out node_modules dist .vscode-test/ *.vsix .DS_Store .idea .history __pycache__/ *.pyc *.pyo *.pyd .claude # Build and dependencies src/lib/notebooks/ .nyc_output/ coverage/ test-results/ .venv/ # Test directories test_dbt_project/ test-workspace/ # IDE and editor files *.swp *.swo .vscode/ *.code-workspace # OS generated files .DS_Store .DS_Store? ._* .Spotlight-V100 .Trashes ehthumbs.db Thumbs.db .aider* certs mcp.json # Docker setup docker-setup/*.vsix docker-setup/.env # Playwright MCP .playwright-mcp/ # GitHub issues dashboard cache monitoring/github_issues/.cache/ ================================================ FILE: .gitpod.yml ================================================ # This configuration file was automatically generated by Gitpod. # Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml) # and commit this file to your remote git repository to share the goodness with others. # Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart ports: # webview_panels - port: 5173 visibility: public onOpen: open-browser # webview_panels storybook - port: 6006 visibility: public onOpen: open-browser tasks: - init: npm install - name: webview_panels install before: | cd webview_panels npm install gp sync-done webview_panels_install - name: webview_panels storybook command: | cd webview_panels gp sync-await webview_panels_install npm run storybook - name: webview_panels dev server command: | cd webview_panels gp sync-await webview_panels_install npm run dev ================================================ FILE: .husky/pre-commit ================================================ npx lint-staged ================================================ FILE: .mcp.json ================================================ { "mcpServers": { "playwright": { "command": "npx", "args": ["-y", "@playwright/mcp"], "env": { "BROWSER_NAME": "chromium" } } } } ================================================ FILE: .nycrc.json ================================================ { "extends": "@istanbuljs/nyc-config-typescript", "all": true, "check-coverage": true, "reporter": ["text", "lcov"], "exclude": [ "coverage/**", "packages/*/test/**", "test/**", "test{,-*}.{js,cjs,mjs,ts}", "**/*{.,-}test.{js,cjs,mjs,ts}", "**/__tests__/**", "**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress}.config.{js,cjs,mjs,ts}", "**/node_modules/**", "dist/**", "out/**", "src/test/**", "src/**/*.test.ts", "src/test/suite/**" ], "include": ["src/**/*.ts"], "branches": 5, "lines": 7, "functions": 7, "statements": 7 } ================================================ FILE: .prettierignore ================================================ node_modules ================================================ FILE: .prettierrc ================================================ { "plugins": ["prettier-plugin-organize-imports"] } ================================================ FILE: .vscodeignore ================================================ .vscode/** .vscode-test/** out/** src/** **/node_modules !dist/node_modules/** .gitignore vsc-extension-quickstart.md **/tsconfig.json **/.eslintrc.json **/*.map **/*.ts .github .claude .mcp.json .playwright-mcp docker-setup **/*.test.* CLAUDE.md webview_panels !webview_panels/dist # Non-runtime directories (docs site, test fixtures, monitoring, git hooks) documentation test-fixtures monitoring .husky # Dev/CI config files not needed in the installed extension jest.config.js .gitpod.yml .nycrc.json codecov.yml .prettierignore sweep.yaml prepareBuild.js CONTRIBUTING.md ================================================ FILE: CLAUDE.md ================================================ # Claude Code - dbt Power User VSCode Extension Architecture Guide ## Project Overview **vscode-dbt-power-user** is a comprehensive VSCode extension that makes VSCode seamlessly work with dbt (data build tool). It's an open-source project published by Altimate AI that extends VSCode with advanced dbt features including auto-completion, query preview, lineage visualization, documentation generation, and AI-powered features. ### Key Statistics - **Version**: 0.57.3 - **Project Type**: VSCode Extension (TypeScript/React) - **License**: MIT - **Architecture**: Multi-layered with webview panels, Python integrations, and MCP server ## High-Level Architecture ### 1. Core Extension Architecture The extension follows a **dependency injection pattern** using Inversify container: - **Entry Point**: `src/extension.ts` → `DBTPowerUserExtension` - **DI Container**: `src/inversify.config.ts` manages all service dependencies - **Main Extension Class**: `DBTPowerUserExtension` orchestrates all components ### 2. Multi-Process Architecture The extension operates across multiple processes: 1. **Main Extension Process** (Node.js/TypeScript) - VSCode API integration - File system operations - dbt CLI interactions 2. **Webview Panels** (React/TypeScript) - Modern React-based UI components - Located in `webview_panels/` directory - Built with Vite, uses Antd for UI components 3. **Python Bridge Integration** - dbt core/cloud integration via Python scripts - Key files: `dbt_core_integration.py`, `dbt_cloud_integration.py` - Jupyter kernel for notebook functionality 4. **MCP Server** (Model Context Protocol) - AI integration and tool calling functionality - Located in `src/mcp/` ### 3. Key Module Organization ``` src/ ├── manifest/ # dbt project parsing and management ├── dbt_client/ # dbt integration (core, cloud, fusion) ├── webview_provider/ # Webview panel management ├── autocompletion_provider/ # Language server features ├── services/ # Business logic services ├── commands/ # VSCode command implementations ├── mcp/ # Model Context Protocol server └── telemetry/ # Analytics and tracking ``` ## Core Functionality Areas ### 1. dbt Integration Support **Multiple Integration Types**: - **dbt Core**: Direct Python integration via Python bridge - **dbt Cloud**: API-based integration with dbt Cloud services - **dbt Fusion**: Command-line integration with dbt-fusion CLI - **Core Command**: CLI wrapper integration for dbt core **Key Integration Files**: - `src/dbt_client/dbtCoreIntegration.ts` - dbt Core Python integration - `src/dbt_client/dbtCloudIntegration.ts` - dbt Cloud API integration - `src/dbt_client/dbtFusionCommandIntegration.ts` - dbt Fusion CLI integration - `dbt_core_integration.py` - Python bridge for Core integration ### 2. Language Server Features **Provider Architecture**: Each feature implemented as a separate provider: - `autocompletion_provider/` - IntelliSense for dbt models, macros, sources - `definition_provider/` - Go-to-definition functionality - `hover_provider/` - Hover information - `code_lens_provider/` - Inline actions - `validation_provider/` - SQL validation ### 3. Webview Panel System **Modern React Architecture** (`webview_panels/`): - **Build System**: Vite + TypeScript + React 18 - **State Management**: Redux Toolkit - **UI Framework**: Antd + custom components - **Data Visualization**: Perspective.js, Plotly.js **Key Panels**: - `modules/dataPilot/` - AI chat interface - `modules/queryPanel/` - Query results and analysis - `modules/lineage/` - Data lineage visualization - `modules/documentationEditor/` - Documentation management - `modules/insights/` - Project insights and actions ### 4. AI and Advanced Features **DataPilot AI Integration**: - Chat-based interface for dbt assistance - Query explanation and optimization - Documentation generation - Test suggestions **MCP Server Integration**: - Tool calling for dbt operations - Integration with Claude and other AI models - Located in `src/mcp/server.ts` ## Build System and Tooling ### 1. Multi-Stage Build Process **Main Extension Build** (Webpack): ```bash npm run webpack # Development build npm run vscode:prepublish # Production build ``` **Webview Panels Build** (Vite): ```bash npm run panel:webviews # Build React components ``` ### 2. Development Workflow **Key Scripts**: - `npm run compile` - Compile the code - `npm run watch` - Development with hot reload - `npm run test` - Jest-based testing - `npm run lint` - ESLint + Prettier - `npm run build-vsix` - Package extension **Development Environment**: - Uses VSCode's built-in debugger ("Launch Extension") - Hot reload for webview panels - Python environment auto-detection ### 3. Testing Strategy **Test Configuration** (`jest.config.js`): - **Unit Tests**: Jest + ts-jest - **Mock System**: Custom VSCode API mocks - **Coverage**: Istanbul-based coverage reporting - **Test Location**: `src/test/` with mock infrastructure ## Key Dependencies and Integrations ### 1. VSCode Extension Dependencies **Required Extensions**: - `samuelcolvin.jinjahtml` - Jinja templating support - `ms-python.python` - Python environment integration - `altimateai.vscode-altimate-mcp-server` - MCP server ### 2. Major Technical Dependencies **Backend (Node.js)**: - `inversify` - Dependency injection - `python-bridge` - Python process communication - `zeromq` - Jupyter kernel communication - `@modelcontextprotocol/sdk` - MCP protocol **Frontend (React)**: - `react` 18 + `react-dom` - `@reduxjs/toolkit` - State management - `antd` - UI component library - `@finos/perspective` - Data grid and visualization ### 3. Python Integration **Python Scripts**: - `dbt_core_integration.py` - Core dbt operations - `dbt_cloud_integration.py` - Cloud API operations - `dbt_healthcheck.py` - Project health analysis - `altimate_notebook_kernel.py` - Jupyter integration ## Configuration and Extensibility ### 1. Extension Configuration **Comprehensive Settings** (190+ configuration options): - dbt integration mode selection - Query limits and templates - AI features and endpoints - Lineage visualization options - Defer-to-production configuration ### 2. Language Support **File Type Associations**: - `jinja-sql` - Primary dbt model files - `jinja-yaml` - dbt configuration files - `jinja-md` - Documentation files - Custom notebook format (`.notebook`) ### 3. Command System **80+ Commands Available**: - Model execution (`dbtPowerUser.runCurrentModel`) - Documentation generation (`dbtPowerUser.generateSchemaYML`) - Query analysis (`dbtPowerUser.sqlLineage`) - AI assistance (`dbtPowerUser.openDatapilotWithQuery`) ## Deployment and Distribution ### 1. Multi-Platform Distribution **CI/CD Pipeline** (`.github/workflows/ci.yml`): - **Build Matrix**: macOS, Ubuntu, Windows - **Visual Studio Marketplace**: Primary distribution - **OpenVSX Registry**: Open-source alternative - **Platform-specific builds**: Architecture-aware packaging ### 2. Release Process **Automated Release**: - Git tag triggers release pipeline - Pre-release and stable channel support - Slack notifications for release status - VSIX package generation ## Development Guidelines ### 1. Code Organization Principles - **Dependency Injection**: All services use Inversify DI - **Provider Pattern**: Language features as modular providers - **Event-Driven**: Manifest changes trigger updates across components - **Separation of Concerns**: Clear boundaries between UI, business logic, and dbt integration ### 2. Adding New Features **For Language Features**: 1. Create provider in appropriate `*_provider/` directory 2. Register in `inversify.config.ts` 3. Wire up in `DBTPowerUserExtension` **For UI Features**: 1. Add React component in `webview_panels/src/modules/` 2. Update routing in `AppRoutes.tsx` 3. Add state management slice if needed **For dbt Integration**: 1. Extend appropriate dbt client (`dbtCoreIntegration.ts` etc.) 2. Add Python bridge function if needed 3. Update MCP server tools if AI-accessible ### 3. Testing Approach - **Unit Tests**: Mock VSCode APIs and dependencies - **Integration Tests**: Test with real dbt projects - **Manual Testing**: Use "Launch Extension" debug configuration - **Webview Testing**: Storybook for component development ## Common Development Patterns ### 1. Manifest-Driven Architecture The extension heavily relies on dbt's `manifest.json` for understanding project structure. Most features key off manifest parsing events. ### 2. Multi-Integration Support Always consider how features work across dbt core, cloud, and other integration types. Use strategy pattern for integration-specific behavior. ### 3. Webview Communication Uses VSCode's webview messaging system with typed message contracts. State is synchronized between extension and webview contexts. ### 4. Python Bridge Pattern For dbt operations requiring Python, use the established bridge pattern with JSON serialization and error handling. This architecture enables the extension to provide comprehensive dbt development support while maintaining modularity and extensibility for future enhancements. --- # User Guide ## Core Features Overview The dbt Power User extension accelerates dbt and SQL development by 3x through three key phases: ### 🔧 DEVELOP - **SQL Visualizer**: Visual query builder and analyzer - **Query Explanation**: AI-powered SQL query explanation - **Auto-generation**: Generate dbt models from sources or raw SQL - **Auto-completion**: IntelliSense for dbt models, macros, sources, and doc blocks - **Click to Run**: Execute models directly from editor - **Query Translation**: Translate SQL between different dialects - **Compiled SQL Preview**: View compiled dbt code before execution ### 🧪 TEST - **Query Results Preview**: Execute and analyze query results with export capabilities - **Test Generation**: AI-powered test generation for dbt models - **Column Lineage**: Detailed data lineage with code visibility - **Defer to Production**: Run models without rebuilding dependencies - **SQL Validation**: Validate SQL without execution - **Model Lineage**: Visual representation of model dependencies ### 🤝 COLLABORATE - **Documentation Generation**: AI-powered documentation creation - **Code Collaboration**: Discussion threads on code and documentation - **Project Governance**: Automated checks for code quality and standards - **SaaS UI Integration**: Web-based interface for dbt docs and lineage - **Query History & Bookmarks**: Track and share query executions - **Export Workflows**: Share lineage and documentation externally ## DataMates AI Integration The extension includes **AI Teammates** through the DataMates Platform: - **Coaching**: Personalize AI teammates for specific requirements - **Query Assistance**: AI-powered query explanation and optimization - **Documentation**: Automated documentation generation - **Test Suggestions**: Smart test recommendations - **SQL Translation**: Cross-dialect SQL conversion ## Feature Availability **Free Extension Features**: - SQL Visualizer, Model-level lineage, Auto-generation from sources - Auto-completion, Click to Run, Compiled SQL preview - Query results preview, Defer to production, SQL validation **With Altimate AI Key** (free signup at [app.myaltimate.com](https://app.myaltimate.com)): - Column-level lineage, Query explanation AI, Query translation AI - Auto-generation from SQL, Test generation AI, Documentation generation AI - Code/documentation collaboration, Lineage export, SaaS UI - Project governance, Query history & bookmarks --- # Installation and Setup ## Installation Methods ### Native Installation Install directly from [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=innoverio.vscode-dbt-power-user) or via VS Code: 1. Open VS Code Extensions panel (`Ctrl+Shift+X`) 2. Search for "dbt Power User" 3. Click Install 4. Reload VS Code if prompted ### Dev Container Installation Add to your `.devcontainer/devcontainer.json`: ```json { "customizations": { "vscode": { "files.associations": { "*.yaml": "jinja-yaml", "*.yml": "jinja-yaml", "*.sql": "jinja-sql", "*.md": "jinja-md" }, "extensions": ["innoverio.vscode-dbt-power-user"] } } } ``` ### Cursor IDE Support The extension is also available for [Cursor IDE](https://www.cursor.com/how-to-install-extension). Install the same way as VS Code. ## Required Configuration ### 1. dbt Integration Setup Configure how the extension connects to dbt: - **dbt Core**: For local dbt installations with Python bridge (default) - **dbt Cloud**: For dbt Cloud API integration - **dbt Fusion**: For dbt-fusion CLI integration - **dbt Core Command**: For CLI-based dbt core integration Set via `dbt.dbtIntegration` setting. #### dbt Fusion Integration dbt Fusion is a command-line interface that provides enhanced dbt functionality. When using fusion integration: - Requires dbt-fusion CLI to be installed in your environment - Extension automatically detects fusion installation via `dbt --version` output - Provides full feature support including query execution, compilation, and catalog operations - Uses JSON log format for structured command output parsing ### 2. Python Environment Ensure Python and dbt are properly installed and accessible. The extension will auto-detect your Python environment through the VS Code Python extension. ### 3. Optional: Altimate AI Key For advanced AI features, get a free API key: 1. Sign up at [app.myaltimate.com/register](https://app.myaltimate.com/register) 2. Add API key to `dbt.altimateAiKey` setting 3. Set instance name in `dbt.altimateInstanceName` setting ## Project Setup 1. Open your dbt project folder in VS Code 2. Run the setup wizard: Select "dbt" in bottom status bar → "Setup Extension" 3. The extension will auto-install dbt dependencies if enabled 4. Verify setup via Command Palette → "dbt Power User: Diagnostics" --- # Troubleshooting for Developers ## Quick Diagnostics ### 1. Setup Wizard Use the built-in setup wizard for automated issue detection: - Click "dbt" or "dbt is not installed" in bottom status bar - Select "Setup Extension" - Follow guided setup process ### 2. Diagnostics Command Run comprehensive system diagnostics: - Open Command Palette (`Cmd+Shift+P` / `Ctrl+Shift+P`) - Type "diagnostics" → Select "dbt Power User: Diagnostics" - Review output for environment issues, Python/dbt installation status, and connection problems ### 3. Problems Panel Check VS Code Problems panel for dbt project issues: - View → Problems (or `Ctrl+Shift+M`) - Look for dbt-related validation errors ## Debug Logging Enable detailed logging for troubleshooting: 1. Command Palette → "Set Log Level" → "Debug" 2. View logs: Output panel → "Log" dropdown → "dbt" 3. Reproduce the issue to capture debug information ## Developer Tools For advanced debugging: - Help → Toggle Developer Tools - Check console for JavaScript errors and detailed logs ## Common Issues **Extension not recognizing dbt project**: - Verify `dbt_project.yml` exists in workspace root - Check Python environment has dbt installed - Run diagnostics command for detailed analysis **Python/dbt not found**: - Configure Python interpreter via VS Code Python extension - Verify dbt is installed in selected Python environment - Set `dbt.dbtPythonPathOverride` if using custom Python path **Connection issues**: - Verify database connection in dbt profiles - Check firewall/network settings - Review connection details in diagnostics output ## Getting Help - Join [#tools-dbt-power-user](https://getdbt.slack.com/archives/C05KPDGRMDW) in dbt Community Slack - Contact support at [altimate.ai/support](https://www.altimate.ai/support) - Use in-extension feedback widgets for feature-specific issues --- # Core Development Features ## Auto-completion and Navigation ### Model Auto-completion - **Smart IntelliSense**: Auto-complete model names with `ref()` function - **Go-to-Definition**: Navigate directly to model files - **Hover Information**: View model details on hover ### Macro Support - **Macro Auto-completion**: IntelliSense for custom and built-in macros - **Parameter Hints**: Auto-complete macro parameters - **Definition Navigation**: Jump to macro definitions ### Source Integration - **Source Auto-completion**: IntelliSense for configured sources - **Column Awareness**: Auto-complete source column names - **Schema Navigation**: Navigate to source definitions ### Documentation Blocks - **Doc Block Auto-completion**: IntelliSense for documentation references - **Definition Linking**: Navigate to doc block definitions ## Query Development ### SQL Compilation and Preview - **Compiled Code View**: See final SQL before execution - **Template Resolution**: Preview Jinja templating results - **Syntax Highlighting**: Enhanced SQL syntax highlighting for dbt files ### Query Execution - **Preview Results**: Execute queries with `Cmd+Enter` / `Ctrl+Enter` - **Result Analysis**: Export results as CSV, copy as JSON - **Query History**: Track executed queries - **Configurable Limits**: Set row limits for query previews (default: 500 rows) ### SQL Formatting - **Auto-formatting**: Integration with sqlfmt - **Custom Parameters**: Configure formatting rules - **Batch Processing**: Format multiple files ## AI-Powered Development ### Query Explanation - **Natural Language**: Get plain English explanations of complex SQL - **Step-by-step Analysis**: Breakdown of query logic - **Performance Insights**: Query optimization suggestions ### Code Generation - **Model from Source**: Generate base models from source tables - **Model from SQL**: Convert raw SQL to dbt models - **Test Generation**: AI-powered test suggestions - **Documentation Generation**: Auto-generate model documentation ### Query Translation - **Cross-dialect Support**: Translate SQL between database dialects - **Syntax Adaptation**: Handle dialect-specific functions and syntax ## Documentation This is a MkDocs-based documentation site for the dbt Power User VSCode Extension by Altimate AI. The site uses the Material theme and is organized around user workflows: Develop, Test, and Collaborate. ### Development Commands - **Install dependencies**: `pip install --requirement documentation/requirements.txt` - **Start development server**: `cd documentation; mkdocs serve` (serves at http://127.0.0.1:8000) - **Build site**: `cd documentation; mkdocs build` - **Deploy to GitHub Pages**: `cd documentation; mkdocs gh-deploy` ### Architecture #### Content Organization - `documentation/docs/` contains all documentation content in Markdown format - Content is organized by feature areas: `setup/`, `develop/`, `test/`, `document/`, `govern/`, `discover/`, `teammates/`, `datamates/`, `arch/` - Images and assets are stored within feature-specific directories - `documentation/mkdocs.yml` contains all site configuration #### Key Configuration Files - `documentation/mkdocs.yml`: Main site configuration including navigation, theme settings, and plugins - `documentation/requirements.txt`: Python dependencies for MkDocs and plugins - `documentation/docs/overrides/`: Custom theme overrides (currently empty) - `documentation/docs/javascripts/`: Custom JavaScript for enhanced functionality #### Theme Configuration The site uses Material theme with: - Custom Altimate AI branding and colors - Google Analytics integration (G-LXRSS3VK5N) - Git revision date tracking via plugin - Built-in feedback system - Dark/light mode support #### Navigation Structure Navigation follows a three-phase user journey: 1. **Setup**: Installation and configuration 2. **Develop**: Core development features 3. **Test**: Testing and validation tools 4. **Additional**: Documentation, collaboration, discovery, and AI features ### Working with Content #### Adding New Pages 1. Create `.md` files in the appropriate `docs/` subdirectory 2. Update the `nav` section in `mkdocs.yml` to include the new page 3. Follow existing naming conventions for consistency #### Images and Assets - Store images in the same directory as the referencing markdown file - Use relative paths for image references - Common assets go in `docs/assets/` #### Internal Links Use relative markdown links to reference other pages. The site has extensive cross-referencing between related features. ### Testing Changes Always test locally with `mkdocs serve` before deploying. The development server provides live reload for content changes. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Guidelines Thank you for your interest in contributing to the dbt™ Power User extension! We greatly appreciate the dedication of our community members in helping us enhance this project. ## Bug Reports and Feature Requests One of the simplest ways to contribute is by sharing your thoughts with us. If you come across any bugs or have ideas for new features or improvements, please start a discussion in our dedicated channel: `#tools-dbt-power-user` on the [dbt™ community Slack](https://getdbt.slack.com/archives/C05KPDGRMDW). Additionally, take a look at the [GitHub issues](https://github.com/innoverio/vscode-dbt-power-user/issues) to see if the topic has already been raised. If not, feel free to create a new issue. Your detailed bug reports, along with steps to reproduce the issue and relevant environment information, are invaluable to us and other contributors. ## How to Contribute We warmly welcome contributions from our active community! If you're interested in contributing to the extension, follow these steps: 1. **Fork the Repository:** Start by forking the repository to your own GitHub account. 1. **Clone the Repository:** Clone your forked repository to your local machine: ```bash git clone https://github.com/your-username/vscode-dbt-power-user.git cd vscode-dbt-power-user ``` 1. **Set Up Development Environment:** Setting up the development environment for the dbt™ Power User extension is designed to be straightforward. Most of the setup is already prepared, but you might need to follow a few steps to ensure a smooth experience: 1. **Install Node.js and npm:** If you haven't already, make sure you have Node.js and npm (Node Package Manager) installed on your machine. You can download and install them from the [official Node.js website](https://nodejs.org/). When choosing a Node.js version, we recommend installing the latest LTS (Long-Term Support) version as recommended on the website to take advantage of the latest features, optimizations, and bugfixes. 1. **Install Required Node Packages:** In the root directory of your cloned repository (`vscode-dbt-power-user`), open a terminal and run the following command to install the required Node.js packages: ```bash npm install npm run install:panels ``` 1. **Start Debugging:** In the Visual Studio Code interface, navigate to the "Run and Debug" sidebar. Click on "Launch Extension" to start the debugging process. This will open a new window with the dbt™ Power User extension installed. During this debug session, the existing installation of dbt-power-user will be overridden, allowing you to test your changes without affecting the installed extension. - **Note:** If you're running the dbt Power User extension in debug mode using `Debug Extension` and you make changes to the extension's code, you can see those changes take effect immediately by reloading the VS Code instance where the extension is running. - To reload the VS Code instance in debug mode, you can either: - Use the command "Developer: Reload Window" from the command palette by pressing Ctrl + Shift + P and typing "Developer: Reload Window". - Simply press Ctrl/Cmd + R to reload the VS Code instance. 1. **Open a dbt™ Project as a Target:** To effectively test your changes, open a dbt™ project as a target. You can either use an existing project or create a new virtual environment with the necessary dbt™ package installed. Creating a virtual environment ensures a clean environment that won't interfere with any existing dbt™ installations. It's important to note that the dbt™ Power User extension itself does not require any Python/dbt™ installation. 1. **Explore and Test:** With the extension debug window open and your dbt™ project loaded, take the time to explore and test your changes. Verify that the extension behaves as expected and that your changes integrate seamlessly. Following these steps will help you establish a productive development environment for contributing to the dbt™ Power User extension. If you encounter any issues during setup, don't hesitate to reach out to the community for assistance. 1. **Make Your Changes:** Feel free to implement the changes or new features you have in mind. If you're new to contributing, our [contributing page](https://github.com/innoverio/vscode-dbt-power-user/contribute) is a great starting point. Choose an issue listed there and familiarize yourself with the extension. 1. **Thorough Testing:** Test your changes thoroughly to ensure they work as expected. 1. **Clear Commit Messages:** Write clear and descriptive commit messages. 1. **Push Changes:** Push the changes to your forked repository. 1. **Create a Pull Request:** Submit a pull request to the main repository, explaining the purpose and details of your changes. ## Code Style and Formatting Maintaining consistent code style and formatting is crucial for readability and collaboration. Ensure your code aligns with the existing conventions and formatting guidelines used in the project. ## Adding New Features or Fixing Bugs When introducing new features or addressing bugs, consider the current codebase and community needs. Engage in discussions with fellow community members if you're unsure about design decisions or implementation details. ## Local Development with @altimateai/dbt-integration Library When working on the extension, you may need to develop against a local version of the `@altimateai/dbt-integration` library instead of the published npm package. This allows you to test changes to both the extension and the integration library simultaneously. ### Setup for Local Development 1. **Clone the dbt-integration repository:** First, ensure you have the `altimate-dbt-integration` repository cloned as a sibling directory to this project: ```bash cd /path/to/your/projects git clone https://github.com/altimateai/altimate-dbt-integration.git cd vscode-dbt-power-user ``` Your directory structure should look like: ``` /path/to/your/projects/ ├── altimate-dbt-integration/ └── vscode-dbt-power-user/ ``` 2. **Switch to local development mode:** Modify the following configuration files to use the local TypeScript source instead of the npm package: **jest.config.js**: Uncomment the local development lines: ```javascript // Development: use local TypeScript source (same as webpack and tsconfig) "^@altimateai/dbt-integration$": "/../altimate-dbt-integration/src/index.ts", // Production: use npm package (commented out for development) // "^@altimateai/dbt-integration$": "@altimateai/dbt-integration", ``` **tsconfig.json**: Update the configuration: ```json { // "rootDir": "src", "rootDirs": ["src", "../altimate-dbt-integration/src"], "paths": { "@altimateai/dbt-integration": [ "../altimate-dbt-integration/src/index.ts" ], "@extension": ["./src/modules.ts"], "@lib": ["./src/lib/index"] } } ``` **webpack.config.js**: Update the alias and copy plugin configurations: ```javascript // In resolve.alias section: "@altimateai/dbt-integration": path.resolve( __dirname, "../altimate-dbt-integration/src/index.ts", ), // In CopyWebpackPlugin, comment out production copies and uncomment development copies: // Development: use local Python files { from: path.resolve( __dirname, "../altimate-dbt-integration/node_modules/python-bridge/node_python_bridge.py", ), to: "node_python_bridge.py", }, // ... (other local file copies) ``` ### Switching Back to Production Mode When you're done with local development, revert the configuration changes to use the published npm package: 1. **jest.config.js**: Comment out local development lines and uncomment production lines 2. **tsconfig.json**: Set `"rootDir": "src"` and remove the local path mapping 3. **webpack.config.js**: Remove local alias and use npm package copies in CopyWebpackPlugin ### Benefits of Local Development Mode - **Real-time changes**: Modify both the extension and integration library simultaneously - **Debugging**: Set breakpoints and debug across both codebases - **Testing**: Test integration library changes before publishing - **Development workflow**: Faster iteration when working on features that span both repositories ### Important Notes - Ensure both repositories are on compatible branches when doing local development - The local development setup expects the `altimate-dbt-integration` directory to be a sibling of `vscode-dbt-power-user` - Always test with the production npm package configuration before submitting pull requests - The Python files from the integration library are copied during the webpack build process ## Testing Comprehensive testing is essential for maintaining the extension's stability and reliability. While adding new features or fixing bugs, run tests locally helps ensure your changes don't introduce regressions. ## Security If you identify security vulnerabilities or potential issues, please report them responsibly. Reach out to the maintainers directly to discuss and report the issue privately. ## Credits and Acknowledgments Our heartfelt thanks go out to all the contributors and our vibrant community members who have supported this project. Your contributions are a cornerstone of our progress. We also want to acknowledge that the dbt™ logo is a trademark of dbt Labs, Inc. ## Feedback and Communication Open communication is a core value of our community. Join discussions on our GitHub repository, participate in issue discussions, and provide feedback on proposed changes. Thank you for being a vital part of our open-source community! Your contributions help us enhance the dbt™ Power User extension, providing an improved experience for all users. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 innover.io Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # vscode-dbt-power-user ![Last updated](https://img.shields.io/visual-studio-marketplace/last-updated/innoverio.vscode-dbt-power-user) ![Version](https://img.shields.io/visual-studio-marketplace/v/innoverio.vscode-dbt-power-user) ![Installs](https://img.shields.io/visual-studio-marketplace/i/innoverio.vscode-dbt-power-user) ![Build passing](https://github.com/innoverio/vscode-dbt-power-user/workflows/.github/workflows/ci.yml/badge.svg) This [open source](https://github.com/AltimateAI/vscode-dbt-power-user) extension makes VSCode seamlessly work with [dbt™](https://www.getdbt.com/). If you need help with setting up the extension, please check the [documentation](https://docs.myaltimate.com/setup/installation/). For any issues or bugs, please [contact us](https://www.altimate.ai/support) via chat or Slack. **Features:** | Feature | Details | | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | [Auto-complete dbt™ code](#autocomplete) | Auto-fill model names, macros, sources and docs. Click on model names, macros, sources to go to definitions. | | [Preview Query results and Analyze](#querypreview) | Generate dbt™ model / query results. Export as CSV or analyze results by creating graphs, filters, groups | | [Column lineage](#lineage) | Model lineage as well as column lineage | | [Generate dbt™ Models](#genmodel) | from source files or convert SQL to dbt™ Model (docs) | | [Generate documentation](#gendoc) | Generate model and column descriptions or write in the UI editor. Save formatted text in YAML files. | | [Defer to prod](#defertoprod) | Build your model in development without building (by defering) your upstream models | | [Click to run parent / child models and tests](#clicktorun) | Just click to do common dbt™ operations like running tests, parent / child models or previewing data. | | [Compiled query preview and explanation](#queryexplanation) | Get live preview of compiled query as your write code. Also, generate explanations for dbt™ code written previously (by somebody else) | | [Project health check](#healthcheck) | Identify issues in your dbt™ project like columns not present, models not materialized | | [SQL validator](#validateSQL) | Identify issues in SQL like typos in keywords, missing or extra parentheses, non-existent columns | | [Big Query cost estimator](#bqcost) | Estimate data that will be processed by dbt™ model in BigQuery | | [Other features](#otherfeatures) | dbt™ logs viewer (force tailing) | Note: This extension is fully compatible with dev containers, code spaces and remote extension. See [Visual Studio Code Remote - Containers](https://code.visualstudio.com/docs/remote/containers) and [Visual Studio Code Remote - WSL](https://code.visualstudio.com/docs/remote/wsl). The extension is supported for dbt™ versions above 1.0. ## Features ### Autocomplete model, macro, source names and click to go to definition Auto-fill model names, macros, sources and docs. Click on model names, macros, sources to go to definitions. [(docs)](https://docs.myaltimate.com/develop/autocomplete/) ![autocomplete](media/images/autocomplete.gif) ### Preview query results and analyze Generate dbt™ model / query results. Export as CSV or analyze results by creating graphs, filters, groups. [(docs)](https://docs.myaltimate.com/test/queryResults/) ![previewquery](media/images/previewquery.gif) ### Column lineage View model lineage as well as column lineage with components like models, seeds, sources, exposures and info like model types, tests, documentation, linkage types. [(docs)](https://docs.myaltimate.com/test/lineage/) ![lineage](media/images/lineage.gif) ### Generate dbt™ Models from source or SQL Generate dbt™ models from sources defined in YAML. You can also convert existing SQL to a dbt™ model where references get populated automatically. [(docs)](https://docs.myaltimate.com/develop/clicktorun/) ![genmodel](media/images/genmodel.gif) ### Generate documentation Generate model and column descriptions automatically or write descriptions manually in the UI editor. Your descriptions are automatically formatted and saved in YAML files. [(docs)](https://docs.myaltimate.com/document/generatedoc/) ![gendoc](media/images/gendoc.gif) ### Defer to prod Defer building your upstream models when you make changes in development by referencing production models. Here's [(more info)](https://docs.getdbt.com/blog/defer-to-prod) about the concept. This functionality can be used in dbt™ core with the extension. [(docs)](https://docs.myaltimate.com/test/defertoprod/) ### Click to run parent/child models and tests Just click to do common button operations like executing tests, building or running parent / child models. [(docs)](https://docs.myaltimate.com/develop/clicktorun/) ![autocomplete](media/images/runmodeltests.gif) ### Compiled query preview and explanation Get live preview of compiled query as your write code. Also, generate explanations for dbt™ code written previously (by somebody else). [(docs)](https://docs.myaltimate.com/develop/explanation/) ![explanation](media/images/explanation.gif) ### Project health check Identify issues in your dbt™ project like columns not present, models not materialized. [(docs)](https://docs.myaltimate.com/test/healthcheck/) ![healthcheck](media/images/healthcheck.gif) ### SQL validator Validate SQL to identify issues like mistyped keywords, extra parentheses, columns no present in database [(docs)](https://docs.myaltimate.com/test/sqlvalidation/) ![sql-validator](media/images/sqlValidation.gif) ### Big Query cost estimator Estimate data that will be processed by dbt™ model in BigQuery [(docs)](https://docs.myaltimate.com/test/bigquerycost/) ![bqcostestimator](media/images/bqcostestimator.gif) ### Other features **dbt™ logs view (force tailing)** ![dbt-log](media/images/dbt-log.gif) Please check [documentation](https://docs.myaltimate.com/arch/faq/) for additional info. For any issues or bugs, please [contact us](https://www.altimate.ai/support) via chat or Slack. ================================================ FILE: altimate_notebook_kernel.py ================================================ import json import re from datetime import datetime import jupyter_client import queue class CustomDecoder(json.JSONDecoder): def __init__(self, *args, **kwargs): super().__init__(object_hook=self.object_hook, *args, **kwargs) def object_hook(self, obj): for key, value in obj.items(): if value == 'null': obj[key] = None return obj # Notebook kernel which will responsible for creating kernel executors for each notebook # should shut down kernel after notebook is closed # also store the cell outputs/data and use it for further executions # TODO: implement as required - extract to separate file # a method which will be called once a notebook is open, create an instance of this class # save doc uri as unique identifier for the notebook # initialize a jupyter kernel executor on notebook open and shutdown on close # store results of each cell execution and update after each execution # handle cell deletions # destroy this instance when notebook is closed class JupyterKernelExecutor: def __init__(self): self.kernel_manager = jupyter_client.KernelManager() self.kernel_manager.start_kernel() self.kernel_client = self.kernel_manager.client() self.kernel_client.start_channels() print('session pid', self.kernel_client.session.pid) # print('ip', self.kernel_manager.connection_file.split('-')[1]) # Get the connection file connection_file = self.kernel_manager.connection_file # Load connection info connection_info = jupyter_client.find_connection_file(connection_file) with open(connection_info) as f: connection_data = json.load(f) # Extract WebSocket URL websocket_url = f"ws://{connection_data['ip']}:{connection_data['shell_port']}/api/kernels/{self.kernel_manager.kernel_id}/channels" print(connection_data, self.kernel_manager.kernel_name, self.kernel_manager.kernel_spec.name) def execute(self, code, user_expressions=None): self.kernel_client.wait_for_ready() # print("Executing code:", code, user_expressions) # Execute the code self.kernel_client.execute(code, silent=False, store_history=True, user_expressions=user_expressions) # Capture and return the output output = [] start_time = datetime.now() while True: try: msg = self.kernel_client.get_iopub_msg(timeout=1) def datetime_converter(o): if isinstance(o, datetime): return o.__str__() # print("msg", msg) if msg['msg_type'] == 'stream': # for stdout output.append({'mime': 'text/plain', 'value': msg['content']['text']}) elif msg['msg_type'] == 'comm_open': state = msg['content']['data']['state'] state['model_id'] = msg['content']['comm_id'] # Handle comm_open messages output.append({'mime': 'application/vnd.jupyter.widget-view+json', 'value': state}) elif msg['msg_type'] in ['execute_result', 'display_data']: # Flag to check if any key other than 'text/plain' exists other_keys_exist = False # Iterate over the keys in msg['content']['data'] for key, value in msg['content']['data'].items(): # Check if the key is not 'text/plain' if key != 'text/plain': # Append the dictionary to the output list output.append({'mime': key, 'value': value}) other_keys_exist = True # If no other keys exist, add the 'text/plain' value if not other_keys_exist and 'text/plain' in msg['content']['data']: output.append({'mime': 'text/plain', 'value': msg['content']['data']['text/plain']}) elif msg['msg_type'] == 'error': output.append({'mime': 'text/plain', 'value': '\n'.join(msg['content']['traceback'])}) elif msg['msg_type'] == 'status' and msg['content']['execution_state'] == 'idle': break except queue.Empty: if (datetime.now() - start_time).total_seconds() > 30: # Timeout after 30 seconds break return output def shutdown(self): # Shutdown the kernel client and kernel manager self.kernel_client.stop_channels() self.kernel_manager.shutdown_kernel() del self.kernel_client class AltimateNotebookKernel: def __init__(self, doc_uri): """ Initialize the AltimateNotebookKernel instance. Parameters: doc_uri (str): The unique identifier for the notebook. """ self.doc_uri = doc_uri self.kernel_executor = self.initialize_kernel_executor() self.cell_results = {} def get_session_id(self): return self.kernel_executor.kernel_client.session.pid def close_notebook(self): """ Method to be called when the notebook is closed. Shuts down the Jupyter kernel executor. """ if self.kernel_executor: self.shutdown_kernel_executor() self.kernel_executor = None print(f"Notebook {self.doc_uri} closed and kernel shut down.") def get_connection_file(self): return self.kernel_executor.kernel_manager.connection_file def initialize_kernel_executor(self): """ Initializes the Jupyter kernel executor. Returns: kernel_executor: The initialized kernel executor. """ kernel_executor = JupyterKernelExecutor() return kernel_executor def shutdown_kernel_executor(self): """ Shuts down the Jupyter kernel executor. """ # Placeholder for actual kernel shutdown logic print("Kernel executor shut down.") def get_sql_result_by_cell(self, cell_id): code = f"cell_{cell_id}" return self.kernel_executor.execute(code) def store_sql_result(self, cell_id, result): """ Stores the result of a cell execution. Parameters: cell_id (str): The unique identifier for the cell. result: The result of the cell execution. """ resultJson = json.loads(result) # Construct the code to store the result in the Jupyter kernel code = f""" cell_{cell_id} = {resultJson} """ self.execute_python(code) def execute_python(self, code): """ Executes a cell and stores the result. Parameters: cell_id (str): The unique identifier for the cell. code (str): The code to be executed in the cell. Returns: result: The result of the cell execution. """ response = self.kernel_executor.execute(code) return response def delete_cell(self, cell_id): """ Handles cell deletion. Parameters: cell_id (str): The unique identifier for the cell to be deleted. """ if cell_id in self.cell_results: del self.cell_results[cell_id] print(f"Cell {cell_id} deleted.") def destroy_instance(self): """ Destroys the instance when the notebook is closed. """ self.close_notebook() self.cell_results.clear() print(f"Instance for notebook {self.doc_uri} destroyed.") ================================================ FILE: codecov.yml ================================================ coverage: status: project: default: target: 10% # the required coverage value threshold: 1% # the leniency in hitting the target patch: default: target: 90% threshold: 1% ignore: - "test/**/*" # ignore test files - "out/**/*" # ignore compiled output - "**/*.d.ts" # ignore type declaration files ================================================ FILE: docker-setup/Dockerfile ================================================ FROM codercom/code-server:latest USER root # Install Node.js 20 RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ && apt-get install -y nodejs \ && corepack enable # Install Python 3, pip, git, and other dependencies RUN apt-get update && apt-get install -y \ python3 \ python3-pip \ python3-venv \ git \ xvfb \ && rm -rf /var/lib/apt/lists/* # Install dbt-duckdb globally (available for all users) RUN pip3 install --break-system-packages dbt-duckdb USER coder # Create altimate directory RUN mkdir -p ~/.altimate # Set up dbt profiles for both test projects # Projects are copied from the read-only extension-src mount to writable home dirs at container startup RUN mkdir -p ~/.dbt && printf '%s\n' \ 'jaffle_shop:' \ ' target: dev' \ ' outputs:' \ ' dev:' \ ' type: duckdb' \ ' path: /home/coder/jaffle-shop-duckdb/jaffle_shop.duckdb' \ ' threads: 4' \ '' \ 'dbt_core_sample_duckdb:' \ ' target: go_sales' \ ' outputs:' \ ' go_sales:' \ ' type: duckdb' \ ' path: /home/coder/dbt-core-sample-duckdb/go_sales.db' \ > ~/.dbt/profiles.yml # Install all required extensions for dbt Power User (extensionDependencies in package.json) RUN code-server --install-extension ms-python.python \ && code-server --install-extension samuelcolvin.jinjahtml \ && code-server --install-extension altimateai.vscode-altimate-mcp-server EXPOSE 3001 WORKDIR /home/coder COPY --chown=coder:coder start-code-server.sh /usr/local/bin/ ENTRYPOINT [] CMD ["/usr/local/bin/start-code-server.sh"] ================================================ FILE: docker-setup/README.md ================================================ # Docker Development Setup Develop and test the dbt Power User extension in code-server (VS Code in browser) with volume-mounted source for hot-reload. ## Quick Start ```bash npm run docker:deploy ``` This builds the extension, starts the container, and enters watch mode. Open http://localhost:3001/?folder=/home/coder/project in your browser. ## How It Works The extension source is **volume-mounted** into the container (read-only), so you don't need to rebuild a VSIX or the Docker image for every change: 1. `deploy.sh` runs `npm run build` to build the extension 2. Docker container starts with the repo mounted at `/home/coder/extension-src` 3. `start-code-server.sh` symlinks the mounted source into code-server's extensions directory 4. `npm run watch` runs on the host — any source change triggers a rebuild 5. Reload the browser to pick up changes ## Configuration ### Custom dbt Project By default, the container uses the built-in `jaffle_shop_duckdb` project. To use your own: 1. Copy `.env.example` to `.env` 2. Set `DBT_PROJECT_PATH=/absolute/path/to/your/project` 3. Re-run `npm run docker:deploy` ## What's Pre-Installed - **code-server**: VS Code in the browser (port 3001, no auth) - **Node.js 20**: For extension host - **Python 3 + dbt-duckdb**: For dbt integration - **jaffle_shop_duckdb**: Sample dbt project with pre-configured profile and deps - **Python extension**: Required dependency for dbt Power User - **Xvfb**: For headless testing ## Commands | Command | Description | | ----------------------- | -------------------------------------------- | | `npm run docker:deploy` | Build, start container, and enter watch mode | | `npm run docker:logs` | View container logs | | `npm run docker:stop` | Stop the container | ## Playwright MCP Testing Use Playwright MCP tools to programmatically verify the extension in Docker. ### Prerequisites Add to `.mcp.json` (project or global): ```json { "playwright": { "command": "npx", "args": [ "@playwright/mcp@latest", "--executable-path", "/usr/bin/chromium-browser", "--headless" ] } } ``` > **Note:** The `--executable-path` above is a Linux example. On macOS, point to your browser binary instead (e.g. `"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"`), or omit `--executable-path` entirely and run `npx playwright install` to use Playwright's bundled Chromium. The package is `@playwright/mcp` (NOT `@anthropic-ai/mcp-playwright`). ### Automated E2E Test Checklist Follow this exact sequence — each step is one tool call: **1. Verify bundle before deploying** (saves debugging stale code later): ```bash grep -c "myKeyFunction" dist/extension.js # confirm expected code is bundled ``` **2. Deploy with `--build`** (NEVER use `docker compose restart` — code-server caches extension state): ```bash docker compose -f docker-setup/docker-compose.yml up --build -d ``` **3. Wait for ready + seed dbt** (seed BEFORE browser connects — extension gets its own DuckDB connection): ```bash for i in $(seq 1 15); do curl -sf http://localhost:3001/healthz > /dev/null 2>&1 && break; sleep 2; done CID=$(docker ps -q --filter "name=docker-setup-code-server") docker exec $CID bash -c 'cd /home/coder/jaffle-shop-duckdb && dbt seed && dbt run' ``` **4. Check extension activation via logs** (don't waste time clicking through the UI): ```bash CID=$(docker ps -q --filter "name=docker-setup-code-server") latest=$(docker exec $CID bash -c 'ls -td /home/coder/.local/share/code-server/logs/*/ | head -1') docker exec $CID grep "innoverio\|removed" "${latest}remoteagent.log" docker exec $CID grep "innoverio" "${latest}exthost1/remoteexthost.log" ``` - ✅ `ExtensionService#_doActivateExtension innoverio.vscode-dbt-power-user` = activated - ❌ `Marked extension as removed` = `.obsolete` bug (fixed in PR #1859) **5. Create test SQL files** (do this from the host before connecting Playwright): ```bash docker exec $CID bash -c 'cat > /home/coder/jaffle-shop-duckdb/test_query.sql << "SQL" SELECT * FROM {{ ref("customers") }} LIMIT 10 SQL' ``` **6. Connect Playwright and navigate**: ``` mcp__playwright__browser_navigate → http://localhost:3001/?folder=/home/coder/jaffle-shop-duckdb mcp__playwright__browser_wait_for → 20 seconds (extension activation + dbt parsing) ``` **7. Open file** — two approaches, tree click is more reliable: _Option A: Click file tree_ (recommended — Ctrl+P can be flaky in Playwright): ``` mcp__playwright__browser_evaluate → const items = document.querySelectorAll('[role="treeitem"]'); const target = Array.from(items).find(i => i.getAttribute('aria-label') === 'test_query.sql'); if (target) { target.click(); target.click(); } // double-click to pin tab ``` _Option B: Quick Open_ (sometimes the file doesn't open — retry if title doesn't change): ``` mcp__playwright__browser_press_key → Control+P mcp__playwright__browser_snapshot → find textbox ref mcp__playwright__browser_type → ref=, text='test_query.sql', submit=true ``` **8. Execute and capture**: ``` mcp__playwright__browser_wait_for → 2 seconds mcp__playwright__browser_press_key → Control+Enter mcp__playwright__browser_wait_for → 12 seconds mcp__playwright__browser_take_screenshot ``` ### Common Pitfalls | Pitfall | Symptom | Fix | | --------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `docker compose restart` after rebuild | Tests pass/fail inconsistently | Always use `up --build` | | Browser connects before `dbt seed` | "Table does not exist" error | Seed first, then navigate | | Extension marked as "removed" | No dbt commands in palette | Symlink must use versioned name (`innoverio.vscode-dbt-power-user-0.60.1`), not bare name. `extensions.json` registration alone is insufficient — code-server's obsolete scanner matches directory names. See PR #1859. | | Wrong Playwright package | MCP fails to connect | Use `@playwright/mcp`, not `@anthropic-ai/mcp-playwright` | | `cd` into `node_modules/` for symlink ops | Subsequent commands run from wrong dir | Use absolute paths or don't cd | | Multiple worktrees on same Docker setup | Port conflict / container name collision | Use `docker compose -p ` and different ports in `docker-compose.override.yml` | | Quick Open (`Ctrl+P`) doesn't open file | File stays on previous tab | Use `evaluate()` to click tree items directly: `document.querySelectorAll('[role="treeitem"]')` find + click. More reliable than Quick Open in Playwright. | | `.sql` files open as "MS SQL" not "Jinja SQL" | No dbt-specific syntax highlighting | Expected until `jinjahtml` activates. Extension activation order varies — `jinjahtml` maps `.sql` → `jinja-sql` but may not be ready when the file first opens. Reload or reopen the file. | ## Troubleshooting **Container won't start:** ```bash npm run docker:stop cd docker-setup && docker compose up --build ``` **Extension not loading:** ```bash # Check the symlink exists docker exec -it docker-setup-code-server-1 ls -la ~/.local/share/code-server/extensions/ # Check extension source is mounted docker exec -it docker-setup-code-server-1 ls /home/coder/extension-src/package.json ``` **Rebuild from scratch:** ```bash npm run docker:stop cd docker-setup && docker compose build --no-cache && docker compose up -d ``` **Check dbt installation:** ```bash docker exec -it docker-setup-code-server-1 dbt --version ``` ================================================ FILE: docker-setup/deploy.sh ================================================ #!/bin/bash set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" cd "$PROJECT_ROOT" # Determine docker compose command (V2 syntax with fallback) if docker compose version &>/dev/null; then DC="docker compose -f docker-setup/docker-compose.yml" else DC="docker-compose -f docker-setup/docker-compose.yml" fi # Step 1: Check for .env file with DBT_PROJECT_PATH if [ -f "$SCRIPT_DIR/.env" ]; then echo "Loading .env from docker-setup/.env" export $(grep -v '^#' "$SCRIPT_DIR/.env" | xargs) fi if [ -n "$DBT_PROJECT_PATH" ]; then echo "Using custom dbt project: $DBT_PROJECT_PATH" else echo "Using built-in jaffle_shop_duckdb project" fi # Step 2: Build the extension echo "" echo "Building extension..." npm run build # Step 3: Build and start the container echo "" echo "Building and starting code-server container..." $DC up --build -d # Step 4: Wait for code-server to be ready echo "" echo "Waiting for code-server to be ready..." MAX_WAIT=60 WAITED=0 until curl -sf http://localhost:3001/healthz > /dev/null 2>&1; do if [ $WAITED -ge $MAX_WAIT ]; then echo "Timed out waiting for code-server after ${MAX_WAIT}s" echo "Check logs with: npm run docker:logs" exit 1 fi sleep 2 WAITED=$((WAITED + 2)) echo " Waiting... (${WAITED}s)" done echo "code-server is ready at http://localhost:3001/?folder=/home/coder/project" # Step 5: Start watch for auto-recompilation echo "" echo "Starting watch mode for hot-reload..." echo " Changes to extension source will auto-rebuild." echo " After rebuild, reload code-server in browser to pick up changes." echo "" npm run watch ================================================ FILE: docker-setup/docker-compose.yml ================================================ services: code-server: build: context: . dockerfile: Dockerfile ports: - "3001:3001" environment: - PORT=3001 volumes: - ..:/home/coder/extension-src:ro - ${DBT_PROJECT_PATH:-/home/coder/jaffle-shop-duckdb}:/home/coder/project restart: unless-stopped ================================================ FILE: docker-setup/start-code-server.sh ================================================ #!/bin/bash # Copy test projects from read-only extension-src mount to writable home dirs and install deps for project in jaffle-shop-duckdb dbt-core-sample-duckdb; do src="/home/coder/extension-src/test-fixtures/$project" dest="/home/coder/$project" if [ -d "$src" ] && [ ! -d "$dest" ]; then echo "Setting up $project..." cp -r "$src" "$dest" cd "$dest" && dbt deps 2>&1 || true fi done # Symlink the volume-mounted extension source into code-server extensions directory EXTENSIONS_DIR="$HOME/.local/share/code-server/extensions" mkdir -p "$EXTENSIONS_DIR" # Read version from package.json for the symlink name. # code-server's obsolete scanner matches directory names against the registry format # `publisher.name-version`. Without the version suffix, it marks the extension as # removed on every boot — even if extensions.json has the correct entry. EXT_VERSION=$(node -e "console.log(require('/home/coder/extension-src/package.json').version)") EXT_DIR_NAME="innoverio.vscode-dbt-power-user-${EXT_VERSION}" # Remove any stale symlink or directory rm -rf "$EXTENSIONS_DIR/innoverio.vscode-dbt-power-user" rm -rf "$EXTENSIONS_DIR"/innoverio.vscode-dbt-power-user-* rm -rf "$EXTENSIONS_DIR/altimate.vscode-dbt-power-user" # Create symlink with version suffix (required by code-server) ln -s /home/coder/extension-src "$EXTENSIONS_DIR/$EXT_DIR_NAME" # Register the symlinked extension in extensions.json so code-server doesn't mark it as obsolete. # code-server only knows about extensions installed via `code-server --install-extension`; # symlinked extensions must be added to the registry manually. EXTJSON="$EXTENSIONS_DIR/extensions.json" if [ ! -f "$EXTJSON" ]; then echo "[]" > "$EXTJSON" fi node -e " const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('/home/coder/extension-src/package.json', 'utf8')); const id = pkg.publisher + '.' + pkg.name; const dirName = '$EXT_DIR_NAME'; let exts = JSON.parse(fs.readFileSync('$EXTJSON', 'utf8')); exts = exts.filter(e => !e.identifier || e.identifier.id !== id); exts.push({ identifier: { id }, version: pkg.version, location: { \$mid: 1, path: '$EXTENSIONS_DIR/' + dirName, scheme: 'file' }, relativeLocation: dirName, metadata: { installedTimestamp: Date.now(), source: 'vsix' } }); fs.writeFileSync('$EXTJSON', JSON.stringify(exts, null, 2)); console.log('Registered', id, '@' + pkg.version, 'in extensions.json'); " # Clear .obsolete so code-server doesn't skip our extension on first scan echo '{}' > "$EXTENSIONS_DIR/.obsolete" # Determine project directory if [ -d "/home/coder/project" ] && [ "$(ls -A /home/coder/project 2>/dev/null)" ]; then PROJECT_DIR="/home/coder/project" else PROJECT_DIR="/home/coder/jaffle_shop_duckdb" fi # Start code-server with the project open exec code-server \ --bind-addr 0.0.0.0:${PORT:-3001} \ --auth none \ --disable-telemetry \ --disable-workspace-trust \ --log debug \ "$PROJECT_DIR" ================================================ FILE: documentation/docs/arch/beta.md ================================================ /// admonition | Only use the following steps for "dbt Cloud" environments. If you have a dbt Core environment, use the [required config instructions for "dbt Core" environments](../setup/reqdConfig.md). If you have a dbt Fusion environment, use the [required config instructions for "dbt Fusion" environments](../setup/reqdConfigFusion.md). type: warning /// /// admonition | dbt Cloud integration is available as beta functionality type: tip /// ## Enable dbt Cloud Integration by adding an API key dbt Cloud integration in Power User VSCode extension requires an API key. There are also multiple preview features in the extension including [generate dbt documentation](../document/generatedoc.md), [column lineage](../test/lineage.md), [query explanation](../develop/explanation.md), [generate dbt model from SQL](../develop/genmodelSQL.md) that are also enabled with an API key. /// details | You can get an API key for free by signing up at [www.altimate.ai](https://www.altimate.ai)
/// You need to add the API key from "Settings->API key" in your Altimate instance to the VSCode extension settings. You also need to add "Instance name" in the extension settings. Please get your instance name from your Altimate AI URL. If your URL for Altimate instance is - "companyx.app.myaltimate.com", then instance name is "companyx". Go to the VSCode extension settings, and then add an API key and instance name. /// details | Here's a demo of how to add an instance name and an API Key to the extension settings
/// ## Use the setup wizard for configuration (recommended) /// admonition | Need to setup environment variables? Refer to this [section](https://docs.myaltimate.com/setup/optConfig/#environment-variables-setup) type: warning /// This method will save a bunch of time for you, and you can also validate your configuration. Setup wizard will help you in associating sql files with jinja-sql, selecting the right Python interpreter, make sure dbt dependencies are correctly installed etc. In the end, it will also validate your configuration. You can start the setup wizard by clicking on dbt status icon in the bottom status bar, and performing the following necessary steps as shown in the recorded demo below:
**Here are the steps covered in the setup wizard** **Select Python Interpreter** Click on the action button - "Select Python Interpreter" and choose your preferred python interpreter. Usually, choosing an interpreter that's recommended or mapped to your virtual environment software (e.g. venv) as per the list is a good idea. If you know the path of your Python environment, you can choose it from the list, or if the path is not present, you can enter it manually. /// admonition | If needed, please run 'where python' command on terminal to see if it shows path to Python interpreter that you are using. type: tip /// **Install dbt** If dbt is not installed in your environment (dbt status icon on bottom status bar will show it), Click on "Install dbt Cloud" button in the next step. This will install latest version of dbt Cloud CLI in your environment. **Validate Project** Last step is clicking on button - "Validate Project" It will run a bunch of checks to make sure your dbt environment and project are setup correctly. If there are some issues, it will tell you exactly what's wrong as well. /// admonition | If you still can't get the extension setup correctly, please check the [troubleshooting page](../troubleshooting.md) type: tip /// ## Recorded Demo
## Questions and Answers #### Is dbt Cloud or dbt Fusion integration free? Answer: Yes, integration with dbt Cloud or dbt Fusion is free and treated the same as integration with dbt Core. It will not count towards the usage quota. ![Image](images/pricing_clarifications.png) #### Why do I need to add the Altimate API key? The API key is necessary for authentication with our backend. VSCode supports login-based authentication, but it often logs out between sessions, which can disrupt the workflow. The API key provides a more stable and streamlined experience. This is particularly beneficial for large teams, allowing them to integrate the key into their deployment secrets when setting up VSCode as a remote environment. In the future, integration with [Cloud Service token](https://docs.getdbt.com/docs/dbt-cloud-apis/authentication)s might be necessary for deeper cloud interactions, thus having the Altimate integration in place from the start makes sense. #### What benefits does registering an API key provide? A direct line of communication with our users is established with the authentication in place. This is essential for efficiently communicating hotfixes, new releases, and deprecation warnings. It helps to minimize operational challenges and ensures that users are not left with outdated versions or unaware of updates due to the limitations of VSCode or lack of IDE restarts. Our main goal is to prevent any disruption in your development environment and to support our users proactively. #### What if I don't want to use preview features or accidentally send data to Altimate? We understand the concern about using preview features and the risk of accidental data transmission. To address this, we have implemented stringent data security practices, which you can review in our [security FAQ](https://docs.myaltimate.com/arch/faq/). Our solutions have passed security reviews by several large organizations in the US, and we are open to undergoing similar reviews for your organization. Additionally, we are working on making some preview features available offline through our [open-source Python CLI package](https://github.com/AltimateAI/datapilot-cli). #### How are you addressing concerns about data transmission in preview features? To directly address concerns about data transmission, we have added a "local-mode-only" setting in VSCode. If enabled, this setting prevents backend calls for any feature except authentication. This setting can be reviewed by your security team since our [client code](https://github.com/AltimateAI/vscode-dbt-power-user/blob/master/src/altimate.ts) is open-source. Add the following setting in vscode settings.json ```json { dbt.isLocalMode: True } ``` ================================================ FILE: documentation/docs/arch/faq.md ================================================ The dbt Power User extension is developed and maintained by [Altimate AI](https://www.altimate.ai). We are a software company based in the San Francisco Bay Area and have many large enterprise companies as customers. We have done many security/governance reviews for these companies and we are SOC 2 Type 2 certified. Here is our [Privacy Policy](https://www.altimate.ai/privacy) and [Terms of Use](https://www.altimate.ai/terms). /// admonition | If you need us to do a security review with your IT / security teams, please [contact us](https://www.altimate.ai/support) via chat or Slack. type: tip /// ### **Security Measures & Protocols** ### 1. **Is my data encrypted during transmission?** Yes, all data transmitted to and from our service is encrypted using Transport Layer Security (TLS). This ensures that your data remains confidential and cannot be intercepted or tampered with during transmission. ### 2. **How do you prevent unauthorized access to your systems?** Our systems are designed with multiple layers of security: - **Transmission**: We use Transport Layer Security (TLS) to encrypt data during transmission. - **Infrastructure Security**: Our service is hosted on AWS, operating within a Private Virtual Private Cloud (VPC). This provides a secluded environment, significantly reducing intrusion risks. - **Internal Access Controls**: Only authorized developers have access to our servers. Access rights are managed and restricted using AWS's Role-Based Access Control (RBAC) mechanism. ### 3. **Do you comply with industry security standards (e.g., ISO 27001, SOC 2)?** We take security and compliance very seriously at Altimate AI, and we have SOC 2 TYPE 2 certification already. ### 4. **Where are your data centers located, and what security measures are in place there?** Our data centers are managed through Amazon Web Services (AWS), which has facilities in multiple geographic regions around the world. By leveraging AWS, we ensure our users benefit from the rigorous security standards that this leading cloud provider upholds. ### 5. **How are users authenticated and managed?** Users are authenticated with email and password combinations in the SaaS UI. In the VSCode extension, Python package, users use the API key associated with their account. In the enterprise edition, we provide OAuth authentication as well. ### 6. **Do you have a disaster recovery and business continuity plan?** Yes, at Altimate AI, we have a robust disaster recovery plan in place. Our data is backed up frequently to ensure minimal data loss. In the event of any system failure, our recovery processes are designed to restore services within an hour. This quick recovery time minimizes disruptions and ensures the continuity of our services for our users. /// admonition | If you need us to do a security review with your IT/security teams, please [contact us](https://www.altimate.ai/support) via chat or Slack. type: tip /// --- ### **Data Privacy & Retention** ### 1. **What data do you collect and for what purposes?** The only data we collect is any feedback you may choose to provide us. This feedback is stored for a brief period of 30 days. Its sole purpose is to assist us in quality improvement efforts, allowing us to identify areas where our models can be further refined and enhanced. We also collect telemetry data as per VSCode guidelines using telemetry framework offered by VSCode. Telemetry is used for error and usage reporting in order to make the extension better. You can disable telemetry if needed, as per instructions [here](https://code.visualstudio.com/docs/getstarted/telemetry#_disable-telemetry-reporting). ### 2. **How do you ensure my data privacy?** At Altimate AI, ensuring the privacy of your data is a top priority. Here's how we uphold it: - **Data Isolation**: We employ a multi-tenant architecture that inherently isolates data on a per-tenant basis. - **Strict Access Controls**: Only a select group of authorized developers can access the collected metadata. We employ Amazon Web Services' Identity and Access Management (IAM) policies to meticulously restrict and control access to our various data stores. ### 3. **How long do you retain my data?** At Altimate AI, we maintain a strict policy of not retaining data related to the requests you make while using our service. The only data we retain is any feedback you may choose to provide us. This feedback is stored for a brief period of 30 days. Its sole purpose is to assist us in quality improvement efforts, allowing us to identify areas where our models can be further refined and enhanced. ### 4. **What's your stance on GDPR?** We do not store any actual customer data, we only store aggregate statistics and metadata. As a result, GDPR data deletion requests do not need to be propagated to us because we do not store such data. Our customers typically do not request or require DPAs. However, we're happy to provide a DPA or review a vendor DPA if your organization needs it. /// admonition | If you need us to do a security review with your IT/security teams, please [contact us](https://www.altimate.ai/support) via chat or Slack. type: tip /// ### 5. **What is your cookie policy?** We store only essential cookies, but not to track user data but only to keep few features working as expected. We use only following first party cookies - **Intercom**: Used for support requests. Cookies stored by Intercom and their cookies policy is defined [here](https://www.intercom.com/help/en/articles/2361922-intercom-messenger-cookies) - **Supertokens**: Used for SSO authentication and these cookies will be stored only for tenants with SSO. More details on Supertokens cookies policy can be viewed [here](https://supertokens.com/docs/passwordless/common-customizations/sessions/cookie-consent) --- ### **Use of Data for AI Model Training** ### 1. **Do you use my data to train your AI models?** At Altimate AI, our primary objective is to provide accurate and efficient documentation using our AI models. However, we do not use any specific client data to train our models. Our models are designed to be tenant-agnostic, meaning they do not learn or differentiate based on individual client data. Any data processed by our service is not repurposed for model training or enhancement. ### 2. **How do you ensure my data isn't unintentionally used for model improvement?** We employ strict data isolation and access controls. The multi-tenant architecture isolates data on a per-tenant basis, and our internal access controls ensure that only a select group of authorized developers can access the metadata. Coupled with our tenant-agnostic model approach, our infrastructure is designed to prevent any unintentional usage of your data outside its primary purpose. ### 3. **Can I opt-in or opt-out of allowing my data to be used for model training in the future?** Currently, we do not use client data for model training, so there's no opt-in or opt-out mechanism. If our policy were to change in the future, we would provide users with clear communication and choices regarding the use of their data. /// admonition | If you need us to do a security review with your IT/security teams, please [contact us](https://www.altimate.ai/support) via chat or Slack. type: tip /// --- ### **LLM & AI Security** ### 1. **How does Altimate handle LLM access?** Altimate gives you two options for LLM access: - **BYOK (Bring Your Own Key)** — Always free and unlimited. Use your own API keys from any of 35+ supported providers (Anthropic, OpenAI, AWS Bedrock, Azure OpenAI, Google, Ollama, and more). With BYOK, your data goes directly to your chosen provider — **Altimate never sees it**. - **[Altimate LLM Gateway](llm-gateway.md)** — A managed option for users who don't want to manage API keys. The gateway dynamically routes to the best model for each task. See below for its security details. ### 2. **Does the Altimate LLM Gateway store my prompts or responses?** Your full prompts and responses are not retained after processing. Your data is not used to train, fine-tune, or improve any models. The gateway does store limited **metadata** for each request: - Number of prompt and completion tokens - Latency - Model used This metadata is used for billing, performance monitoring, and routing optimization. Additionally, a small number of prompts are sampled for **anonymous categorization** to power routing and model ranking. This categorization is stored completely anonymously and is never associated with your account or user ID. No code, SQL, credentials, or PII are retained. You can opt out of all telemetry by reaching out to us via the Intercom chat built into the [Altimate dashboard](https://app.myaltimate.com). ### 3. **What metadata does the Altimate LLM Gateway collect?** | Metadata | Purpose | |----------|---------| | Number of prompt tokens | Usage tracking and billing | | Number of completion tokens | Usage tracking and billing | | Latency | Performance monitoring | | Model used | Routing optimization | | Anonymous prompt categorization (sampled) | Model ranking and routing | Prompt categorization is stored completely anonymously — never linked to your account or user ID. You can opt out via the Intercom chat in the [Altimate dashboard](https://app.myaltimate.com). /// admonition | If you need us to do a security review with your IT/security teams, please [contact us](https://www.altimate.ai/support) via chat or Slack. type: tip /// --- ## **What data get sent to the SaaS backend for the preview features?** ### Model Definition - **Model Name:** The name of the model. - **Model Schema:** Schema details of the model. - **Model SQL:** SQL queries related to the model. - **Adapter Type:** Type of adapter used. The above model attributes will be referenced in the following feature descriptions ### 1. Documentation Generations - **Model Attributes:** Refer to the Model Definition. - **Existing Documentation:** Any existing documentation for the model. - **Parent Models:** Corresponding parent models associated with the current model. ### 2. SQL to Model - **SQL:** SQL queries used. - **Adapter Type:** Type of adapter used (Refer to Model Definition for adapter type details). - **All Models Present:** List of all models present. - **All Sources Present:** List of all sources present. ### 3. SQL Explanation - **SQL:** SQL queries used for explanation. - **Adapter Type:** Type of adapter used (Refer to Model Definition for adapter type details). ### 4. Column Lineage - **SQL:** SQL queries related to column lineage. - **Adapter Type:** Type of adapter used (Refer to Model Definition for adapter type details). - **Model Attributes:** Refer to the Model Definition. - **Upstream and Downstream Models Attributes:** Information about all upstream and downstream models. Includes models open in the LINEAGE panel in the extension and additional ones necessary for generating the lineage. (Refer to the model Definition for details about model attributes). ### 5. Defer-to-prod (with saas mode - "DataPilot dbt integration" option) - **dbt manifest file** In the SaaS mode - when you configure "DataPilot dbt integration", manifest files are uploaded to the SaaS instance. /// admonition | If you would like to connect your on-premise storage for manifest file uploads, please [contact us](https://www.altimate.ai/support) via chat or Slack. type: info /// All of the details can be found in the code [here](https://github.com/AltimateAI/vscode-dbt-power-user/blob/master/src/altimate.ts). Please note that we only send meta-data, such as model schema and queries to the backend. We never send actual data to the backend and we do not store any of the meta-data. /// admonition | If you need us to do a security review with your IT/security teams, please [contact us](https://www.altimate.ai/support) via chat or Slack. type: tip /// ================================================ FILE: documentation/docs/arch/llm-gateway.md ================================================ --- status: new --- # Altimate LLM Gateway The Altimate LLM Gateway is a managed LLM service that gives you access to the best AI models — **60-80% cheaper** than buying tokens directly from providers. No API keys to manage, no billing across multiple providers, no rate limits to worry about. ## How It Works The gateway dynamically routes each request to the best model for the task across **Sonnet 4.6, Opus 4.6, GPT-5.4, GPT-5.3, and GPT-5.4-mini**. You pay a flat token price regardless of which model handles your request — no surprise bills from expensive model routing. ## Pricing | Plan | Price | Tokens/mo | $/M tokens | Overage (per 1M tokens) | |------|-------|-----------|------------|------------------------| | **Community** | $0/mo | 10M (one-time) | Free | BYOK only | | **Pro Tier 1** | $29/mo | 20M | $1.45 | $5/M tokens | | **Pro Tier 2** | $89/mo | 70M | $1.27 | $3/M tokens | | **Enterprise** | Custom | Custom | Custom | Negotiated | Tokens are counted as input + output combined. All tiers get access to all models — the upgrade incentive is volume, not capability. ## What Would This Cost You Directly? Buying 20M tokens directly from providers: | Model | Direct Cost (20M tokens) | With Altimate Pro Tier 1 | Savings | |-------|--------------------------|--------------------------|---------| | Sonnet 4.6 | ~$84 | **$29** | ~65% | | Opus 4.6 | ~$140 | **$29** | ~79% | | GPT-5.4 (short context) | ~$75 | **$29** | ~61% | | GPT-5.4 (long context) | ~$135 | **$29** | ~79% | With Altimate, you pay $29 flat regardless of which model handles your task. Buying the same 20M tokens directly from providers would cost $75-140 depending on the model — and you'd have to manage API keys, billing, and rate limits across multiple providers yourself. ## BYOK vs. Gateway | | BYOK (Bring Your Own Key) | Altimate LLM Gateway | |---|---|---| | **Cost** | Free and unlimited | Token-based pricing (10M tokens free) | | **API Keys** | You manage your own keys | No keys needed | | **Models** | Any model from your provider | Dynamic routing across best-in-class models | | **Data Path** | Direct to your provider — Altimate never sees it | Through Altimate — see [Security FAQ](faq.md#llm-ai-security) for data handling details | | **Best For** | Users with existing API keys or strict data residency requirements | Users who want simplicity and cost savings | Both options are always available. You can use BYOK and the gateway side by side. ## Models Available The gateway routes across the following models based on task complexity, context length, and quality requirements: | Model | Provider | Strengths | |-------|----------|-----------| | **Claude Sonnet 4.6** | Anthropic | Excellent price/performance for most data engineering tasks | | **Claude Opus 4.6** | Anthropic | Highest quality for complex reasoning and analysis | | **GPT-5.4** | OpenAI | Strong general-purpose capabilities | | **GPT-5.3** | OpenAI | Cost-effective for simpler tasks | | **GPT-5.4-mini** | OpenAI | Fast, lightweight tasks | You don't choose the model — the gateway selects the optimal one for each request automatically. ## Security The Altimate LLM Gateway is designed with enterprise security requirements in mind: ### Data Handling - **Your data is not used to train, fine-tune, or improve any models.** - The gateway stores limited metadata (token counts, latency, model used) for billing and routing. - A small number of prompts are sampled for anonymous categorization to improve routing. See [Security FAQ](faq.md#llm-ai-security) for full details. ### Compliance - **SOC 2 Type II** certified - **TLS 1.3** encryption for all data in transit - AWS infrastructure in private VPC with network isolation - IAM-based RBAC with MFA enforcement for developer access /// admonition | If you need us to do a security review with your IT/security teams, please [contact us](https://www.altimate.ai/support) via chat or Slack. type: tip /// ## Getting Started 1. Install the [Datamates extension](https://marketplace.visualstudio.com/items?itemName=altimateai.vscode-altimate-mcp-server) in your IDE 2. Open the command palette (`Cmd+Shift+P` / `Ctrl+Shift+P`) and select **Datamates: Open Altimate Code Chat** 3. The Community plan with 10M free tokens is available immediately — no credit card required To upgrade or manage your plan, visit the [Altimate pricing page](https://www.altimate.ai/pricing). ================================================ FILE: documentation/docs/arch/pricingfaq.md ================================================ The Power User for dbt extension is developed and maintained by [Altimate AI](https://www.altimate.ai). We are a software company based in the San Francisco Bay Area, and have many large enterprise companies as customers. ### **Info about pricing plans is available on the [Pricing page](https://www.altimate.ai/pricing)** ### 1. **Are you going to charge for current free features in the "power user for dbt" extension?** No. We have no plans to convert previously available free features into paid features. Newly developed features may be released with a credit quota for the free community plan. Please check the [pricing page](https://www.altimate.ai/pricing) for more details. ### 2. **What exactly is credit for the feature?** You can find the definition of credit for different features in the table below. | Feature | Definition of credit | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Docs Generation | Each generated description = 1 credit. For bulk generation, 3 generated descriptions = 1 credit. If you bulk generate descriptions for more than 300 columns, only first 100 credits are counted | | Column Lineage | Each column lineage generation = 1 credit. Once the lineage for a particular column in a model is generated, no additional credits are counted for that particular column lineage unless VSCode is restarted | | Query Explanation | Each query explanation= 1 credit, Follow-up question = 1 credit | | Model Generation | Each dbt model generation = 2 credits | | Model Updates | Each model change generation = 1 credit | | Tests Generation | Each test generation = 1 credit. No credit count for adding default tests without code generation. | | Defer to Prod | Each dbt command executed with defer flag (saas mode) = 1 credit | | Query Translation | Each query translation = 3 credits | | Project Governance | Each scan = 5 credits. Same project scan is not counted again unless VSCode is restarted. | | Collaboration | One share = 5 credits. It doesn't matter how many comments are added, only first time share operation is counted. | | SQL Visualizer | Each DAG Generation = 1 credit. No additional credits are counted for creating that particular DAB unless VSCode is restarted | | AI agent in UI | Each agent execution = 1 credit | Features that are not listed here, have unlimited free usage. Their usage is not counted towards the no. of credits ### 3. **What is considered an upgrade or downgrade of the subscription?** Upgrade or downgrade of the subscription is determined based on the price of the subscription plan. If the price of the original subscription plan is higher than the price of the newer subscription plan, it's considered a downgrade. If the price of the original subscription plan is lower than the price of the new subscription plan, it's considered an upgrade. ### 4. **What happens during an upgrade of the subscription?** As soon as you upgrade the subscription, changes will be made to your instance for the new subscription. You will be charged the difference between a new subscription price and the prorated current subscription price based on the no. of credits remaining. ### 5. **What happens during a downgrade of the subscription?** Your current subscription will be downgraded at the end of the current billing cycle. There is no refund when you downgrade your subscription, as the subscription is downgraded at the end of the billing cycle. ### 6. **What happens if I change from a yearly subscription to a monthly subscription?** If the price of the monthly subscription is lower than the yearly subscription, it's considered a downgrade. So, the change in the subscription will be made only after the current billing period is over. If you would like to make an immediate change to your subscription, make the change at a yearly plan level. It will be considered an upgrade and will be effective immediately. /// admonition | If you can't find your question here, please [contact us](https://www.altimate.ai/support) via Slack. type: tip /// ================================================ FILE: documentation/docs/develop/autocomplete.md ================================================ dbt-power user extension auto-completes model, macro, column names in the VSCode ## Models a) Autocomplete model ![Autocomplete model](images/autocompleteModel.gif) b) Go to model definition ![Go to model definition](images/definitionModel.gif) ## Macros /// details | a) Autocomplete macro ![Autocomplete macro](images/autocompleteMacro.gif) /// /// details | b) Go to macro definition ![Go to macro definition](images/definitionMacro.gif) /// ## Sources /// details | a) Autocomplete source ![Autocomplete source](images/autocompleteSource.gif) /// /// details | b) Go to source definition ![Go to source definition](images/definitionSource.gif) /// ## Doc blocks /// details | a) Autocomplete doc block ![Autocomplete doc block](images/autocompleteDoc.gif) /// /// details | b) Go to doc block definition ![Go to doc block definition](images/definitionDoc.gif) /// ================================================ FILE: documentation/docs/develop/clicktorun.md ================================================ There are two methods to do it. You can either do it from the top right corner toolbar or from the extension side pane ### Method 1: Build and run models from the toolbar The toolbar action to build models is present on the top right corner of the VSCode as shown in the image below: ![buildRunModels](images/buildRunModels.png) ### Method 2: Run models from the side panel ![extensionPanel](images/extensionPanel.gif) /// admonition | You cannot build models from the side panel type: info /// ================================================ FILE: documentation/docs/develop/compiledCode.md ================================================ ## Preview compiled code (SQL) The toolbar action to preview compiled code is present on the top right corner of the VSCode as shown in the image below: ![buildRunModels](images/compiledPreview.png) ================================================ FILE: documentation/docs/develop/explanation.md ================================================ Query explanation is invaluable to understanding a complex piece of dbt or SQL code (especially written by others!). ## Start Query Explanation You can get an explanation for the entire query code in the file or selected parts. If you need an explanation for only some part of the code, select that code first. You can trigger the query explanation functionality in three different ways. ### Right Click Menu #Right-click, select "DataPilot" menu, and choose an action to "explain" the query.
![rightClick](images/queryExplainRightClick.png)
### Utilize SQL actions menu Press "SQL actions" button from the toolbar. It will open the "SQL actions" menu drawer, as shown below. Please select the "Explain query" action.
![sqlActions](images/queryExplainSQLActions.png)
### Explain query button from the query results panel There is an explain query button in the query results panel where you can see compiled SQL. Pressing that button will start query explanation workflow.
![explainButton](images/queryExplainButton.png)
## Get more details in the DataPilot chat Once you start the query explanation workflow in the DataPilot chat panel on the left-hand side, DataPilot will show more specific suggestions to get detailed explanations on specific areas. You can choose one of those suggestions or ask more questions in the input box below the suggestions, as shown in the image below. ![explainSuggestion](images/queryExplainSuggestion.png)
## Recorded demo video /// admonition | Please provide feedback on the generated explanations using thumbs up / down buttons. Your feedback will help us tremendously to improve this functionality. type: tip /// /// admonition | This feature requires an API key. You can get it by signing up for free at [www.altimate.ai](https://www.altimate.ai) type: info /// ================================================ FILE: documentation/docs/develop/genmodelSQL.md ================================================ You can convert existing SQL into dbt model as below. The extension automatically converts SQL to jinja-sql by populating right references.
/// admonition | This feature requires an API key. You can get it by signing up for free at [www.altimate.ai](https://www.altimate.ai) type: info /// ================================================ FILE: documentation/docs/develop/genmodelSource.md ================================================ Generating model from sources defined in yaml file is very easy as below: ![Generate model from source](images/generateModelSource.gif) /// details | You can configure a file name template and prefix in the extension settings ![Generate model settings](images/genModelSettings.png) /// /// admonition | Why does the generated model appear with the syntax {{ adapter.quote(column_name)}}? type: info This syntax provides a safe way for the adapter to quote the columns. Since the extension supports different adapters, this is the easiest way to ensure that it works for all of them. /// ================================================ FILE: documentation/docs/develop/translateSQL.md ================================================ You can translate SQL queries from one dialect to another using this functionality. For example, translate query in Postgres SQL dialect to Snowflake SQL dialect ### Supported SQL dialects athena, bigquery, clickhouse, databricks, doris, drill, duckdb, hive, mysql, oracle, postgres, presto, prql, redshift, snowflake, spark, sqlite, starrocks, tableau, teradata, trino, tsql ### Step 1: Create a new file and add SQL query ![newTranslateFile](images/newTranslateFile.png) ### Step 2: Right click, and choose DataPilot -> Translate (SQL Dialect) ![rightClickTranslate](images/rightClickTranslate.png) ### Step 3: Provide source and destination dialect DataPilot chat interface will open on the left hand side, where you should provide source and destination dialects. Source dialect: Current dialect of the SQL present in the file Destination dialect: The SQL dialect in which you want to translate the query /// admonition | Destination dialect value is auto-populated in the drop-down based on the data backend for current dbt Project. It can be changed by clicking on it. type: tip /// Hit the "Translate" button after source and destination dialects are provided. ![translateButton](images/translateButton.png) /// admonition | Query Translate (SQL Dialect) functionality works on the whole file, and not selected code snippet. DataPilot will proceed with assumption that the whole file needs to be translated. type: info /// ### Step 4: Review Translated SQL and Explanation In this step, DataPilot will give you translated SQL along with explanation of what has been changed in translation. Different databases use different functions for common operations or sometimes syntax is different. DataPilot translation takes care of these differences and it also provides explanation of those differences. ![actulTranslation](images/actualTranslation.png) /// admonition | You can click on "Replace" button below translated query, and translate query will replace the original query in the file. type: tip /// ### Step 5: Convert SQL to dbt Model (optional) If needed, you can covert SQL from a file to a dbl model with the SQL to dbt Model functionality [(details)](genmodelSQL.md) ### Limitations /// details | Following are a few limitations - If there are functions we can't identify, then we will not be able to convert it. In those scenario, it will be kept as is. - We do not look at data types. If there are some data types which are not supported in the target database, we may not be able translate those /// ### Recorded Demo ================================================ FILE: documentation/docs/develop/updatemodel.md ================================================ Updating or changing an existing dbt (or SQL) model using natural language is quite straightforward with this functionality. ### Step 1: Trigger the Operation Select a piece of code, or if you want to use a whole file, right-click and choose the DataPilot menu and choose the "change" submenu ![Start Query Change](images/startDataPilotChange.png) ### Step 2: Instructions for Changes Give instructions for changes in the input text box ![Input to DataPilot for change](images/changeDataPilotChat.png) ### Step 3: Copy the Code #Copy the changed code and put it in a file ![Copy Changed Code](images/createCodeFile.png) ### Recorded demo video /// admonition | Please provide feedback on the generated explanations using thumbs up / down buttons. Your feedback will help us tremendously to improve this functionality. type: tip /// /// admonition | This feature requires an API key. You can get it by signing up for free at [www.altimate.ai](https://www.altimate.ai) type: info /// ================================================ FILE: documentation/docs/discover/setupui.md ================================================ This page covers the setup steps necessary to view your dbt documentation and lineage in the SaaS UI. The steps below ship your manifest.json and catalog.json projects to SaaS UI in order to visualize information like dbt model/column descriptions and column lineage. /// admonition | Please note that this lineage and documentation in UI functionality is not yet supported with dbt 1.8 type: info /// /// admonition | If you want to re-create any existing dbt core integration using Connections, kindly delete the existing integration first and then create a fresh connection. type: warning /// ## Step 1: Create a dbt Core Connection 1. Navigate to **Settings -> Connections** and click **Create new connection** ![DBT Core Connection](images/DBT_Cloud_Connection.png) 2. Select **dbt Core** as the connection type & provide the required connection name & description details ![Create DBT Core Connection](images/DBT_Core_Create_Connection.png) 3. Provide **Environment Name** & Click **Create Connection** to create the dbt Core connection ![Final Create DBT Core Connection](images/DBT_Core_Create_Final_Connection.png) | Field | Description | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Connection name | Unique connection name, this can be mapped to your dbt Project | | Connection description | A brief description of the connection (e.g., "Production dbt Core project for analytics") | | Environment name | Environment name can be based on which environment that you are going to upload manifest.json and catalog.json files from. For now, just add the value as "prod" for your production environments. | ## Step 2: Install the open-source DataPilot CLI The next step is to install the latest version of DataPilot CLI. It will be used to upload manifest and catalog files to the SaaS instance. Please run the following command to install the latest version of the DataPilot CLI. ``` pip install altimate-datapilot-cli --upgrade ``` Here's the link to the repo: [https://github.com/AltimateAI/datapilot-cli](https://github.com/AltimateAI/datapilot-cli) ## Step 3: Execute the command for uploading the manifest and catalog files Go to **Settings -> Connections** page and click on the dbt core connection name for the connection created. Copy the command for uploading files in the overlay screen on the side. /// admonition | manifest and catalog files don't contain any information about your data. It's all metadata about your environment. Please feel free to check our [security page](https://docs.myaltimate.com/arch/faq/) for more info on how we protect your metadata. type: info /// ![DBT Core Connection Details](images/copyCommand.png)
You need to update the following placeholders in the copied command - | Placeholder | Description | Example | | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | | Path/to/manifest/file | This is path to your manifest file in the project directory. It's usually stored in the 'target' directory in your dbt project. | ./target/manifest.json | | Path/to/catalog/file | This is the path to your catalog file in the project directory. It's usually stored in the 'target' directory in your dbt project. | ./target/catalog.json | | Path/to/run-results/file | Path to your run results file in the project directory | ./target/run_results.json | | Path/to/semantic-manifest/file | Path to your semantic manifest file in the project directory | ./target/semantic_manifest.json | | Path/to/sources/file | Path to your sources file in the project directory | ./target/sources.json | /// admonition | In addition to the required `manifest.json` and `catalog.json`, we now support uploading additional artifacts — `run_results.json`, `semantic_manifest.json`, and `sources.json` — for richer insights. These are optional but recommended for complete visibility into your dbt project. type: info /// /// admonition | If you are missing manifest.json or catalog.json files in the target directory, please run the `dbt build` and `dbt docs generate` commands. Also, you can add steps to upload the manifest and catalog files command in your dbt pipelines. That way, you will always have up-to-date documentation and lineage in UI without any manual steps. type: tip /// Here's the sample output after running the command and successfully uploading your files. ``` (.venv) pradnesh@pradneshs-MacBook-Air jaffle_shop % datapilot dbt onboard --backend-url https://api.tryaltimate.com --token 00x0x0x0x0x0x0 --instance-name freemegatenant --dbt_core_integration_id 1 --dbt_core_integration_environment prod --manifest-path ./target/manifest.json --catalog-path ./target/catalog.json Manifest onboarded successfully! Catalog onboarded successfully! Manifest and catalog ingestion has started. You can check the status at https://freemegatenant.demo.tryaltimate.com/settings/integrations/1/prod ``` /// admonition | It takes a few minutes to upload the files and sync that info with the rest of the UI. You can check the status of the upload by going to the link provided in the command output. type: tip /// ## Automating with CI/CD Pipelines To ensure your dbt documentation and lineage in the UI stays up-to-date automatically, we strongly recommend integrating the manifest and catalog upload process into your CI/CD pipeline. This eliminates manual steps and ensures that any changes to your dbt project are immediately reflected in the SaaS UI. ## Automatic Sync with dbt Cloud Connection For dbt Cloud users, you can now set up automatic artifact syncing in the SaaS UI using the dbt Cloud API connection. This eliminates the need for manual file uploads or CLI commands. ### Prerequisites: Create a dbt Cloud Service Token Before setting up the connection, create a service token in dbt Cloud with **Job Viewer** permission. This grants read-only access to the Jobs API for fetching artifacts (manifest.json, catalog.json) from your dbt Cloud runs. 1. Click your account name in the left menu and select **Account settings** 2. Select **Service Tokens** from the left sidebar 3. Click **+ New Token** 4. Enter a descriptive name (e.g., "Altimate Integration") 5. Assign the **Job Viewer** permission and select the projects you want to sync 6. Click **Save** 7. **Important**: Copy and save the token immediately — you won't be able to view it again ![DBT Cloud Connection](images/dbtCreateServiceToken.png) ![DBT Cloud Connection](images/dbtServiceTokenPermission.png) > **Note**: Permission availability may vary by dbt Cloud plan. Refer to the [dbt Cloud Service Tokens documentation](https://docs.getdbt.com/docs/dbt-cloud-apis/service-tokens) for details. ![DBT Cloud Connection](images/DBT_Cloud_Connection.png) ### Setup Steps 1. Navigate to **Settings -> Connections** and click **Create new connection** 2. Select **dbt Cloud** as the connection type 3. Provide the required connection details: 4. **Service Account Token**: Generate a new Service Token from dbt Cloud Account Settings ([learn more](https://docs.getdbt.com/docs/dbt-cloud-apis/service-tokens)) 5. **Account ID**: Available at `https://cloud.getdbt.com/next/settings/accounts/{{account_id}}` 6. **Custom URL** (optional): For custom dbt Cloud instances (defaults to `https://cloud.getdbt.com/api/v2/`) 7. Click **Test Connection** to verify your setup 8. Configure the sync schedule: - **Scheduled**: Sync artifacts on a regular schedule. Select from Daily, Weekly, or Monthly frequency options and choose the time (UTC) when sync should occur (e.g., Daily at 12:00 AM UTC) - **Real-time**: (Coming soon) Immediate sync when dbt Cloud runs complete 9. Click **Create Connection** After creation, your dbt Cloud projects and environments will be automatically discovered.
/// admonition | Automatic syncing keeps your documentation and lineage always up-to-date without manual intervention type: tip /// /// admonition | The dbt cloud connection deletion has a processing delay of a few hours. If you need to recreate the same connection immediately, contact us. type: warning /// ================================================ FILE: documentation/docs/discover/viewdocs.md ================================================ This page highlights functionality for searching and viewing documentation for your DBT Projects. Please go to Code -> dbt from the navigation menu on the left-hand side to view all your dbt models, seeds, and other components. /// admonition | [Setups steps](./setupui.md) needed for the information to show in SaaS UI type: warning /// ## Search and Filter On top of that, you can search for specific entities or columns by name via the search bar. This search also searches across the descriptions written for models and columns. On the left-hand side, there are different filters available for dbt project name, entity type, materialization etc., so you can filter different entities easily. ![dbt Models](images/dbtModel.png)
## View Schema Suppose you want to get a quick view of the schema. Click "View Schema" button for a particular dbt Model, and the side screen will quickly show the view of schemas. In this view, you can also quickly see the documentation written for the model and columns. ![view Schemas](images/viewSchema.png)
## View Details (Docs) The "View details" button takes you to a more detailed view, where you can see the schema, documentation, lineage, and code information. ![view Details](images/viewDetails.png)
In addition to schemas, you can also view the actual code and compiled code in the "Code" tab. ## Recorded Demo /// admonition | Using this feature requires an API key. You can get it by signing up for free at [www.altimate.ai](https://www.altimate.ai) Also, you need to perform the setup steps outlined on the [Setup UI page](setupui.md) type: info /// ================================================ FILE: documentation/docs/discover/viewlineage.md ================================================ In the DataPilot SaaS UI, you can see model level as well as column level lineage. You can also see the types of changes that occurred during the column lineage traversal e.g. column was unchanged or alias was used /// admonition | [Setups steps](./setupui.md) needed for the information to show in SaaS UI type: warning /// ## View Model Level Lineage Go to code -> dbt from the left-hand navigation menu and see the available list of dbt Models. You can also search on the top or use provided filters to find the exact model you are looking for. ![dbt Models](images/dbtModel.png)
Click on the "View Details" button and go to the Lineage tab. You can expand the lineage further by clicking on (+) signs on individual blocks. ![View Lineage](images/viewLineage.png)
/// admonition | If you click on the specific model in the lineage view, the lineage graph for only that model will be highlighted, as shown in the image above. type: tip /// ## View Column Level Lineage In order to view column-level lineage, first click on "View Details" button in one of models shown in the lineage view. This will display a list of columns present in the model. Click on the column for which you need to view the lineage. ![Column Selection](images/columnSelection.png)
Once you click on the column, you will see the column lineage view as below: ![Lineage SaaS](images/lineageSaaS.png)
## Code transformations You can also see how that particular column was created from previous stage columns via transformation information available as icons at each block level. The following type of transformations are shown in this view: | Type | Description | | -------------- | ----------------------------------------------------- | | Original | The original column in the lineage graph | | Alias | Alias was used on the previous stage columns | | Transformation | Transformation was used on the previous stage columns | | Unchanged | No change was made in the previous stage column | | Not sure | Not known how this column was created | If code is available for a particular transformation, a small code icon is displayed. When you click on the code icon, it shows the list of code transformations that were performed to create that column. ![SaaS Code](images/saasCode.png) ## Recorded Demo /// admonition | Using this feature requires an API key. You can get it by signing up for free at [www.altimate.ai](https://www.altimate.ai) Also, you need to perform the setup steps outlined on the [Setup UI page](setupui.md) type: info /// ================================================ FILE: documentation/docs/document/docblocks.md ================================================ # Support for dbt Doc Blocks dbt Power User provides comprehensive support for dbt doc blocks, allowing you to create, manage, and reference documentation blocks throughout your dbt project.
## What are dbt Doc Blocks? Doc blocks are reusable documentation components in dbt that allow you to define documentation once and reference it multiple times across your project. They're defined in `.md` files and can contain any markdown content including text, images, tables, and code snippets. ```markdown {% docs doc_description %} This is a reusable documentation block that can be referenced throughout your dbt project. {% enddocs %} ``` ## Referencing Doc Blocks ### In Schema Files Doc blocks can be referenced in your `schema.yml` files for consistent documentation across models: ```yaml version: 2 models: - name: my_model description: "{{ doc('my_model') }}" columns: - name: model_id description: "{{ doc('model_id') }}" ``` ## Propagating Doc Blocks Using the Propagate Docs Selector - you can select which downstream models you want to propagate docs to: ![propagateSelector](images/propagateSelector.png) ================================================ FILE: documentation/docs/document/generatedoc.md ================================================ Instead of writing documentation manually, you can generate the documentation! ### Bulk generate documentation Bulk documentation generation is possible for all columns. You can also choose to bulk-generate documentation only for columns that are missing descriptions. ![Bulk Edit Options](images/bulkEditOptions.png) ### Regenerate documentation based on preferences around length and persona We have added the DataPilot chat panel on the left-hand side. There, you can regenerate documentation based on specific preferences e.g. personas, content length. ### Language and Persona Settings When it comes to generating documentation, the following settings are available:
**Language:** You can choose between French, English, Dutch, German at this time.
**Personas:** you can choose between technical user, business user and general user. If you specify general user, we don't use specific persona to generate documentation.

Settings can be configured in the settings panel. Settings panel is opened by clicking "Settings" button in the top right corner of the Documentation Editor tab. Existing documentation (whether generated or written manually) can be further updated using regeneration functionality with DataPilot. /// admonition | Save changes in YAML file type: tip You can save the changes in the existing or a new YAML file with save button at the bottom of the panel. If you see any issues with the content that's saved in the YAML file, please check the [optional config section](../setup/optConfig.md/#column-name-setup-for-yaml-file-updates). /// ### Propagate docs to downstream models You can propagate already written descriptions to downstream model columns easily. **First, click on "Docs Propagation" button for the particular column** ![Propagate Docs](images/propagateDocs.png) **Select columns in different models where docs should be propagated** Select the checkboxes for right columns in models and click on the "Propagate documentation to selected models" button ![Choose Models](images/chooseModels.png) ### Personalize and Coach AI You can personalize and coach the Documentation Writer AI teammate. /// admonition | Personalize and Coach Documentation Writer AI type: tip Please check more info about how to personalize and coach documentation writer AI teammate [here](../teammates/coach.md). If you would like to learn more about AI teammates, please check this [doc page](../teammates/introduction.md) /// ### Documentation Collaboration You can also enable reviews of documentation via collaboration workflow. Please check details [here](../govern/collaboration.md#document-collaboration-workflow) ### Interactive Demo Here's a demo of generating model and column descriptions:
/// admonition | Document generation or propagation requires an API key. You can get it by signing up for free at [www.altimate.ai](https://wwww.altimate.ai) type: info /// ### Recorded Demo
================================================ FILE: documentation/docs/document/write.md ================================================ You can write descriptions for dbt models and columns in the Documentation Editor. The documentation editor also shows the descriptions that were written previously. Those previously written descriptions can be updated as well. For columns that are not referenced in the dbt model, you can click “sync with db” button to get those columns shown in the documentation editor. /// admonition | Save changes in YAML file type: tip You can save the changes in the existing or a new YAML file with the save button at the bottom of the panel. If you see any issues with the content that's saved in the YAML file, please check the [optional config section](../setup/optConfig.md/#column-name-setup-for-yaml-file-updates). ///
/// admonition | You can also generate the documentation, please refer the section on [generate documentation](generatedoc.md) type: tip /// ================================================ FILE: documentation/docs/govern/collaboration.md ================================================ Collab functionality enables you to discuss code and documentation easily with the stakeholders via VSCode and UI. Many stakeholders are not comfortable using IDE directly; this functionality enables them also to have a discussion with technical users. ## Code Collaboration Workflow /// admonition | Code Collaboration workflow allows you to discuss code without creating a PR type: tip /// ### Start a discussion You can start a discussion at the file or code block levels (single or multiple lines) by pressing (+) sign next to the code. ![StartDiscussion](images/startDiscussion.png)
/// admonition | If you click on the display icon as shown in the image above, you can show details of the discussion previously started type: tip /// When a discussion is started, the extension generates dbt docs and uploads those docs to the Altimate AI SaaS instance so that non-technical stakeholders can contribute to the discussion via SaaS UI as well. ### Add a comment Once the discussion is started, you can publish a comment and also tag other users from the Altimate AI SaaS instance. ![Add comment](images/discussionText.png)
/// admonition | If you don't want to tag the user but still want to share the link with them, just copy the link from the comments box, and share it with them manually (Slack, Email etc.) type: tip /// ### Email notification (if a user is tagged) Tagger user receives an email notification with a link to open the discussion in the Altimate AI UI. ![Sample Email](images/sampleEmail.png)
### Non-technical users can reply to comment via UI ![Comment UI](images/commentUI.png) ### Technical user can see the comment in IDE directly The technical user who started the discussion in IDE can see the replies from other users directly in the IDE. ![List Comments](images/listComments.png) The discussion can continue from here onwards or it can be resolved by clicking resolve action available on top of the comment textbox. Discussion can be started from a UI also similarly, by clicking on (+) sign next to the code blocks. ## Document Collaboration Workflow Table and column descriptions can also be reviewed and discussed with this functionality. ### Start a discussion Discussion can be started via VSCode in the Documentation Editor panel or from SaaS product UI also from the link shared as covered in code collaboration workflow above. ![Doc Discussion](images/startDocDiscussion.png) ### Add a comment You can add a comment and tag a user to the comment. In that case, that particular user will receive an email notification to open the discussion. Please check the code collaboration workflow for email notification example. ![Doc Comment](images/docDiscussion.png) ### View all doc discussions You can view all doc discussion on line no. 1 of the model file, and copy discussion links from there. Click on the display icon shown on the left-hand side of line no. 1. You can copy the documentation discussion link from here and share it with other users. You can also resolve the discussion as needed. ![Doc Comment](images/firstLineList.png) ## Lineage Export Workflow You can export lineage from IDE to SaaS UI and share the URL with other team members for better collaboration ### Export lineage You can export model-level and column-level lineage. Just click the export button at the top of the panel and give the export a name. ![Export Action](images/exportAction.png) Once you create an export, a link will be generated at the bottom. You can copy this link and share it with others. ![Export Link](images/exportLink.png) ### View Lineage in SaaS Anyone can use this link, and view the lineage in SaaS UI. ![Lineage View](images/lineageView.png) You can also view the lineage from the list of all exported lineage views available from "Collab" -> "Export Lineage" left-hand side navigation ![Lineage List](images/lineageList.png) /// admonition | In order to view the lineage in SaaS UI from the link, a user needs to be registered in the same Altimate AI instance type: info /// ## Recorded Demo /// admonition | This feature requires an API key. You can get it by signing up for free at [www.altimate.ai](https://www.altimate.ai) type: info /// ================================================ FILE: documentation/docs/govern/governance.md ================================================ Prevent issues from getting shipped to production! Project governance functionality lets you swiftly scan all dbt projects in your workspace against dbt best practices and organizational guidelines to quickly bring issues to your attention. ## Available via Extension & Python Package Project governance functionality is available in the power user extension and the open-source Python package. /// admonition | Also, the project governance functionality is available as a Python package, so the checks can be integrated in your Git, CI/CD workflows. More details [here](https://datapilot.readthedocs.io/en/latest/insights.html) type: tip /// ## Configure Checks You can configure different checks per your preferences in a local YAML file or the SaaS configuration stored in the Altimate AI backend. Navigate to the Action panel in the extension First, navigate to the Actions panel in the extension and go to "Project Governance" tab. Then, choose the dbt project for which you want to perform a governance check. ![ActionsPanel](images/actionsPanel.png)
### Manual Configuration of Checks The configuration of checks and the list of checks to be performed can be defined in a YAML file. More details on the configuration and YAML file [here](https://datapilot.readthedocs.io/en/latest/configuration.html#project-health-configuration) Select the YAML file by clicking on "Select Config Path" ![ConfigPath](images/configPath.png)
### SaaS Configuration of Checks /// admonition | SaaS configuration method allows you to share multiple configurations and reference those as per environments, teams, and projects. type: tip /// First, navigate to governance menu item on left hand side nav and choose "new dbt Governance Config". ![newCheckConfig](images/newCheckConfig.png)
In the governance config, a name and description should be provided. The owner field is automatically populated by the name of the current user. In the model section, you can define regex patterns for names to define different types of models. The default configuration is already pre-populated as: ``` staging: "^stg_.*" # Regex for staging models mart: "^(mrt_|mart_|fct_|dim_).*" # Regex for mart models intermediate: "^int_.*" # Regex for intermediate models base: "^base_.*" # Regex for base models ``` Below that section, there are different types of checks available. Few of the checks, require additional configuration. In that case, please click on the "configure" button next to the check to provide necessary configuration. You can search for specific checks and select / deselect checks that should be included in the config. This list also shows files required to perform the check. Checks require either a manifest file or catalog file or both files. You can filter checks based on the type of files required. Please hit the "save" button after you are done creating dbt Governance config on this screen. ![listChecks](images/listChecks.png)
Select the right config for checks from the list ![SaaSCheckConfig](images/saasCheckConfig.png)
## Do the Scan Click the start scan button. You may be asked to install Altimate AI DataPilot package. /// admonition | DataPilot python package is [open source package](https://github.com/AltimateAI/datapilot), that's actually used to do governance checks with the power user extension type: tip /// The scan usually runs in a few seconds, but depending on the size of your project and manifest / catalog files it may take longer. ![DataPilotPackage](images/DataPilotPackage.png)
## View Scan Results View the scan results right below the "Start Scan" button after the scan is finished. Scan results are organized by file type and check type and can be filtered on the same. You can clear results via a button or start the scan again. ![ScanResults](images/scanResults.png)
## List of Checks Checks between the Python package and Power User extension are the same. You can find a complete list of checks [here](https://datapilot.readthedocs.io/en/latest/insights.html) ## Recorded Demo ================================================ FILE: documentation/docs/govern/multiproject.md ================================================ # Multi Project Support with dbt-loom Power User extension now supports dbt-loom, where native features like query preview, compiled code, column lineage support multi-project references. ## Setup dbt-loom in your project - Instructions on how to [install and configure dbt-loom](https://github.com/nicholasyager/dbt-loom) - Sample project [example with dbt-loom configured](https://github.com/Bl3f/dbt-loom-example) /// admonition | Please make sure dbt_loom.config.yml file is in the root of your multi-project directory type: warning /// ## Reference models from other projects in code via auto-complete Use standard dbt-loom syntax to reference models from other projects. For example, ``` {{ ref('core', 'orders') }} ``` where 'orders' model from the 'core' project is referenced. ![Reference Loom](images/referenceLoom.png) ## Visualize Lineage with models from the other projects When you visualize the model or column lineage, models from other projects will be shown with the special "ext" type as below. ![Lineage Loom](images/lineageLoom.png) ## Limitations - Clicking on referenced model name in the code, and then opening that model file doesn't work for multi-project references. ## Recorded Demo
================================================ FILE: documentation/docs/govern/notebooks.md ================================================ **Why Notebooks?** Notebooks allow you to codify your usual ad-hoc data analysis, data preparation workflows, so they can be reused. For example, if you have a workflow for standard data analysis - check for duplicates, check timezone field, make sure only certain values are present for a "customer_type" column - you can create a notebook to codify these steps. Then, you can always trigger this notebook with a click of button for a table or data output of particular model to validate or clean the data. In these notebooks, you can create different 'cells' mapping to jinja-sql, python or markdown text in a single file, and you can execute these cells together. **Notebook templates** You can also utilize some out-of-the-box notebook templates for common tasks - e.g. profile the data results, create a config file for the project, create dbt staging models etc. **Notebook sharing** Many times, this ad-hoc work is dependent on organizational best practices and the type of data that your team works on. So, these notebooks can be reused by your team members as well as extended teams. That's why we have enabled sharing - where you can share notebooks with other users in your Altimate AI saas instance. This is especially useful when you have popular notebooks spread across multiple code repositories. /// admonition | We would love to hear your feedback on what additional functionality we can build in this area. If you have encountered any issues in this functionality, please message us over [chat](https://app.myaltimate.com/contactus) type: tip /// ## Enable notebooks in VSCode You just need to add the following line to the settings.json file in your .vscode directory at the root of your repo. ``` "dbt.enableNotebooks": true, ``` /// admonition | If you don't have the .vscode directory at the root of the repo, please create it type: info /// Then, reload or restart your VSCode. ## Create new notebook You can create a notebook by going to File -> New File and then choosing the option of DataPilot Notebook as shown below. ![Start Notebook](images/startNotebook.png)
After this, it will give you an option of creating a blank notebook or use one of the existing templates. For now, let's choose a "blank notebook". The first code cell will be automatically created for you. ![Cells Info](images/cellsInfo.png)
You can add additional code cells or markdown text cells. There are additional options available for the code cell to run cells, split, delete etc. as below ![Cell Actions](images/cellActions.png)
After you are done, you can just save the notebook file like any other file in VSCode. ## Use notebook templates There are a few ways in which you can trigger notebook templates. /// admonition | The notebook templates may use some standard Python packages, and VSCode may ask for your confirmation before installing the necessary packages type: info /// **There is a contextual notebook menu available at the top of the file when you open any SQL file in VSCode** ![Top Menu](images/topMenu.png)
After notebook is created based on the template, just click on "Run all" button on top to execute the notebook and get results. Following notebook templates are available today: ### [Template] Profile the query This template will have two cell blocks. First block will have SQL / jinja query and it will produce preview of data results. The second block will have python code for profiling the query. ### [Template] Get test suggestions This template will recommend tests based on the context of the dbt model, and generate code for those tests as well. ![testCode](images/testCode.png)
### [Template] Generate dbt base model SQL This template will generate base model SQL based on your input of source name and table information. ![baseModel](images/baseModel.png)
### [Template] Generate dbt model yaml This template creates config yaml for documentation and tests for the particular dbt model from where the action is triggered. ![configYaml](images/configYaml.png)
### [Template] Generate dbt model CTE This template generates model CTEs from dbt model file. **You can also create notebooks from the "Actions" tab in VSCode (present in the bottom panel)** ![actionsTab](images/actionsTab.png)
There are some notebook templates available as pre-configured notebooks. These templates are used at dbt Project level. ### [Template] Generate dbt source yaml Based on schema name specified in the code block, it can generate source yaml for you automatically. You need to provide schema name as an input in the code block. ![sourceYaml](images/sourceYaml.png)
### [Template] Extract exposures from Metabase This template helps extract exposures from Metabase ![expoMetabase](images/expoMetabase.png)
/// admonition | We would love to hear your feedback on what additional templates or functionality we can build in this area. If you have encountered any issues in this functionality, please message us over [chat](https://app.myaltimate.com/contactus) type: tip /// ## Share notebook Once you save the notebook, you can share it with other users in DataPilot instance. This enables you to share notebooks across repositories with the whole organization. ![shareNotebook](images/shareNotebook.png)
## Recorded demo
================================================ FILE: documentation/docs/govern/querybookmarks.md ================================================ # Query History and Bookmarks Now, you can see the history of all the SQL queries or dbt models you ran. So, you can re-run them to view and compare results to see how your data has changed between code changes. You can also look at the compiled SQL code of queries that were run previously. Queries from the query history can also be bookmarked and tagged. So, you can reuse those queries next time you need to do a similar ad-hoc analysis. Users can also share their query bookmarks with other users in your org-specific DataPilot SaaS instance. That way, you can have a shared repository of the most useful queries across the team. /// admonition | Your VSCode session stores only the last 10 queries in the query history, and this info is stored locally on your machine and not in the SaaS instance. type: info /// ## View Query History You can view query history in the "History" tab of the "Query History" panel. You can also search your query history for specific queries. ![Query History](images/queryHistory.png)
## Execute Query from History If you hover over the query from the history, you can perform a few actions, like executing the query (play icon) or bookmarking the query (bookmark icon). If you execute the query, the results are shown in the main code editor window as shown below: ![Execute History](images/executeHistory.png)
/// admonition | In this way, you can [compare query results](https://docs.myaltimate.com/test/queryResults/#compare-query-results) between different runs of your query type: info /// ## Add Bookmark If you execute some query frequently, you can also bookmark that query by clicking the bookmark button when you hover the query entry in the history. ![Bookmark Query](images/addBookmark.png)
/// admonition | You can add tags to your queries for better organization. You can also search your bookmarks by text or by tags. Many users use tags to categorize queries based on a specific purpose or a project. type: tip /// ## Share Bookmark You can share your bookmarks with other users in your Altimate AI instance so they can utilize those queries. When you share a bookmark, it's shared with all the users in "your" Altimate AI SaaS instance. ![Share Query](images/shareBookmark.png)
## View Code Query or bookmark code can be easily viewed by clicking on the query. Also, this code can be opened in the main code editor window by clicking on the icon as shown below: ![View Code](images/bookmarkCode.png)
## Recorded Demo /// admonition | This feature requires an API key. You can get it by signing up for free at [www.altimate.ai](https://www.altimate.ai) type: info /// ================================================ FILE: documentation/docs/index.md ================================================ # Accelerate dbt and SQL Development by 3x Welcome to the docs for [dbt Power User VSCode Extension](https://marketplace.visualstudio.com/items?itemName=innoverio.vscode-dbt-power-user), an [open sourced](https://github.com/AltimateAI/vscode-dbt-power-user) extension created by [Altimate AI](https://www.altimate.ai/). It offers various features across three important phases of dbt and SQL work - develop, test, and collaborate.
Accelerate dbt and SQL Development by 3x
## Datamates with AI Teammates The Power User extension is part of the Datamates Platform. Datamates Platform offers the functionality to automate and accelerate the work of data teams in various areas of platform engineering, data engineering, and analytics engineering. Datamates Platform consists of many AI teammates to help data teams with their work. For example, there is a documentation generator teammate that's specifically developed to handle data documentation work. These AI teammates are available as part of the power user extension to offer the functionality ranging from dbt models, docs, tests generation to SQL translation & explanation. AI teammates can be coached by you and personalized for your specific requirements. To learn more - please check this section on [coaching AI teammates](./teammates/coach.md).
## Feature Comparison The dbt Power User Extension has great features out of the box. Add a free [Altimate api key](setup/reqdConfig.md#enable-saas-features-by-adding-api-key) to unlock all the features in "With Altimate AI Key" below. ## Support Power user extension and Datamates Platform is developed and maintained by [Altimate AI team](https://www.altimate.ai). Please join the dbt Community Slack Channel [#tools-dbt-power-user](https://getdbt.slack.com/archives/C05KPDGRMDW) to meet with the community of users of the extension. If you run into issues, please [contact us](https://www.altimate.ai/support) via Slack or chat ================================================ FILE: documentation/docs/javascripts/feedback,js ================================================ var feedback = document.forms.feedback feedback.addEventListener("submit", function(ev) { ev.preventDefault() /* Retrieve page and feedback value */ var page = document.location.pathname var data = ev.submitter.getAttribute("data-md-value") /* Send feedback value */ console.log(page, data) }) ================================================ FILE: documentation/docs/overrides/.gitkeep ================================================ ================================================ FILE: documentation/docs/overrides/main.html ================================================ {% extends "base.html" %} {% block announce %} New! Altimate Code — #1 on ADE-Bench. 100+ deterministic tools for dbt & SQL. 10M tokens of Sonnet, Opus & GPT-5 — free! Get Started → {% endblock %} {% block extrahead %} {% endblock %} ================================================ FILE: documentation/docs/setup/configuration.md ================================================ # Configuration Settings This page provides a comprehensive overview of all available configuration settings in the dbt Power User extension. The settings are organized by category for easy reference. ## Core Settings ### dbt Integration Settings #### dbt.dbtIntegration - **Type**: string - **Default**: "core" - **Options**: ["core", "cloud", "fusion", "corecommand"] - **Description**: Choose how you want to integrate with dbt. Use "core" for local dbt installations with Python bridge, "cloud" for dbt Cloud integration, "fusion" for dbt Fusion CLI integration, or "corecommand" for command-line based dbt core integration. This setting determines how the extension interacts with your dbt environment. #### dbt.dbtPythonPathOverride - **Type**: string - **Description**: Specify a custom path to your Python executable or entrypoint. This is useful when you need to use a specific Python environment different from the VS Code Python extension's default. Most users should leave this empty and configure their Python environment through the VS Code Python extension instead. #### dbt.dbtCustomRunnerImport - **Type**: string - **Default**: "from dbt.cli.main import dbtRunner" - **Description**: Customize the Python import statement used to import the dbt runner. This is only applicable when using dbt core integration. Useful when you have a custom dbt implementation or need to use a specific dbt runner class. #### dbt.installDepsOnProjectInitialization - **Type**: boolean - **Default**: true - **Description**: Controls whether the extension automatically runs dbt deps when initializing a project. When enabled, the extension will automatically install all package dependencies specified in your packages.yml file when you first open a dbt project. ## Project Configuration #### dbt.allowListFolders - **Type**: array of strings - **Default**: [] - **Description**: Specify which folders in your workspace should be considered for dbt operations. This is particularly useful in monorepos or workspaces with multiple dbt projects. Paths should be relative to the workspace root. #### dbt.deferConfigPerProject - **Type**: object - **Description**: Configure how the extension handles model dependencies across environments. - **Properties**: - `deferToProduction`: boolean - When true, allows running models without rebuilding parent models - `manifestPathForDeferral`: string - Path to the manifest file containing parent model information - `favorState`: boolean - When true, uses the deferred state even if the model exists in both environments ## Command Settings #### dbt.runModelCommandAdditionalParams - **Type**: array of strings - **Default**: [] - **Description**: Add extra command-line parameters to all dbt run commands. Each parameter must be a separate array entry. #### dbt.buildModelCommandAdditionalParams - **Type**: array of strings - **Default**: [] - **Description**: Add extra command-line parameters to all dbt build commands. Similar to runModelCommandAdditionalParams, but specifically for build commands. ## Query Settings #### dbt.queryLimit - **Type**: integer - **Default**: 500 - **Minimum**: 1 - **Description**: Controls the maximum number of rows returned when using the Preview SQL Query command. #### dbt.queryTemplate - **Type**: string - **Default**: "select from ({query}\n) as query limit {limit}" - **Description**: Customize how preview queries are constructed. The template must include {query} and {limit} placeholders. ## UI/Display Settings #### dbt.perspectiveTheme - **Type**: string - **Default**: "Vintage" - **Options**: ["Vintage", "Pro Light", "Pro Dark", "Vaporwave", "Solarized", "Solarized Dark", "Monokai"] - **Description**: Choose the visual theme for the query results viewer. ## Model Generation Settings #### dbt.fileNameTemplateGenerateModel - **Type**: string - **Default**: "{prefix}{sourceName}{tableName}" - **Options**: - "{prefix}{sourceName}{tableName}" - "{prefix}{tableName}" - "{tableName}" - **Description**: Define how new model filenames are generated when creating models from sources. #### dbt.prefixGenerateModel - **Type**: string - **Default**: "base" - **Description**: Set the default prefix used when generating new models from sources. ## SQL Formatting #### dbt.sqlFmtPath - **Type**: string - **Description**: Specify the path to your SQL formatter executable (sqlfmt). #### dbt.sqlFmtAdditionalParams - **Type**: array of strings - **Default**: [] - **Description**: Add extra parameters to the SQL formatting command. ## Altimate AI Integration #### dbt.altimateAiKey - **Type**: string - **DisplayName**: "Altimate AI API Key" - **Description**: Required for features that need backend support. Sign up for a free Altimate AI account at app.myaltimate.com/register. #### dbt.altimateInstanceName - **Type**: string - **DisplayName**: "Altimate AI Instance Name" - **Description**: The instance name for your Altimate AI account. ## Feature Toggles #### dbt.enableNewLineagePanel - **Type**: boolean - **Description**: Enable or disable the new lineage panel in dbt. #### dbt.enableCollaboration - **Type**: boolean - **Default**: true - **Description**: Enable or disable the documentation collaboration features. #### dbt.disableQueryHistory - **Type**: boolean - **Default**: false - **Description**: Control whether the extension keeps track of your query history and bookmarks. #### dbt.enableNotebooks - **Type**: boolean - **Default**: false - **Description**: Enable or disable the Datapilot notebooks feature. ## Lineage Settings #### dbt.lineage.showSelectEdges - **Type**: boolean - **Default**: true - **Description**: Control the visibility of SELECT statement relationships in the lineage graph. #### dbt.lineage.showNonSelectEdges - **Type**: boolean - **Default**: false - **Description**: Control the visibility of non-SELECT relationships in the lineage graph. #### dbt.lineage.defaultExpansion - **Type**: number - **Default**: 1 - **Description**: Set how many levels of relationships are automatically expanded when viewing the lineage graph. ## Other Settings #### dbt.unquotedCaseInsensitiveIdentifierRegex - **Type**: string - **Description**: Define a regular expression pattern to identify unquoted identifiers in your SQL code. #### dbt.conversationsPollingInterval - **Type**: integer - **Default**: 900 - **Minimum**: 30 - **Description**: Set how frequently (in seconds) the extension checks for new comments and conversations in documentation. ================================================ FILE: documentation/docs/setup/cursor_installation_workaround.md ================================================ # Cursor IDE: Installation Workaround /// admonition | Known Cursor Issue type: warning Some Cursor IDE users experience the dbt Power User extension installation freezing for several minutes, followed by a **"Failed to fetch"** error dialog. This is a **known issue on Cursor's side** and has been reported and acknowledged by the Cursor team. See the [Cursor forum thread](https://forum.cursor.com/t/dbt-power-user-extension-installation-freezes-with-repeated-extensionquery-requests-and-failed-to-fetch-error/149385/4) for details. Until the Cursor team resolves this, please try the workarounds below. /// ## Problem When installing the dbt Power User extension in Cursor IDE: 1. The installation **freezes for several minutes** with repeated `extensionQuery` requests. 2. An error dialog appears with a **"Failed to fetch"** message. 3. The extension fails to install through the normal marketplace flow. --- ## Workaround 1: Register extension metadata and reinstall This workaround tricks Cursor into recognizing the extension entry, then lets you cleanly reinstall it. **Step 1.** Open the Cursor extensions metadata file at: | Platform | Path | |----------|------| | macOS / Linux | `~/.cursor/extensions/extensions.json` | | Windows | `%USERPROFILE%\.cursor\extensions\extensions.json` | **Step 2.** Add the following JSON object to the array in that file: /// admonition | Update the paths type: tip Replace `` with your actual home directory path (e.g. `/Users/jane`, `/home/jane`, or `C:/Users/jane`). /// ```json { "identifier": { "id": "innoverio.vscode-dbt-power-user" }, "version": "0.59.1", "location": { "$mid": 1, "fsPath": "/.cursor/extensions/innoverio.vscode-dbt-power-user-0.59.1-universal", "path": "/.cursor/extensions/innoverio.vscode-dbt-power-user-0.59.1-universal", "scheme": "file" }, "relativeLocation": "innoverio.vscode-dbt-power-user-0.59.1-universal", "metadata": { "installedTimestamp": 1768889206275, "pinned": false, "source": "gallery", "id": "4e4743d1-e423-44b9-88a4-d639da0996d1", "publisherId": "4a9e9d34-3546-42a6-971d-5a640a726245", "publisherDisplayName": "Altimate Inc.", "targetPlatform": "universal", "updated": false, "private": false, "isPreReleaseVersion": false, "hasPreReleaseVersion": false } } ``` /// admonition | Validate your JSON type: info If `extensions.json` already has other entries, make sure to add a comma separator between objects so the JSON remains valid. /// **Step 3.** Save the file. **Step 4.** The extension will now appear as installed in Cursor, but it is not actually installed yet — this is just a metadata entry. ![Extension shows as installed after metadata change](images/cursor_workaround_step_1.png) **Step 5.** Reload the Cursor window: - Press `Cmd + Shift + P` (macOS) or `Ctrl + Shift + P` (Windows/Linux) - Type **"Developer: Reload Window"** and press Enter **Step 6.** Open the dbt Power User extension page. You will see an error that the extension files don't exist — this is expected: ![Extension page showing error](images/cursor_workaround_step_2.png) ``` Unable to read file '.../.cursor/extensions/innoverio.vscode-dbt-power-user-0.59.1-universal/package.json' ``` ![Package.json not found error](images/cursor_workaround_step_3.png) **Step 7.** **Uninstall** the extension, then **reinstall** it. You can choose any version you want to install. The installation should now complete successfully. --- ## Workaround 2: Manual VSIX installation If Workaround 1 does not resolve the issue, you can manually install the extension using a `.vsix` file. **Step 1.** Download the `.vsix` file from the [Open VSX Registry](https://open-vsx.org/extension/innoverio/vscode-dbt-power-user): - Go to the extension page and click **Download** to get the `.vsix` file. ![Download VSIX from Open VSX](images/cursor_workaround_manual.png) **Step 2.** Install the `.vsix` file in Cursor: - Open the **Extensions** panel in Cursor - Drag and drop the downloaded `.vsix` file into the Extensions panel The extension will install directly from the file, bypassing the marketplace fetch entirely. --- /// admonition | Still having issues? type: tip If neither workaround resolves the problem, please reach out on the [#tools-dbt-power-user](https://getdbt.slack.com/archives/C05KPDGRMDW) Slack channel or [contact support](https://www.altimate.ai/support). /// ================================================ FILE: documentation/docs/setup/faq.md ================================================ ## Frequently Asked Questions - **Why am I receiving a "Network Error" message when trying to log into my Altimate AI account on the Sign In page?** When logging into your Altimate AI account on the Sign In page, if you encounter a "Network Error" message, it's likely due to the use of the general URL: https://app.myaltimate.com/login. At Altimate AI, we assign a unique URL to each tenant as part of our commitment to ensuring data isolation and providing a seamless user experience. Therefore, to access your account without any issues, it's crucial to use the specific URL that was uniquely created for you at the time of your account registration. - **How to Customize the Target Profile for VS Code dbt Power User Extension Functions?** To customize the target profile in VS Code for dbt Power User extension functions, follow these steps: Open Your dbt Project: Ensure you have your dbt project open in VS Code where the dbt Power User extension is installed. Locate the profiles.yml File: This file contains the configuration for your different dbt profiles. By default, the extension uses the 'dev' profile. Edit the Profile: In the profiles.yml file, you can define multiple profiles. Each profile can have different settings for your database connection and other preferences. Set Your Desired Profile: To change the active profile, you can use the dbt CLI command `dbt --profile your_profile_name debug` in the integrated terminal of VS Code. Alternatively, some extensions allow profile selection through the extension settings. Check if dbt Power User has such an option by going to the extension settings in VS Code. Restart VS Code: After making changes, restart VS Code to ensure that the new profile settings are loaded. Verify the Change: Run a command like "compiled dbt preview" or "query preview" to verify that the extension is now using your specified profile. ================================================ FILE: documentation/docs/setup/installation.md ================================================ /// admonition | We've released our [Datamates Extension](https://marketplace.visualstudio.com/items?itemName=altimateai.vscode-altimate-mcp-server) - to give you the power to use AI TeamMates! [Click here](https://datamates-docs.myaltimate.com/) to learn more. /// There are a few different ways in which extension can be installed. You can install it natively or in a dev container. ## Install the extension natively You can install the extension from VSCode directly or from the [VSCode Marketplace](https://marketplace.visualstudio.com/items?itemName=innoverio.vscode-dbt-power-user) You can install the extension from Cursor or any other VSCode compatible editors directly or from the [Open VSX Registry](https://open-vsx.org/extension/innoverio/vscode-dbt-power-user) /// admonition | Cursor IDE users: Installation may freeze type: warning Some Cursor users experience the extension installation freezing for several minutes and showing a "Failed to fetch" error. This is a known Cursor issue. If you encounter this, please follow the [Cursor installation workaround](cursor_installation_workaround.md) to resolve it. /// /// details | Here's how to install the extension in VSCode
/// /// admonition | Need to setup environment variables? Refer to this [section](https://docs.myaltimate.com/setup/optConfig/#environment-variables-setup) type: warning /// /// admonition | If you are seeing the message "Reload required", please reload the VSCode or restart the VSCode. type: info /// ## Install the extension in a dev container (or in codespaces) You need to do the below steps only if you need to setup the extension in a devcontainer (or GitHub Codespaces) Please add the following configuration in to your devcontainer.json file: ``` "customizations": { "vscode": { ... "files.associations": { "*.yaml": "jinja-yaml", "*.yml": "jinja-yaml", "*.sql": "jinja-sql", "*.md": "jinja-md" }, ... }, "extensions": [ ... "innoverio.vscode-dbt-power-user", ... ] } } ``` /// admonition | Please do NOT forget to do required configuration based on your dbt setup: [dbt Core](reqdConfig.md), [dbt Cloud](reqdConfigCloud.md), or [dbt Fusion](reqdConfigFusion.md), and [optional configuration](optConfig.md)!! type: warning /// ================================================ FILE: documentation/docs/setup/optConfig.md ================================================ ## Environment Variables Setup There are multiple sources from where the extension reads the environment variables. - [Using vscode Integrated terminal Profiles](#environment-variables-setup-using-the-integrated-terminal-profiles-vscodesettingsjson) - [Variables set outside of Visual Code in .zshrc or .bashrc](#environment-variables-set-outside-of-visual-code-zshrc-bashrc) - [dot env file (we get this from python extension)](#environment-variables-through-pythonenvfile) /// admonition | Environment variable available in vscode terminal doesn't necessarily will be available to the extension. Extension can only read environment variables if available in the above sources type: warning /// ### Environment variables set outside of Visual Code (.zshrc, .bashrc, ...) If you have variables set in .zshrc or .bashrc the extension automatically picks it up. These environment variables will be passed to all operations of the extension. If you make changes to the environment variables you need to restart vscode. /// admonition | The environment variable should be valid for all your dbt projects. For example DBT_PROFILES_DIR can be set to ., that way dbt will lookup the profiles.yaml file inside the root of the dbt project. type: info /// ### Environment variables setup using the integrated Terminal Profiles ( .vscode/settings.json ) The extension will read any VSCode configurations in .vscode/settings.json and pass them to all operations of the extension: OSX: ``` "terminal.integrated.env.osx": { "ENV_VARIABLE_NAME": "VALUE" } ``` Windows: ``` "terminal.integrated.env.windows": { "ENV_VARIABLE_NAME": "VALUE" } ``` Linux: ``` "terminal.integrated.env.linux": { "ENV_VARIABLE_NAME": "VALUE" } ``` The official documentation can be found [here](https://code.visualstudio.com/docs/terminal/profiles) /// admonition | Visual Code variable substitution is not supported except the environment variable pattern ${env:*} and ${workspaceFolder}. type: warning /// ### Environment variables through python.envFile The extension also loads an environment variable definitions file identified by the python.envFile setting. The default value of this setting is `${workspaceFolder}/.env`. This way supports all Visual Code variable substitution patterns but the environment variables will not be available to the vscode terminal. Read all about [environment variables](https://code.visualstudio.com/docs/python/environments#_environment-variables) supported by the Visual Code Python extension. /// admonition | Make sure the .env file is in the [right format](https://www.dotenv.org/docs/security/env) or else the extension won't be able to detected the variables. type: warning /// ### Listing Environment Variables detected by the extension We have a debugging utility available within the extension that lists down the environment variables available to the extension and the source from where it is read. Here is how you can trigger the debugging utility 1. Cmd +Shift + P (Ctrl + Shift + P in case of windows) to start the VSCode command bar 2. Select the option dbt Power User: Print environment variable ![Debug Command](images/debugCmd.png) 3. It should print all the environment variables detected and the sources as well ![Sample Output](images/listEnvVariables.png) /// admonition | In certain scenarios, step 3 has to be executed twice to get the environment variables printed. type: warning /// ### Default DBT Env Support The following env variables when set are automatically picked by the extension DBT_PROFILES_DIR : This is used to override the folder where profiles file is located DBT_TARGET_PATH : This is for overriding the dbt target path ## Custom dbt Runner The dbt.dbtCustomRunnerImport configuration can be used to plug your own dbt runner. This dbt runner has to invoke dbt but can perform authentication or additional configuration steps before invoking dbt or your own custom dbt executable. Code can be imported from the selected Python environment, so just install your own library or inline the code in the configuration. Note that the Python code should be written in one line. For example let's add this custom dbt runner in your project root folder, called `my_custom_runner.py` . In this case we inherit from the existing dbtRunner from dbt, but that's optional, as long as the interface is compatible with dbt, it should work as expected. ```python from typing import List from dbt.cli.main import dbtRunner, dbtRunnerResult class MyCustomRunner(dbtRunner): def invoke(self, args: List[str], **kwargs) -> dbtRunnerResult: print("Here you can perform setup or authentication or modify args") super().invoke(args, **kwargs) ### patch the dbtRunner dbtRunner = MyCustomRunner ``` Now in the vscode config json add an entry for `"dbt.dbtCustomRunnerImport”` . ```json { "dbt.dbtCustomRunnerImport”: “from my_custom_runner import dbtRunner” } ``` Note that the custom dbtRunner is only used for dbt command invocations. Some operations like metadata fetching, query execution will directly use dbt api and will therefore not use the instructions set up in the custom dbt runner. For more information you can check out these resources: - [Programmatic Invocation Support in dbt](https://docs.getdbt.com/reference/programmatic-invocations) - [dbt Runner Implementation in dbt core](https://github.com/dbt-labs/dbt-core/blob/ddd6506beafd410847d0702267c1e87150377668/core/dbt/cli/main.py#L43) ## [Deprecated] MSSQL, Synapse, Oracle - Query Preview Config /// admonition | It is no longer required to specify query template for any of the adapter. type: warning /// Your database may not support standard SQL LIMIT statements like `SELECT * from table LIMIT 10`. You can override this default behaviour through `dbt.queryTemplate`. Please make a PR in the [dbt-power-user repo](https://github.com/AltimateAI/vscode-dbt-power-user) if you find that you need to change `dbt.queryTemplate` for your favourite adapter and help the community. ### `dbt.queryTemplate` for Oracle Change to `select * from ({query})\n where ROWNUM <= {limit}` ### `dbt.queryTemplate` for MS SQL, Synapse Change to `set rowcount {limit}; {query}` or `{query}\n order by 1 OFFSET 0 ROWS FETCH FIRST {limit} ROWS ONLY` (note that your query can't have an order by clause). ## Enable color highlighting of terminal output on Windows These instructions are for setting the TERM environment variable to xterm-256color in a VSCode terminal instance. This configuration instructs the terminal to operate in a mode that supports 256 colors and various text formatting options, enhancing the display of colored output. So, the relevant dbt command outputs e.g job status "Success" or "Failure" are shown in the right color. ``` "terminal.integrated.env.windows": { "TERM": "xterm-256color" } ``` /// admonition | Above instructions for color highlighting are for Windows machines only type: warning /// ## Skip Project To specify project folders explicitly and control which projects are included in the build process, you can configure the `dbt.allowListFolders` setting. This can be particularly useful when you have a large number of projects in the same workspace. You can define workspace-relative paths to include as follows: ```json "dbt.allowListFolders": [ "folder1", "folder2" ] ``` ## Configure Source to Model file name template You can configure the file name template for source to model files. To do so, go to the extension setting and choose the template you want to use and specify the prefix. ![Template for source to model generation](images/source-to-model-template.png) ## Configure sqlfmt sqlfmt is a SQL formatter that you can use with dbt-power-user VSCode extension so your dbt models are always correctly formatted. ### Install `sqlfmt` We recommend installing sqlfmt with the jinjafmt extra (which will also install the Python code formatter, Black). Install sqlfmt by running, ``` pipx install 'shandy-sqlfmt[jinjafmt]' ``` If you don't have 'pipx' installed, please install it using instructions [here](https://github.com/pypa/pipx). Find more about sqlfmt in their [docs](https://docs.sqlfmt.com/getting-started/installation). ### Configure `dbt.sqlFmtPath` or `dbt.sqlFmtAdditionalParams` You can configure the path to sqlfmt through `dbt.sqlFmtPath` and you can configure additional parameters through `dbt.sqlFmtAdditionalParams`. ### Usage Please select "dbt Power User" (extension id:`innoverio.vscode-dbt-power-user`) as the default formatter. You can do this either by using the context menu (right click on a open dbt model in the editor) and select "Format Document With...", or you can add the following to your settings: ```json "[jinja-sql]": { "editor.defaultFormatter": "innoverio.vscode-dbt-power-user" } ``` ### Format on save You can enable format on save for python by having the following values in your settings: ```json "[jinja-sql]": { "editor.defaultFormatter": "innoverio.vscode-dbt-power-user", "editor.formatOnSave": true } ``` ## Setup Altimate API credentials using Environment variables You can now setup the Altimate Instance Name and Key using environment variables add following entries to .env file ```bash ALTIMATE_KEY= ALTIMATE_INSTANCE_NAME= ``` Now, the vscode settings.json to include ``` { "dbt.altimateAiKey": "${env:ALTIMATE_KEY}", "dbt.altimateInstanceName": "${env:ALTIMATE_INSTANCE_NAME}" } ``` This setting is available in v0.37.9 and onwards. ## Column name setup for YAML file updates DataPilot can update your YAML files when documentation or tests are added or edited. Different databases use different column casing conventions. For example, Snowflake always uses upper-case column names, whereas Postgres uses lower-case column names by default. These database-specific standards may create issues in DataPilot behavior without the correct configuration. These issues may include - - The documentation editor may save column names in incorrect casing in YAML files. - Operations like "bulk generate" documentation for missing columns may generate documentation for existing columns. If you are facing these issues, you can adjust the following settings: - New column names are always saved in lowercase in YAML files, except for columns with [quoted identifiers](https://docs.getdbt.com/reference/resource-properties/quote), regardless of the database. (dbt handles database-specific casing automatically this way) If you want to save the column name as it is in the database, set `dbt.showColumnNamesInLowercase`=false in .vscode/settings.json - If you update documentation or tests for the existing columns, we will not change the casing for column names that are already present in YAML files If there are columns with [quoted identifiers](https://docs.getdbt.com/reference/resource-properties/quote), we don't change the casing of such columns; we add the 'quote: true' attribute for those columns. You can also set a regex string to find unquoted identifiers by adding `dbt.unquotedCaseInsensitiveIdentifierRegex`= 'your_regex' in .vscode/settings.json or change it in VSCode extension settings. ## Disable setup walkthrough pop-up To disable the popup that prompts for extension to be setup when loaded for the first time set the setting dbt.hideWalkthrough to true in vscode's settings.json ``` { "dbt.hideWalkthrough": true } ``` ================================================ FILE: documentation/docs/setup/reqdConfig.md ================================================ /// admonition | Only use the following steps for "dbt Core" environments. If you have a dbt Cloud environment, use the [required config instructions for "dbt Cloud" environments](./reqdConfigCloud.md). If you have a dbt Fusion environment, use the [required config instructions for "dbt Fusion" environments](./reqdConfigFusion.md). type: warning /// ## Use the setup wizard for configuration (recommended) /// admonition | Need to setup environment variables? Refer to this [section](https://docs.myaltimate.com/setup/optConfig/#environment-variables-setup) type: warning /// This method will save a bunch of time for you, and you can also validate your configuration. Setup wizard will help you in associating sql files with jinja-sql, selecting the right Python interpreter, make sure dbt dependencies are correctly installed etc. In the end, it will also validate your configuration. You can start the setup wizard by clicking on dbt status icon in bottom status bar, and perform following necessary steps as shown in click-through demo below:
**Here are steps covered in the setup wizard** **Select Python Interpreter** Click on the action button - "Select Python Interpreter" and choose your preferred python interpreter. Usually, choosing interpreter that's recommended, or mapped to your virtual environment software (e.g. venv) as per the list is a good idea. If you know the path of your Python environment, you can choose it from the list or if the path is not present there, you can enter it manually. /// admonition | If needed, please run 'where python' command on terminal to see if it shows path to Python interpreter that you are using. type: tip /// **Install dbt** If dbt is not installed in your environment (dbt status icon on bottom status bar will show it), Click on "Install dbt" button in the next step. It will ask for dbt version and adapter that's need for your database environment (e.g. Snowflake). Based on these inputs, setup wizard will automatically install dbt in your environment **Install dbt deps** Many times project failures or weird errors are seen if dbt dependencies are not installed. In this step, once you click on 'Run dbt deps' button, setup wizard will automatically run dbt deps command for your project. **Validate Project** Last step is clicking on button - "Validate Project" It will run a bunch of checks to make sure your dbt environment and project are setup correctly. If there are some issues, it will tell you exactly what's wrong as well. /// admonition | If you still can't get the extension setup correctly, please contact us via slack or chat through [support page](https://www.altimate.ai/support) type: tip /// ## Manual method of configuration /// admonition | Please follow the manual method only if you couldn't use the setup wizard above. type: info /// ### Associate \*.sql files with jinja-sql There are two different methods to do this. Please follow only one method: #### Method 1: Configure in Preferences > Settings in the extension ![File Associations](images/associations.png) #### Method 2: Update the settings.json file directly /// details | Type 'settings.json' in the VS Code command pallet to open it ![Open Settings.json](images/settingsTrigger.png) /// and add following lines at the end of settings.json ``` "files.associations": { "*.sql": "jinja-sql", "*.yml": "jinja-yaml" }, ``` ### Associate Python Interpreter with dbt installation Ensure that the Python interpreter selection is always visible on the bottom strip of the VS Code for ease of use: /// details | Enable python interpreter visibility by updating VSCode settings ![Enable Python interpreter visibility](images/interpreterVisibility.gif) /// Select the Python interpreter that has dbt installed. ![Select Python interpreter](images/selectInterpreter.gif) /// admonition | Tip type: info If you select a python environment with dbt already installed, the dbt label on the bottom strip of the VS Code will show a checkmark. /// /// details | If dbt is shown as not installed in the extension, the extension can install dbt for you automatically - just click on the dbt status icon on the bottom strip of the VSCode. type: tip
/// /// admonition | Warning for Python path overrides type: warning Avoid using the setting dbt.dbtPythonPathOverride unless using Meltano, the extension depends on the Python interpreter for visual code compatible environment variable parsing. /// ## Enable SaaS features by adding API key There are multiple features in the extension, including [generate dbt documentation](../document/generatedoc.md), [column lineage](../test/lineage.md), [query explanation](../develop/explanation.md), [generate dbt model from SQL](../develop/genmodelSQL.md) that require an API key. /// details | You can get an API key for free by signing up at [www.altimate.ai](https://www.altimate.ai)
/// You need to add the API key from "Settings->API key" in your Altimate instance to the VSCode extension settings. You also need to add "Instance name" in the extension settings. Please get your instance name from your Altimate AI URL. If your URL for Altimate instance is - "companyx.app.myaltimate.com", then instance name is "companyx". Go to VSCode extension settings, and add API key and instance name there. /// details | Here's demo of how to add instance name and API Key to the extension settings
/// ================================================ FILE: documentation/docs/setup/reqdConfigCloud.md ================================================ /// admonition | Only use the following steps for "dbt Cloud" environments. If you have a dbt Core environment, use the [required config instructions for "dbt Core" environments](./reqdConfig.md). If you have a dbt Fusion environment, use the [required config instructions for "dbt Fusion" environments](./reqdConfigFusion.md). type: warning /// /// admonition | dbt Cloud integration is available as beta functionality type: tip /// ## Enable dbt Cloud Integration by adding an API key dbt Cloud integration in Power User VSCode extension requires an API key. There are also multiple preview features in the extension including [generate dbt documentation](../document/generatedoc.md), [column lineage](../test/lineage.md), [query explanation](../develop/explanation.md), [generate dbt model from SQL](../develop/genmodelSQL.md) that are also enabled with an API key. /// details | You can get an API key for free by signing up at [www.altimate.ai](https://www.altimate.ai)
/// You need to add the API key from "Settings->API key" in your Altimate instance to the VSCode extension settings. You also need to add "Instance name" in the extension settings. Please get your instance name from your Altimate AI URL. If your URL for Altimate instance is - "companyx.app.myaltimate.com", then instance name is "companyx". Go to the VSCode extension settings, and then add an API key and instance name. /// details | Here's a demo of how to add an instance name and an API Key to the extension settings
/// ## Use the setup wizard for configuration (recommended) /// admonition | Need to setup environment variables? Refer to this [section](https://docs.myaltimate.com/setup/optConfig/#environment-variables-setup) type: warning /// This method will save a bunch of time for you, and you can also validate your configuration. Setup wizard will help you in associating sql files with jinja-sql, selecting the right Python interpreter, make sure dbt dependencies are correctly installed etc. In the end, it will also validate your configuration. You can start the setup wizard by clicking on dbt status icon in bottom status bar, and performing following necessary steps as shown in the recorded demo below:
**Here are the steps covered in the setup wizard** **Select Python Interpreter** Click on the action button - "Select Python Interpreter" and choose your preferred python interpreter. Usually, choosing interpreter that's recommended, or mapped to your virtual environment software (e.g. venv) as per the list is a good idea. If you know the path of your Python envionment, you can choose it from the list or if the path is not present there, you can enter it manually. /// admonition | If needed, please run 'where python' command on terminal to see if it shows path to Python interpreter that you are using. type: tip /// **Install dbt** If dbt is not installed in your environment (dbt status icon on bottom status bar will show it), Click on "Install dbt Cloud" button in the next step. This will install latest version of dbt Cloud CLI in your environment. **Validate Project** Last step is clicking on button - "Validate Project" It will run a bunch of checks to make sure your dbt environment and project are setup correctly. If there are some issues, it will tell you exactly what's wrong as well. /// admonition | If you still can't get the extension setup correctly, please check the [troubleshooting page](../troubleshooting.md) type: tip /// ## Questions and Answers #### Is dbt Cloud integration free? Answer: Yes, integration with dbt Cloud is free and treated the same as integration with dbt Core. It will not count towards the usage quota. ![Image](images/pricing_clarifications.png) #### Why do I need to add the Altimate API key? The API key is necessary for authentication with our backend. VSCode supports login-based authentication, but it often logs out between sessions, which can disrupt the workflow. The API key provides a more stable and streamlined experience. This is particularly beneficial for large teams, allowing them to integrate the key into their deployment secrets when setting up VSCode as a remote environment. In the future, integration with [Cloud Service token](https://docs.getdbt.com/docs/dbt-cloud-apis/authentication)s might be necessary for deeper cloud interactions, thus having the Altimate integration in place from the start makes sense. #### What benefits does registering an API key provide? A direct line of communication with our users is established with the authentication in place. This is essential for efficiently communicating hotfixes, new releases, and deprecation warnings. It helps to minimize operational challenges and ensures that users are not left with outdated versions or unaware of updates due to the limitations of VSCode or lack of IDE restarts. Our main goal is to prevent any disruption in your development environment and to support our users proactively. #### What if I don't want to use preview features or accidentally send data to Altimate? We understand the concern about using preview features and the risk of accidental data transmission. To address this, we have implemented stringent data security practices, which you can review in our [security FAQ](https://docs.myaltimate.com/arch/faq/). Our solutions have passed security reviews by several large organizations in the US, and we are open to undergoing similar reviews for your organization. Additionally, we are working on making some preview features available offline through our [open-source Python CLI package](https://github.com/AltimateAI/datapilot-cli). #### How are you addressing concerns about data transmission in preview features? To directly address concerns about data transmission, we have added a "local-mode-only" setting in VSCode. If enabled, this setting prevents backend calls for any feature except authentication. This setting can be reviewed by your security team since our [client code](https://github.com/AltimateAI/vscode-dbt-power-user/blob/master/src/altimate.ts) is open-source. Add the following setting in vscode settings.json ```json { dbt.isLocalMode: True } ``` ================================================ FILE: documentation/docs/setup/reqdConfigFusion.md ================================================ /// admonition | Only use the following steps for "dbt Fusion" environments. If you have a dbt Core environment, use the [required config instructions for "dbt Core" environments](./reqdConfig.md). If you have a dbt Cloud environment, use the [required config instructions for "dbt Cloud" environments](./reqdConfigCloud.md). type: warning /// /// admonition | dbt Fusion integration provides enhanced performance and features type: tip /// ## What is dbt Fusion? dbt Fusion is a command-line interface that provides enhanced dbt functionality with improved performance and additional features. Unlike standard dbt Core, dbt Fusion is a standalone executable that doesn't require a Python environment, making it easier to install and manage. ### Key Benefits of dbt Fusion Integration: - **Standalone Installation**: No Python environment required - **Enhanced Performance**: Optimized execution compared to standard dbt - **Cross-Platform Support**: Available for macOS, Linux, and Windows - **Simple Setup**: Single executable installation - **Full VSCode Integration**: Complete feature support in the extension ## Use the setup wizard for configuration (recommended) /// admonition | Need to setup environment variables? Refer to this [section](https://docs.myaltimate.com/setup/optConfig/#environment-variables-setup) type: warning /// This method will save a bunch of time for you, and you can also validate your configuration. The setup wizard will help you in associating SQL files with jinja-sql, installing dbt Fusion if needed, and validating your project configuration. You can start the setup wizard by clicking on the dbt status icon in the bottom status bar, and perform the following necessary steps:
**Here are the steps covered in the setup wizard** **Select dbt Integration Type** In the setup wizard, choose "dbt Fusion" as your integration type. This will configure the extension to use the dbt Fusion CLI for all dbt operations. **Install dbt Fusion** If dbt Fusion is not installed in your system (the dbt status icon on the bottom status bar will show it), click on "Install dbt Fusion" button. The setup wizard will automatically install the latest version of dbt Fusion using the appropriate method for your operating system: - **macOS/Linux**: Uses curl to download and install from dbt Labs CDN - **Windows**: Uses PowerShell to download and install from dbt Labs CDN **Associate SQL Files** The wizard will help you associate `*.sql` files with `jinja-sql` language mode for proper syntax highlighting and IntelliSense support. **Validate Project** The last step is clicking the "Validate Project" button. It will run a bunch of checks to make sure your dbt Fusion environment and project are set up correctly. If there are issues, it will tell you exactly what's wrong. /// admonition | If you still can't get the extension setup correctly, please contact us via slack or chat through [support page](https://www.altimate.ai/support) type: tip /// ## Manual method of configuration /// admonition | Please follow the manual method only if you couldn't use the setup wizard above. type: info /// ### Step 1: Install dbt Fusion #### Automatic Installation via Extension 1. Open the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) 2. Type "dbt Power User: Install dbt" 3. Select the command and choose "dbt Fusion" when prompted 4. The extension will automatically download and install dbt Fusion for your platform #### Manual Installation **macOS and Linux:** ```bash curl -fsSL https://public.cdn.getdbt.com/fs/install/install.sh | sh -s -- --update ``` **Windows (PowerShell):** ```powershell irm https://public.cdn.getdbt.com/fs/install/install.ps1 | iex ``` #### Verify Installation After installation, verify that dbt Fusion is properly installed by running: ```bash dbt --version ``` You should see output that includes "dbt-fusion" in the version information. ### Step 2: Configure dbt Integration Type Set the integration type to fusion in your VSCode settings: #### Method 1: Via VSCode Settings UI 1. Open VSCode Settings (`Ctrl+,` / `Cmd+,`) 2. Search for "dbt integration" 3. Set "Dbt: Dbt Integration" to "fusion" #### Method 2: Via settings.json Add the following to your VSCode settings.json: ```json { "dbt.dbtIntegration": "fusion" } ``` ### Step 3: Associate *.sql files with jinja-sql #### Method 1: Configure in Preferences > Settings ![File Associations](images/associations.png) #### Method 2: Update settings.json directly ```json { "files.associations": { "*.sql": "jinja-sql", "*.yml": "jinja-yaml" } } ``` ### Step 4: Verify Configuration After configuration, check that: 1. The bottom status bar shows "dbt fusion" with a checkmark 2. You can execute dbt commands through the extension 3. IntelliSense and syntax highlighting work in your dbt files ## Enable SaaS features by adding API key There are multiple features in the extension, including [generate dbt documentation](../document/generatedoc.md), [column lineage](../test/lineage.md), [query explanation](../develop/explanation.md), [generate dbt model from SQL](../develop/genmodelSQL.md) that require an API key. /// details | You can get an API key for free by signing up at [www.altimate.ai](https://www.altimate.ai)
/// You need to add the API key from "Settings->API key" in your Altimate instance to the VSCode extension settings. You also need to add "Instance name" in the extension settings. Please get your instance name from your Altimate AI URL. If your URL for Altimate instance is - "companyx.app.myaltimate.com", then instance name is "companyx". Go to VSCode extension settings, and add API key and instance name there. /// details | Here's demo of how to add instance name and API Key to the extension settings
/// ## Feature Support dbt Fusion integration supports most extension features with some exceptions: ### ✅ Supported Features - **Query Execution**: Execute models and preview results - **SQL Compilation**: View compiled SQL code - **Auto-completion**: IntelliSense for models, macros, and sources - **Column Lineage**: Trace data lineage between models - **SQL Validation**: Validate SQL without execution - **Defer to Production**: Run models without rebuilding dependencies - **Test Generation**: AI-powered test generation - **Query Explanation**: AI-powered SQL explanation ### ❌ Limited Features - **Documentation Generation**: Not supported in dbt Fusion CLI - **Some Advanced Features**: May have limitations compared to dbt Core integration ## Questions and Answers #### What is dbt Fusion and how is it different from dbt Core? dbt Fusion is an enhanced command-line interface for dbt that provides improved performance and additional features. Unlike dbt Core, which requires a Python environment, dbt Fusion is a standalone executable that can be installed independently. #### Is dbt Fusion integration free? Yes, dbt Fusion integration is free and treated the same as integration with dbt Core. It will not count towards the usage quota for AI features. #### Do I need Python installed to use dbt Fusion? No, dbt Fusion is a standalone executable that doesn't require a Python environment. This makes it easier to install and manage compared to dbt Core. #### Can I switch between dbt Core and dbt Fusion integrations? Yes, you can switch between integration types by changing the `dbt.dbtIntegration` setting in VSCode. The extension will automatically detect and use the appropriate dbt executable. #### What if dbt Fusion is not available for my platform? dbt Fusion supports macOS, Linux, and Windows. If you encounter installation issues, you can fall back to using dbt Core or dbt Cloud integrations instead. #### Why do I need to add the Altimate API key? The API key is necessary for advanced AI-powered features like query explanation, test generation, and column lineage. Basic dbt operations (execution, compilation) work without an API key. ================================================ FILE: documentation/docs/setup/sso.md ================================================ If you would like to use SSO for authentication, please follow the instructions below.
We support OIDC (OpenID Connect) today, and SAML is not yet supported. We will explain the process for Okta as authentication provider below, but similar steps can be used for other authentication providers as well. ## OKTA SSO Setup ## Step 1: Create a web app Choose authentication method as "OIDC" and type of application as "web application". ![Authentication Type](images/authType.png) Add additional config for - **Sign-in redirect URI**: https://"instance-url"/login/callback/okta
**Sign-out redirect URI**: https://"instance-url"/login Your instance-url may be either "instance-name".app.getaltimate.com or "instance-name".app.myaltimate.com /// admonition | If you don't know the instance-name in above URIs, please contact the support team type: tip /// Also, in "Assignments" section, please choose "skip group assignments" for now. ![Authentication Config](images/authConfig.png) Finally, save the config to create the Okta app. ## Step 2: Share config info with our support team Share Client-ID, Client-Secret, OIDC provider (Okta) URL (e.g.https://trial-1627894-admin.okta.com/admin/app/) with the support team. ![Authentication Details](images/authDetails.png) ## Step 3: Start using Altimate AI instance with SSO Once our support team has done necessary config based on the info provided by you in the earlier step, we will notify you. You can also create a bookmark app in Okta by following these [instructions from Okta documentation](https://support.okta.com/help/s/article/create-a-bookmark-app?language=en_US) Please use URL as https://.getaltimate.com/login-redirect/okta or https://.myaltimate.com/login-redirect/okta based on instructions shared by our support team.
After this point, you can start using your Altimate AI instance with SSO and start onboarding users! /// admonition | You need to have an enterprise plan for the SSO integration. type: info /// ## Azure AD SSO Setup ## Step 1: Create Azure AD App Go to Azure [home](https://portal.azure.com/#home) and search “Active Directory” in search bar, and select active directory ![Azure AD Search](images/AzureADSearch.png) Click + Add and select App registration. ![New AD App](images/newADApp.png) Enter a valid application name and in “Redirect URI” section, select “Web” and enter url “https:///login/callback/active-directory” (replacing “instance url” with your instance url) ![AD URL](images/ADURL.png) Your instance-url may be either "instance-name".app.getaltimate.com or "instance-name".app.myaltimate.com /// admonition | If you don't know the instance-name in above URIs, please contact the support team type: tip /// After registration, in overview section of the application, copy and save “Application (client) ID”. Then, Click “Add a certificate or secret” ![Add Certificate](images/AddCertificate.png) click “New client secret”. Enter description and select expiration according to your preferences. ![Add Client Secret](images/addClientSecret.png) /// admonition | Make sure to contact us to update the secrets if it is expired. type: tip /// Copy and save the secret value ![Save Secret Value](images/saveSecretValue.png) - Click “Default Directory | App registrations” link in the top left breadcrumb - Click “Endpoints” - Copy and save “OpenID Connect metadata document” url ![Document URL](images/documentURL.png) ## Step 2: Share config info with our support team From instructions in 'Step1' above, copy following information and share it with our support team ``` Application (client) ID client secret OpenID Connect metadata document url ``` ## Step 3: Start assigning users / groups to app Once our support team has done necessary config based on the info provided by you in the earlier step, we will notify you. After this point, you can start with additional steps necessary as below: ### Update the app to require user assignment 1. Sign in to the [Microsoft Entra admin center](https://entra.microsoft.com/). 2. If you have access to multiple tenants, use the Directories + subscriptions filter in the top menu to switch to the tenant containing the app registration from the Directories + subscriptions menu. 3. Browse to Identity > Applications > Enterprise applications, then select All applications. 4. Select the application you created earlier. Use the filters at the top of the window to search for a specific application. 5. On the application's Overview page, under Manage, select Properties. 6. Locate the setting Assignment required? and set it to Yes. 7. Select Save on the top bar. ![App Config](images/appConfig.png) ### Assign the app to users and groups to restrict access Once you've configured your app to enable user assignment, you can go ahead and assign the app to users and groups. 1. Under Manage, select the Users and groups then select Add user/group. 2. Under Users, select None Selected, and the Users selector pane opens, where you can select multiple users and groups. 3. Once you're done adding the users and groups, select Select. 4. (Optional) If you have defined app roles in your application, you can use the Select role option to assign the app role to the selected users and groups. 5. Select Assign to complete the assignments of the app to the users and groups. 6. On return to the Users and groups page, the newly added users and groups appear in the updated list. ![App Users](images/appUsers.png) /// admonition | If needed, please check Microsoft Entra documentation [here](https://learn.microsoft.com/en-us/entra/identity-platform/howto-restrict-your-app-to-a-set-of-users) type: tip /// ## Azure AD Troubleshooting If you encounter email related error during login with active directory like below, ask your Azure Active directory admin to add a valid email to the user as explained in next steps ![Error Info](images/errorInfo.png) Solution: In Azure, go to “Users” by searching in top search input. Select the user who has email issue and go to “Properties” and click edit button near “Contact Information”. Enter a valid email in “Email” field and save. User should be able to login now ![Contact Info](images/contactInfo.png) ================================================ FILE: documentation/docs/studio.md ================================================ # Studio ## What is Studio? Studio is a specialized agent designed for users to interact with their data stack using natural language. It is an easy-to-use solution for all your questions across Snowflake, DBT, Tableau, Databricks, Airflow, GitHub and more. ## Key Capabilities Studio empowers users to: - **Analyze performance** of existing workloads across your data infrastructure - **dbt Project analysis** of entire dbt project structure, models, tests, dependencies etc. - **Lineage exploration** to explore the lineage of dbt models and answer related questions - **Documentation search** allows to search through dbt models documentation as well as add missing documentation - **Perform root cause analysis** to quickly identify and resolve issues - **Conduct cross-tool analysis** spanning multiple platforms in a single conversation - **Generate detailed optimization plans** with quantified outcomes and projected savings - **Best practices review** to get recommendations on dbt models best practices implementation ## Why Do We Need Studio? Traditional AI approaches have critical limitations. These limitations are well taken care of through Studio: ### Without Studio 1. User asks: "Why is my dbt model running slow?" 2. Agent searches basic data 3. Finds execution time metrics 4. Provides incomplete analysis with: - Missing: Root cause of performance issue - Missing: Downstream impact on dependent models - Missing: Historical performance trends **Result:** Incomplete, Siloed AI Responses ### With Studio 1. User asks: "Why is my dbt model running slow?" 2. Multi-agentic framework assembles rich context 3. Provides comprehensive analysis: - Root cause identified (e.g., inefficient CTEs, missing indexes) - Downstream impact on dependent models assessed - Actionable optimization recommendations with code examples **Result:** Context-Rich, Memory-Enhanced Intelligence ## Extending Context Beyond the integrations already connected to your SaaS instance, Studio allows you to enrich your queries with additional context: ![Studio Datamates and Knowledge Base](studio/images/Studio-context.png) | Option | Description | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Datamates** | Select any [Datamate](https://datamates-docs.myaltimate.com/user-guide/home/) to widen the context based on tools | | **Knowledge Bases** | Connect organizational knowledge repositories from [Knowledge Hub](https://datamates-docs.myaltimate.com/user-guide/components/knowledgehub/) for deeper context | | **File Attachments** | Upload documents, queries, or data files directly into your conversation | This flexibility ensures you can ask questions across a broader tool set while providing the agent with the context it needs to deliver accurate, actionable insights. ## How Studio Works ### 1. Providing Input When starting a conversation, users have multiple ways to provide context: | Option | Description | | ----------------------- | ------------------------------------------------------------------------------ | | **Ask a question** | Type your query directly in the chat input | | **Add Datamate** | Select a Datamate to access any tool specific information | | **Attach files** | Upload files for analysis (via "+" menu) | | **Knowledge Base** | Access organizational knowledge repositories from Knowledge Hub (via "+" menu) | | **View Prompt Library** | Browse pre-built prompts for common tasks (via 'Select Prompt Library') | ### 2. Results Delivery Once the search is complete, you receive a comprehensive analysis with: - **Executive Summary** - High-level findings - **Key Metrics** - Data tables with critical information - **Detailed Analysis** - In-depth analysis (e.g., query-by-query breakdown) - **Key Findings** - Insights with evidence and impact - **Root Cause Analysis** - Why issues occurred - **Optimization Recommendations** - Prioritized action items - **Next Steps** - Implementation roadmap ### 3. Output Features Analysis results include interactive options: | Feature | Description | | ----------------------- | ------------------------------------------- | | **View detailed steps** | Expand to see the full execution trace | | **Task Progress** | Visual checklist of completed subtasks | | **Download** | Export results for offline use or reporting | ### 4. Confidence Indicators Studio provides transparency about result quality: | Indicator | Meaning | | -------------------- | ----------------------------------------------- | | Low Confidence (50%) | Consider refining your query for better results | | High Confidence | Results are reliable and well-supported | ## Components of Studio Two important components of Studio are Chat History and Prompt Library: ![Studio Chat History and Prompt Store](studio/images/Studio-history-prompt-store.png) ### Chat History Chat History is a persistent record of all your previous conversations with Studio. Located in the left sidebar, it provides quick access to past interactions, allowing you to revisit analyses, continue previous work, or reference earlier insights. #### Key Features | Feature | Description | | --------------------- | --------------------------------------------------------------------------- | | **Conversation List** | All past chats are displayed chronologically with preview titles | | **Status Indicators** | Each conversation shows its current status (e.g., "Completed") | | **Timestamps** | Relative timestamps (e.g., "8h ago", "11h ago") help you locate recent work | | **Search** | Quickly find specific conversations using the search bar | | **New Chat** | Start a fresh conversation at any time with the "+ New Chat" button | #### How It Works - **Automatic Saving** - Every conversation is automatically saved to your Chat History - **Title Generation** - Conversations are titled based on your initial query (e.g., "Help me create a Jira ticket with the below details...", "Analyse the query") - **Quick Resume** - Click any conversation to instantly resume where you left off #### Use Cases - **Continuing Analysis** - Pick up where you left off on a complex investigation - **Reference Past Insights** - Look back at previous cost analyses or query optimizations - **Audit Trail** - Track what questions you've asked and what answers you received - **Knowledge Building** - Build on previous conversations rather than starting from scratch ### Prompt Library Prompt Library is a shared repository of pre-built, reusable prompts that help you get started quickly with common data analysis tasks. It serves as a knowledge base of effective queries created by you, your team and the organization. #### Key Features | Feature | Description | | ---------------------- | ----------------------------------------------------------------------------- | | **Pre-built Prompts** | Ready-to-use prompts for common analytical tasks | | **Tagging System** | Organize prompts with tags like "query", "summary", "test" for easy discovery | | **Ownership Tracking** | See who created each prompt (individual or "Altimate AI") | | **Team Sharing** | Share prompts with "All org" or specific teams | | **Search & Filter** | Find prompts by type, team, owner, or tags | | **Custom Prompts** | Create and save your own prompts with "+ New Saved Prompt" | #### How to Use 1. **Access the Library** - Click "View Prompt Library" from the Suggested Prompts link on the main Studio screen 2. **Browse or Search** - Use filters (Type, Teams, Owner, Tags) or search to find relevant prompts 3. **Select a Prompt** - Click the send icon to use a prompt directly 4. **Customize** - Many prompts have placeholders (e.g., ``) that you can fill in 5. **Save Your Own** - Click "+ New Saved Prompt" to add your custom prompts to the library #### Filters Available | Filter | Purpose | | --------- | ---------------------------------------------------- | | **Type** | Filter by prompt category | | **Teams** | Show prompts shared with specific teams | | **Owner** | Filter by prompt creator | | **Tags** | Filter by semantic tags (query, summary, test, etc.) | | **Sort** | Order by "Recently Added" or other criteria | #### Benefits - **Faster Start** - Don't write prompts from scratch; use proven templates - **Best Practices** - Leverage prompts crafted by experts - **Consistency** - Teams use standardized prompts for common analyses - **Knowledge Sharing** - Share effective prompts across the organization or with specific teams - **Parameterized Templates** - Prompts with placeholders adapt to different scenarios ## Accessing Studio from SaaS Pages Studio is also seamlessly integrated throughout the SaaS platform, providing context-aware assistance directly within the pages. A Studio popup is available on every major page, offering relevant prompts and insights based on the specific data and context of that page. ### How It Works Each SaaS page includes a **Studio button** (chat icon) in the bottom right corner. Clicking this button opens a Studio popup panel that: ![Studio Access Button](studio/images/Studio-chat-button.png) ![Studio Popup Panel](studio/images/Studio-chat-popup.png) - **Automatically understands the page context** - The agent knows which page you're on and what data you're viewing - **Provides relevant suggested prompts** - Pre-built questions specific to that page's functionality - **Allows custom questions** - You can ask your own questions within the page context - **Offers full expansion** - Use the "Expand in Studio" link to open the full Studio experience ### Page-Specific Studio Integration #### Summary Page: When accessing Studio from the **Summary** page, you get assistance focused on cost analysis and spending insights. **Use Cases**: Cost analysis, spending trends, budget forecasting, cost optimization opportunities #### Discover Page: When accessing Studio from the **Discover** page, you get assistance in identifying and prioritizing optimization opportunities. **Use Cases**: Optimization prioritization, cost-saving initiatives, quick wins identification, resource efficiency #### Datasets Page: When accessing Studio from the **Datasets** page, you get assistance for exploring and understanding your data assets. **Use Cases**: Data catalog exploration, metadata analysis, data governance, dependency mapping #### Code Page: When accessing Studio from the **Code** page (Queries tab), you get assistance for query performance analysis and optimization. **Use Cases**: Query optimization, performance troubleshooting, cost attribution, anti-pattern identification #### Infra Page: When accessing Studio from the **Infra** page, you get assistance for infrastructure management and warehouse optimization. **Use Cases**: Resource monitoring, infrastructure cost optimization, utilization analysis ### Benefits of Page-Specific Studio | Benefit | Description | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | **Consistent Experience** | Same powerful Studio's agent capabilities available wherever you need them | | **Contextual Intelligence** | The agent understands what data and metrics are relevant to your current page | | **Faster Insights** | No need to switch between pages. Suggested prompts help you ask the right questions immediately without thinking about context | | **Progressive Disclosure** | Start with a quick popup, expand to full Studio when needed | | **Chat History** | The history for all the chats done through SaaS pages is retained and can be viewed on the main Studio page | /// admonition | Studio is currently in Beta. We're continuously improving the platform based on user feedback. type: info /// /// admonition | For more information and access to Studio, visit [app.myaltimate.com](https://app.myaltimate.com) type: tip /// ================================================ FILE: documentation/docs/stylesheets/extra.css ================================================ /* Announcement banner — thin strip style */ .md-banner { background: linear-gradient(90deg, #1565c0 0%, #6a1b9a 50%, #8e24aa 100%) !important; color: #ffffff !important; padding: 0 !important; overflow: visible !important; position: relative !important; } .md-banner .md-banner__inner { margin: 0 auto !important; padding: 0.3rem 1rem !important; font-size: 0.7rem !important; line-height: 1.4 !important; text-align: center; white-space: nowrap; position: static !important; } .md-banner .md-banner__inner a:not(.md-banner-cta) { color: #e1bee7 !important; text-decoration: none; font-weight: 500; } .md-banner .md-banner__inner a:not(.md-banner-cta):hover { color: #ffffff !important; text-decoration: underline; } .md-banner .md-banner__inner .md-banner-cta { display: inline-block; background: rgba(255, 255, 255, 0.18); color: #ffffff !important; padding: 0.15rem 0.65rem; border-radius: 20px; font-size: 0.65rem; font-weight: 600; text-decoration: none !important; margin-left: 0.5rem; border: 1px solid rgba(255, 255, 255, 0.3); letter-spacing: 0.02em; transition: background 0.2s; vertical-align: middle; } .md-banner .md-banner__inner .md-banner-cta:hover { background: rgba(255, 255, 255, 0.3); } .md-banner .md-banner__button { position: absolute !important; right: 1rem !important; top: 50% !important; transform: translateY(-50%) !important; color: rgba(255, 255, 255, 0.5) !important; float: none !important; margin: 0 !important; padding: 0 !important; } .md-banner .md-banner__button:hover { color: #ffffff !important; } .md-banner .md-banner__button svg { width: 0.85rem; height: 0.85rem; } ================================================ FILE: documentation/docs/teammates/altimate-code.md ================================================ --- status: new --- # Altimate Code in IDE ## What is Altimate Code? [Altimate Code](https://docs.altimate.sh) is the open-source data engineering harness with 100+ deterministic tools for building, validating, optimizing, and shipping data products. It brings AI-powered data engineering directly into your IDE through the Datamates extension, or can be used standalone via CLI and TUI. ## Getting Started in Your IDE ### Install the Datamates Extension - **VS Code** — [Microsoft Marketplace](https://marketplace.visualstudio.com/items?itemName=altimateai.vscode-altimate-mcp-server) - **Cursor / other VS Code-compatible editors** — [Open VSX Registry](https://open-vsx.org/extension/altimateai/vscode-altimate-mcp-server) ### Open Altimate Code Chat 1. Press `Cmd+Shift+P` (macOS) or `Ctrl+Shift+P` (Windows/Linux) to open the command palette 2. Type `Datamates` 3. Select **Datamates: Open Altimate Code Chat** This opens the Altimate Code chat panel where you can interact with agents and run data engineering tools. ## Features ### Agent Modes Altimate Code provides three agent modes to match your workflow: | Mode | Access Level | Use Case | |------|-------------|----------| | **Builder** | Full read/write | Scaffolding dbt projects, writing models, generating tests and docs | | **Analyst** | Read-only | Exploring schemas, running queries, analyzing lineage | | **Plan** | Minimal access | Planning changes, reviewing impact before execution | ### 100+ Data Engineering Tools - **SQL Tools** — Validation without execution, query optimization, anti-pattern detection, dialect translation, PII scanning - **dbt Tools** — Model generation, test generation, documentation generation, project scaffolding, troubleshooting - **Lineage Tools** — Column-level lineage, impact analysis, downstream dependency tracking - **Schema Tools** — Schema exploration, table/column discovery, metadata indexing - **FinOps Tools** — Cost analysis, warehouse spend reports, optimization recommendations - **Warehouse Tools** — Direct query execution, result preview, connection management ## Benchmarks Altimate Code is **#1 on ADE-Bench** — the industry benchmark for AI data engineering agents, created by Benn Stancil (founder of Mode) in collaboration with dbt Labs. It evaluates agents on real-world analytics and data engineering tasks using actual dbt projects and databases. **The harness — not the model — is the differentiator.** Despite using Sonnet 4.6 (not the most expensive model), Altimate Code outperforms agents running on more capable models, demonstrating that purpose-built tooling and deterministic operations outperform raw model capability alone. ### ADE-Bench (DuckDB Local) | Tool | Model | Score | Pass Rate | |------|-------|-------|-----------| | **Altimate Code** | Sonnet 4.6 | **32/43** | **74.4%** | | Cortex Code CLI | Opus 4.6 | 28/43 | 65% | | dbt Labs | Sonnet 4.5 | ~25/43 | 59% | | Claude Code (baseline) | Sonnet 4.6 | ~17/43 | 40% | ### Other Benchmarks | Benchmark | Result | |-----------|--------| | **SQL Anti-Pattern Detection** | 100% accuracy across 1,077 queries, 19 categories. Zero false positives. | | **Column-Level Lineage** | 100% edge match across 500 queries with complex joins, CTEs, and subqueries. | | **Snowflake Query Optimization (TPC-H)** | 16.8% average execution speedup (3.6x vs baseline). | [Full benchmark details →](https://www.altimate.sh/benchmarks) ## LLM Access Two options for powering the AI chat: - **BYOK (Bring Your Own Key)** — Free and unlimited. Use any of 35+ supported providers (Anthropic, OpenAI, AWS Bedrock, Azure OpenAI, Google, Ollama, and more) - **[Altimate LLM Gateway](../arch/llm-gateway.md)** — Managed LLM access with dynamic routing across Sonnet 4.6, Opus 4.6, GPT-5.4, GPT-5.3, and more. 10M tokens free to get started — no API keys to manage ## Standalone Usage Altimate Code can also be used outside the IDE: | Interface | Description | |-----------|-------------| | **TUI** | Interactive terminal UI — `altimate` | | **CLI** | Command-line for scripting — `altimate run` | | **Web UI** | Browser-based interface — `altimate web` | | **CI/CD** | Headless mode for pipelines — `altimate check` | | **GitHub/GitLab** | Automated PR review and issue triage | Install standalone: ```bash npm install -g altimate-code ``` ## Full Documentation - **Altimate Code docs** — [docs.altimate.sh](https://docs.altimate.sh) - **Datamates docs** — [datamates-docs.myaltimate.com](https://datamates-docs.myaltimate.com/) ================================================ FILE: documentation/docs/teammates/coach.md ================================================ You can coach and personalize your AI teammates by giving instructions in the natural language. First, enable AI teammates functionality by going in settings -> teammates menu. ![enableTeammates](images/enableTeammates.png)
/// admonition | Unless you enable teammates in the SaaS instance, coaching and personalization outlined below won't work. type: warning /// ## Documentation Writer Documentation writer AI teammate is available via the documentation editor panel in the Power User VSCode extension. Here are more details on how to [generate documentation](../document/generatedoc.md) ### Trigger coaching If you want to coach your documentation writer, after the documentation is generated, click on "do you want to coach AI" icon as shown below: ![triggerCoachAI](images/triggerCoachAI.png)
### Provide instructions You can provide instructions in the natural language first and then the AI teammate will show you what it has understood. You can edit that understanding further as shown below: ![addInstructions](images/addInstructions.png)
### Curate and update learnings All the learnings done by AI teammates can be curated further by you in the settings -> teammates are in the SaaS UI. You can edit the earlier learnings, enable / disable them or you can simply delete them as well. ![viewLearnings](images/viewLearnings.png)
================================================ FILE: documentation/docs/teammates/introduction.md ================================================ ## What are AI Teammates? As AI is evolving, now we can create virtual AI teammates where some pieces of work can be done autonomously by them. You can coach these virtual teammates just like new members of your team, so the work they deliver is perfectly tailored for your organizations requirements. ## What work AI Teammates can Do? In certain scenarios, they can assist you vs. doing work completely autonomously and you can personalize these assistants as per your requirements. Today, work done by data teams can be divided in three simple buckets - 1. Done by humans 2. Done by humans and AI together 3. Done by AI autnomously ## How AI Teammates Work with Us? AI teammates are integrated in day to day work of data teams through Power User VSCode extension, Python package, and SaaS UI. Power User extension already has comprehensive AI functionality ranging from dbt model,test,docs generation to SQL query translation & explanation. /// admonition | For more info, please check [our website](https://www.altimate.ai) type: tip /// ================================================ FILE: documentation/docs/test/adhocquery.md ================================================ You can run ad hoc SQL queries or dbt Models in the "Query Results" tab ## Click on the "New Query" Button The "New Query" button is available in the "Query Results" bottom panel. ![New Query Trigger](images/newQueryTrigger.png) ## Enter a Query Enter the SQL or dbt query that you want to run ![New Query](images/newQuery.png) ## Run the Query Click on play button to run the query and see the results in the bottom panel ![See Results](images/seeResults.png) ================================================ FILE: documentation/docs/test/bigquerycost.md ================================================ For dbt models running in Big Query, you can get the estimate of the data processed for that model right inside VSCode as below:
/// admonition | Note This feature is supported for dbt version 1.6 and above. /// ================================================ FILE: documentation/docs/test/defertoprod.md ================================================ Defer functionality in dbt allows the user to run a subset of models or tests without having to first build their upstream parents. Usually, it leads to significant cost and time savings during testing of the dbt models. More information about this functionality is available in [dbt docs](https://docs.getdbt.com/blog/defer-to-prod). ### Step 1: Enable Defer to production functionality in the "Actions" panel as below ![enableDefertoprod](images/enableDefertoprod.png)
### Step 2: Choose where your manifest file is stored for the production environment You can either use this functionality in the local mode where your manifest files stay in your local computer. You can also use the SaaS instance to store your manifest files so multiple team members can benefit from prod or staging data. ## Local Mode Choose the location of your local manifest file as shown in the image below: ![chooseLocal](images/chooseLocal.png)
## SaaS Mode /// admonition | To use the Altimate SaaS instance, you will need to install the `altimate-datapilot` python package. You can install it using the following command: `pip install altimate-datapilot`. This package is required to upload the manifest file to the SaaS instance. You can find more information about the package [here](https://github.com/AltimateAI/datapilot). type: tip /// First, create the dbt integration in the Altimate AI SaaS instance by going to Settings -> Integrations from left side navigation menu. This is just to create a reference where you can upload respective manifest files to point to. /// admonition | If you would like to connect your on-premise storage for manifest file uploads, please [contact us](https://www.altimate.ai/support) via chat or Slack. type: info /// ![addIntegration](images/addIntegration.png)
Then, copy the command (last column in above image) from the UI to upload manifest files mapped to this integration. The command will look something like this -
"datapilot dbt onboard --token --instance-name --dbt_core_integration_id --manifest-path 'Path/to/file'"

(You need to replace 'Path/to/file' with the path to your manifest file e.g. /Users/mrx/documents/repos/jaffle_shop/target/manifest.json) /// admonition | You can use this command to run at regular intervals in your CI/CD or orchestration tool so the latest manifest is always available for reference automatically in other environments type: tip /// Now, choose the right dbt integration to reference in your VSCode as below: ![addIntegration](images/chooseSaaS.png)
### Step 3: Enable/disable favor state Turn on favor-state if you need it. If it's turned on, the defer functionality will favor using the node defined in the referenced integration, even if the node exists in the current project. ## Recorded demo video ## Interactive demo
/// admonition | Using defer to prod with SaaS instance requires an API key. You can get it by signing up for free at [www.altimate.ai](https://www.altimate.ai) Local mode doesn't require an API key. type: info /// ================================================ FILE: documentation/docs/test/lineage.md ================================================ Lineage is available as model level lineage and column level lineage. You need to add an API key to view column level lineage. You can view model level lineage without an API key. ## Model lineage - Different dbt entities like sources, seeds, models, tests, metrics, exposures, and model types are shown in the lineage view. - For applicable components, Clicking on "Details" shows a list of columns with descriptions as well as dbt tests that are written for that particular component. ![Model Lineage](images/modelLineage.png) ## Expand lineage graph You can expand lineage at a single expansion level by clicking (+) signs on individual blocks or you can expand multiple levels of lineage by using "Expand" button as show below ![Expand Lineage Button](images/dbtExpand.png) ## Column lineage - After clicking on "Details" for the component, you can see a list of columns. Then, you can click on a column name to change model lineage to column lineage view. ![Column Selection](images/columnSelection.png) - In the column lineage view, links between components are shown as select links and non-select links. Select links are shown as solid lines if there is a direct flow of data between columns through select statements. Non-Select links are shown as dotted lines if columns appear in the condition/clause like where, join, having, etc. Check the Settings tab if you want to disable certain types of links or set specific expansion levels by default. ![Column Lineage](images/columnLineage.png) ## Code transformations - On each individual component, you can see small icons that show how that column was created (through transformation, just name change or passed unchanged). If code is available for a particular transformation, a small code icon is displayed. - When you click on the code icon, it shows the list of code transformations that were performed to create that column. ![Transformation Code](images/TranformationCode.png) /// admonition | Column lineage with code transformation is also available in SaaS UI. Please refer to the section on [SaaS Discovery UI](../discover/viewlineage.md) type: tip /// ![Lineage SaaS](images/lineageSaaS.png) /// details | Following are a few limitations in the column level lineage type: note - Snapshots are not supported (coming soon) - Operators that may result in an incomplete lineage - Unnest - Lateral View Flatten - Json flatten to columns /// ## Export lineage You can export lineage view to a web page. Please check more details [here](../govern/collaboration.md#lineage-export-workflow) ## Interactive demo - column lineage
## Recorded demo - column lineage
/// admonition | Column lineage requires an API key. You can get it by signing up for free at [www.altimate.ai](https://www.altimate.ai) type: info /// ================================================ FILE: documentation/docs/test/queryResults.md ================================================ You can preview the resulting data and SQL query from your code with the extension, export it as CSV and do further analysis. ## Preview results, export and analyze You can preview results for the entire dbt model or select part of the dbt model to preview results only for that selection. After you select, Press Cmd+Enter (Mac) or Control+Enter (Windows/Linux) to run a query.
/// details | You can also preview query results via "execute dbt SQL" operation from the toolbar on the top right corner. You have to click on the "play" button as shown in the image below. ![preview results from toolbar](images/previewresultsToolbar.png) /// /// details | You can view and copy the SQL query that was executed to get the data results. In order to view executed SQL query, you need to click on SQL tab inside "Query Results" bottom panel. There is a copy SQL button also, it becomes visible only when you hover over the top right corner. ![copy sql button](images/copysqlbutton.png) /// ## Configure settings for query preview There are multiple actions available as actions on the top of query results preview window. Please click “configure” button to make it visible. | Action | Details | | ----------- | ---------------------------------------------------------------------------------------------------- | | Dark Mode | You can change configuration of preview results display to light, dark, solarized etc. display modes | | Scroll Mode | Free scroll or aligned scroll | | Read only | Read only or editable mode (Note: editable mode doesn’t save values in the database) | | Reset | Reset button to reset the view | | Export | You can export the data in CSV format | | Copy | You can copy the data shown in the preview. There are multiple options like CSV and JSON format | Query preview is limited to 500 rows by default, this can be configured in Settings area or you can configure query results options in "Help" tab in the bottom panel as below. You can also change table zoom level with 'scale' setting so you can see more columns in a single view or you can clear results by clicking “clear results” button. ![Query results settings](images/queryresultsSettings.png) ## Compare Query Results You can save query results by opening in a tab. Then, make changes to a query and run query results. Now, you can compare previous results with new results easily as below:
## Run an ad-hoc query You can run ad hoc SQL queries or dbt Models in the "Query Results" tab ![New Query](images/newQuery.gif) ================================================ FILE: documentation/docs/test/runctes.md ================================================ # Preview CTEs (Common Table Expressions) dbt Power User allows you to preview individual CTEs (Common Table Expressions) within your dbt models, making it easier to debug and understand complex queries by examining each component separately.
## What are CTEs? Common Table Expressions (CTEs) are temporary named result sets that exist within the execution scope of a single SQL statement. They help break down complex queries into more manageable, readable pieces. In dbt, CTEs are often used to organize complex transformations into logical steps. ## Preview Individual CTEs ### Using the CTE Preview Feature You can preview the results of individual CTEs within your dbt model: 1. **Open your dbt model** in the editor 2. **Locate the CTE** you want to preview 3. **Click on Execute CTE** right above the CTE ================================================ FILE: documentation/docs/test/runtests.md ================================================ There are two methods to run dbt model tests. You can either do it from the top right corner toolbar or from the extension side panel. ### Method 1: Run tests from the toolbar The toolbar action to run tests is present on the top right corner of the VSCode, as shown in the image below: ![Test models](images/runTests.png) ### Method 2: Run tests from the side panel On the left side of the navigation, click on the dbt power user extension icon to open the left-side panel, as shown in the image below. Then, click on the "Test dbt Model" button (hover over the area shown by the red circle to make it visible) to execute dbt model tests. ![Run tests in the side panel](images/testsPanel.png) ================================================ FILE: documentation/docs/test/sqlvalidation.md ================================================ Identify SQL issues like non-existent columns, keyword typos, extra parentheses easily Following SQL checks are available: | Check | Details | | ---------------------------- | ----------------------------------------------------------------------------------------------- | | Identify non-existent column | If SQL is referencing some columns that don't exist, those columns will be identified as error. | | Keyword typos | If there are some typos in SQL keywords, those keywords will be flagged | | Missing or extra parentheses | If there are missed or extra parentheses, that SQL area will be highlighted |
================================================ FILE: documentation/docs/test/sqlvisualizer.md ================================================ # SQL Visualizer (Beta) The new SQL visualizer translates complex SQL queries into intuitive graphical representations. SQL visualizer functionality shows the SQL structure of your code and how different components of the code are connected. The provided SQL query is visually broken down into nodes representing CTEs, joins, filters, and unions, making it easier to understand, refactor, and debug the query components. ### Key Benefits - Enhanced Comprehension: Quickly grasp SQL structure and flow through visual elements. - Improved Productivity: Drag-and-drop functionality for faster query modification. - Better Collaboration: Visual diagrams are effective documentation and are easier to share. - Easier Refactoring: Simplify query restructuring with the visual graph of all components. - Simplified Debugging: Identify and correct errors quickly by visually tracing query logic. /// admonition | This functionality is still in beta. The functionality may change further to streamline the experience type: info /// ## Trigger the functionality ### Actions Toolbar Click on "SQL Actions" icon as shown in the image below and choose "Visualize SQL" option from the drop down as below: ![SQL Actions Trigger](images/buttonSQLActions.png) ### Right Click Menu You can just right click in the file, chose "DataPilot" menu and then choose option "Visualize SQL" ![Right Click Visualizer](images/rightClickVisualizer.png) ### Command Palette Open the model file for which you need to see SQL visualization. Then, press cmd+shift+P (for Mac) or Ctrl+shift+P (for Windows) to bring up the command palette. Search for "SQL visualizer" and execute it. ![Command SQL Visualizer](images/commandSQLVisualizer.png) ## View the SQL components flow Please use the bottom left corner buttons to zoom in / zoom out, etc. ![SQL Flow](images/graphView.png) ## Hover over connections to view the code You can hover over connections like join, group, union to see the code. You can also click on "View Details" within the component to see the list of columns and descriptions. ![View Code](images/codeView.png) ## Same view is available in the SaaS Discovery UI SQL visualizer is also available in the SaaS Discovery UI where you can also see column lineage, dbt Model documentation and compiled code. Please check more details on how to set up SaaS Discovery UI [here](../discover/setupui.md) ![saas SQL Visualizer](images/saasSQLVisualizer.png) ## Recorded Demo
================================================ FILE: documentation/docs/test/utilities.md ================================================ ## dbt logs viewer (force tailing) ![dbt-logviewer](images/dbt-log.gif) ================================================ FILE: documentation/docs/test/writetests.md ================================================ You can generate, view, edit and delete dbt tests in VS Code under the Documentation Editor section. /// details | Following are a few limitations - The [alternative method for defining tests](https://docs.getdbt.com/reference/resource-properties/data-tests#alternative-format-for-defining-tests) is not supported yet - the definition of tests defined as macro is not available yet /// ## View dbt Tests The documentation editor shows the tests that have been added for the dbt model and columns. You can see the details of the tests by clicking on the test name.
![View tests](images/viewTestDetails.gif) ## Add dbt Tests You can add default dbt tests: unique, not_null, accepted_values, relationship by clicking (+) sign next to the "Tests:" label. ![Add tests](images/addGenericTest.gif) ## Generate dbt Tests (Beta) You can also generate test code for custom tests based on dbt.utils and dbt.expectations package. First, click on (+) sign next to the "Tests:" label and choose "custom tests". DataPilot automatically detects which dbt packages you have installed, and it generates test code based on packages in your environment. If a test definition is not available from packages (dbt.utils or dbt.expectations) that you have installed in your environment, DataPilot writes custom SQL test code and puts it in the dbt macro. /// admonition | This functionality is marked as beta. In some rare cases, DataPilot may decide to ignore the packages that you have installed or may generate inaccurate code. type: info /// ![Generate tests](images/testGeneration.png) ## Edit/delete dbt Tests You can edit existing dbt tests if they are default dbt tests: unique, not_null, accepted_values, relationships by clicking on the test and using the "pencil" icon from the details screen. You can delete any dbt test by clicking on the test and using the "trash can icon" from the details screen ![Edit tests](images/editTest.png) ### Getting distinct values for "accepted_values" test As shown in the image above, there is a button to quickly get distinct values for a specific column with a click of a button. This helps you write the "accepted_values" test easily. /// admonition | Save changes in YAML file type: tip You can save the changes in the existing or a new YAML file with save button at the bottom of the panel. If you see any issues with the content that's saved in the YAML file, please check the [optional config section](../setup/optConfig.md/#column-name-setup-for-yaml-file-updates). /// ## Recorded demo video ================================================ FILE: documentation/docs/troubleshooting.md ================================================ ## Troubleshooting Steps Follow these steps to effectively troubleshoot and resolve issues with the extension: ### Setup Wizard Initially, use the setup wizard. This tool is designed to identify and possibly resolve environment issues automatically. You can access it by selecting "dbt" or "dbt is not installed" in the bottom left bar of the IDE and then clicking on "Setup Extension".
### Problems Panel Check the problems panel for any issues with your dbt project. You can access the problems panel by clicking on the `Problems` tab. ![Viewing the Problems Panel](images/problems-panel.png) ### Diagnostics command The diagnostics command in the VSCode Power User extension provides a comprehensive report that covers various aspects of the system and DBT project environment. Here is a summary of the diagnostics it generates: - **Environment Variables**: Lists all the environment variables which can help in understanding the system configuration and paths that might affect the DBT operations. - **Extension Settings**: Details the settings related to the VSCode extension, including preferences like AI keys, instance names, and various enabled or disabled features that influence how the extension behaves. - **Python and DBT Installations**: Confirms the installation of Python and DBT, along with their paths, ensuring that these essential tools are available and correctly set up. - **Workspace and Project Details**: Provides information about the active workspace and DBT projects, such as the version of DBT, project directory, and the first workspace path. - **DBT Project Configuration**: Outlines the configuration for the DBT project, including adapter type and version, and paths to essential files like dbt_project.yml. - **Connection Testing**: Tests and verifies the database connection, detailing the connection parameters and any errors encountered during the connection attempt. - **Potential Issues and Errors**: Identifies any issues with dependencies, such as mismatches in versions of Python libraries, and provides errors encountered during the execution of the DBT commands. Overall, the diagnostics command is designed to help users quickly assess and troubleshoot their DBT setup within VSCode by providing detailed insights into configuration, environment, and connection status. Running the command - On Mac, press `Cmd + Shift + P` or On Windows/Linux, use `Ctrl + Shift + P` - type diagnostics and pick the option listed under the dbt power user extension name and press enter ![Diagnostics](images/diagnostics.png) - this should start a terminal window and print the diagnostic information ### Check Extension Logs If the problem persists, examine the logs in the IDE's output panel. 1. Select `Log -> dbt` from the dropdown menu to view detailed extension logs, which can provide insights into underlying issues. 2. To access more detailed logs, you can change the log level to "Debug": - Open the vscode command palette - - On Mac, do this by pressing `Cmd + Shift + P`. - On Windows/Linux, use `Ctrl + Shift + P`. - Once the command palette opens, type `Set Log Level`, then choose `Debug`. 3. Run the operations again and you should start seeing debug logs in the `Log -> dbt` output stream ![Viewing Log - dbt in the Output Panel](images/extension-logs.png) ### Developer Tools For more in-depth diagnostics, use the developer tools in Visual Studio Code (VSCode). Navigate to `Help -> Toggle Developer Tools` to access these tools, including a console with detailed logs and error messages. ![Accessing Developer Tools in VSCode](images/developer-tools.png) ### Contact Support If issues still remain unresolved, please [contact us](https://www.altimate.ai/support) via Slack or chat for further assistance. /// admonition | Feedback Widgets type: tip There are also feedback widgets in the extension embedded in various features, where you can directly provide feedback on the roadmap or any issues that you encountered. /// /// admonition | Still stuck? [contact us](https://www.altimate.ai/support) via Slack or chat type: tip /// ================================================ FILE: documentation/mkdocs.yml ================================================ site_name: dbt Power User site_description: the best dbt extension for vscode site_author: Altimate Inc theme: name: material logo: assets/logo.png icon: repo: fontawesome/brands/github custom_dir: docs/overrides features: - announce.dismiss - content.action.edit - content.action.view - content.code.annotate - content.code.copy # - content.code.select # - content.tabs.link - content.tooltips # - header.autohide # - navigation.expand - navigation.footer - navigation.indexes # - navigation.instant # - navigation.instant.prefetch # - navigation.instant.progress # - navigation.prune # - navigation.sections # - navigation.tabs # - navigation.tabs.sticky - navigation.top - navigation.tracking - search.highlight - search.share - search.suggest - toc.follow # - toc.integrate palette: # Palette toggle for automatic mode - media: "(prefers-color-scheme)" toggle: icon: material/brightness-auto name: Switch to light mode primary: white accent: blue # Palette toggle for light mode - media: "(prefers-color-scheme: light)" scheme: default toggle: icon: material/brightness-7 name: Switch to dark mode primary: white accent: blue # Palette toggle for dark mode - media: "(prefers-color-scheme: dark)" scheme: slate toggle: icon: material/brightness-4 name: Switch to system preference primary: black accent: blue font: text: Roboto code: Roboto Mono logo: logo extra_css: - stylesheets/extra.css markdown_extensions: - pymdownx.blocks.admonition - markdown.extensions.codehilite: guess_lang: false - pymdownx.highlight: anchor_linenums: true line_spans: __span pygments_lang_class: true - pymdownx.inlinehilite - pymdownx.snippets - pymdownx.superfences - pymdownx.blocks.details plugins: - search - git-revision-date-localized: enable_creation_date: true type: date fallback_to_build_date: true nav: - Welcome: index.md - Setup: - Install the extension: setup/installation.md - Cursor IDE workaround: setup/cursor_installation_workaround.md - Required config: - dbt Core: setup/reqdConfig.md - dbt Cloud: setup/reqdConfigCloud.md - dbt Fusion: setup/reqdConfigFusion.md - Optional config: setup/optConfig.md - All configurations: setup/configuration.md - SSO: setup/sso.md - FAQ: setup/faq.md - Develop: - Autocomplete and go to definition: develop/autocomplete.md - Click to build parent/child models: develop/clicktorun.md - Preview compiled code (SQL): develop/compiledCode.md - Generate dbt model from source: develop/genmodelSource.md - Generate dbt model from SQL: develop/genmodelSQL.md - SQL validation: test/sqlvalidation.md - Query explanation: develop/explanation.md - Update dbt model using natural language: develop/updatemodel.md - Translate SQL queries (dialects): develop/translateSQL.md - Test: - Preview query results: test/queryResults.md - Preview CTEs: test/runctes.md - Run ad hoc query: test/adhocquery.md - SQL Visualizer: test/sqlvisualizer.md - Generate and edit tests: test/writetests.md - Run tests: test/runtests.md - Column Lineage: test/lineage.md - Defer to prod: test/defertoprod.md - Document: - Write documentation: document/write.md - Generate documentation: document/generatedoc.md - Support for doc blocks: document/docblocks.md - Collaborate: - Project Governance: govern/governance.md - Notebooks for ad-hoc analysis: govern/notebooks.md - Collaborate via IDE & UI: govern/collaboration.md - Multi-project Support with dbt-loom: govern/multiproject.md - Query Bookmarks and History: govern/querybookmarks.md - Discover: - Setup UI for docs & lineage: discover/setupui.md - Search and view docs: discover/viewdocs.md - Column lineage with Xformations: discover/viewlineage.md - Utilities: - Big Query cost estimator: test/bigquerycost.md - Logs force tailing: test/utilities.md - AI Teammates: - Introduction: teammates/introduction.md - Coach & Personalize: teammates/coach.md - Altimate Code: teammates/altimate-code.md - Altimate LLM Gateway: arch/llm-gateway.md - Datamates: https://datamates-docs.myaltimate.com/ - Studio (Beta): studio.md - Support & FAQ: - Troubleshooting: troubleshooting.md - Security FAQ: arch/faq.md - Pricing FAQ: arch/pricingfaq.md extra: status: new: Recently added analytics: provider: google property: G-LXRSS3VK5N feedback: title: Was this page helpful? ratings: - icon: material/emoticon-happy-outline name: This page was helpful data: 1 note: >- Thanks for your feedback! - icon: material/emoticon-sad-outline name: This page could be improved data: 0 note: >- Thanks for your feedback! Help us improve this page by using our feedback form. consent: title: Cookie consent description: >- We use cookies to recognize your repeated visits and preferences, as well as to measure the effectiveness of our documentation and whether users find what they're searching for. With your consent, you're helping us to make our documentation better. actions: - accept - reject copyright: > Copyright © 2022 - 2026 Altimate Inc – Change cookie settings copyright: > Copyright © 2022 - 2026 Altimate Inc – Change cookie settings repo_url: https://github.com/AltimateAI/vscode-dbt-power-user edit_uri: edit/master/docs/ ================================================ FILE: documentation/readme.md ================================================ ### Local development - Install required dependencies: `pip install --requirement requirements.txt` - Run mkdocs server: `mkdocs serve` ================================================ FILE: documentation/requirements.txt ================================================ mkdocs>=1.6.1 mkdocs-material>=9.5.43 mkdocs-git-revision-date-localized-plugin>=1.3.0 ================================================ FILE: jest.config.js ================================================ module.exports = { preset: "ts-jest", testEnvironment: "node", roots: ["/src"], testMatch: ["**/*.test.ts"], testPathIgnorePatterns: ["/node_modules/", "/src/test/integration/"], transform: { "^.+\\.tsx?$": ["ts-jest", { tsconfig: "tsconfig.json" }], }, moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], setupFilesAfterEnv: ["/src/test/setup.ts"], reporters: ["default", ["summary", { summaryThreshold: 1 }]], collectCoverageFrom: [ "src/**/*.{ts,tsx}", "!src/test/**", "!**/node_modules/**", ], coverageDirectory: "coverage", moduleNameMapper: { "^vscode$": "/src/test/mock/vscode.ts", "^@lib$": "/src/test/mock/lib.ts", "^node-fetch$": "/src/test/mock/node-fetch.ts", // Development: use local TypeScript source (same as webpack and tsconfig) // "^@altimateai/dbt-integration$": // "/../altimate-dbt-integration/src/index.ts", // Production: use npm package (commented out for development) "^@altimateai/dbt-integration$": "@altimateai/dbt-integration", "^@extension$": "/src/modules.ts", }, }; ================================================ FILE: monitoring/README.md ================================================ # dbt Power User — Error Analysis Dashboard A local Flask web app that queries the `dbt-power-user-telemetry-staging` Application Insights instance and visualises error patterns across all extension users. ## Prerequisites - Python 3.9+ - Azure CLI: `brew install azure-cli` - Logged in to Azure: `az login` - Access to the `altimate-staging` resource group ## Running Locally ```bash cd monitoring pip install -r requirements.txt python app.py ``` Open **http://localhost:5050** in your browser. > Note: macOS reserves port 5000 for AirPlay. The app uses port **5050** by default. ## Dashboard Sections | # | Section | What it shows | | --- | ------------------------ | ------------------------------------------------------------------------------------- | | 1 | **Summary** | Total errors, unique error types, affected instances in the selected time window | | 2 | **Error Trend** | Line chart — errors per hour (≤2 days) or per day (>2 days) | | 3 | **Top Errors** | Horizontal bar chart of top 10 + full table of top 15 with integration mode breakdown | | 4 | **By Integration Mode** | Doughnut chart — core vs cloud vs fusion vs corecommand | | 5 | **By OS / Architecture** | Bar chart — darwin/win32/linux × x64/arm64 | | 6 | **By Extension Version** | Bar chart — error count per version (regression detection) | | 7 | **Unhandled Errors** | Table — `unhandlederror` events grouped by message pattern | | 8 | **Error Details** | Full paginated table (500 rows, 50/page) with expandable stack traces | ## Filters All sections respond to the three filters in the header bar: | Filter | Default | Options | | --------------------- | ----------- | --------------------------------------------- | | **Days** | Last 7 days | 24h, 7d, 14d, 30d | | **Integration Mode** | All | All, core, cloud, fusion, corecommand | | **Extension Version** | All | All + live-fetched versions from App Insights | Click **↺ Refresh** to re-fetch all data without changing filters. ## App Insights Resource | Field | Value | | -------------- | -------------------------------------- | | Resource name | `dbt-power-user-telemetry-staging` | | Resource group | `altimate-staging` | | Subscription | `6ff315ea-c5a6-43fc-aabf-7f1bf1287582` | The backend queries the `customEvents` table. Error events are identified by: - `name` containing `"Error"` or `"error"`, **or** - `name == "unhandlederror"` (uncaught exceptions from the telemetry library) ## API Endpoints All endpoints accept `days`, `mode`, and `version` query parameters: | Endpoint | Description | | ------------------------------ | ---------------------------------------------- | | `GET /api/summary` | Total errors, unique types, affected instances | | `GET /api/trend` | Errors per time bin (hourly or daily) | | `GET /api/top-errors?limit=15` | Top N error types with counts and modes | | `GET /api/by-mode` | Count by dbt integration mode | | `GET /api/by-platform` | Count by OS / architecture | | `GET /api/by-version` | Count by extension version | | `GET /api/unhandled` | Unhandled error message groups | | `GET /api/details?limit=500` | Raw error rows with stack traces | | `GET /api/versions` | Distinct extension versions (for dropdown) | ## Telemetry Source Errors are logged from the extension via `src/telemetry/index.ts` using `sendTelemetryError(eventName, error, properties)`. The TelemetryService automatically attaches `dbtIntegrationMode`, `instanceName`, `ide`, and `localMode` to every event, and strips secrets from stack traces. Named error events are enumerated in `src/telemetry/events.ts`. ================================================ FILE: monitoring/app.py ================================================ """ dbt Power User — Error Analysis Dashboard Flask backend that queries Azure Application Insights telemetry. Authentication: uses AzureCliCredential (requires `az login`). """ from datetime import timedelta from flask import Flask, jsonify, render_template, request from azure.identity import AzureCliCredential from azure.monitor.query import LogsQueryClient, LogsQueryStatus app = Flask(__name__) RESOURCE_ID = ( "/subscriptions/6ff315ea-c5a6-43fc-aabf-7f1bf1287582" "/resourceGroups/altimate-staging" "/providers/microsoft.insights/components/dbt-power-user-telemetry-staging" ) ERROR_FILTER = """(name contains "Error" or name contains "error" or name == "unhandlederror")""" SHORT_NAME_KQL = 'iif(name contains "/", tostring(split(name, "/", 1)[0]), name)' VALID_MODES = {"All", "core", "cloud", "fusion", "corecommand"} _client: LogsQueryClient | None = None def get_client() -> LogsQueryClient: global _client if _client is None: _client = LogsQueryClient(AzureCliCredential()) return _client def run_query(kql: str, days: int) -> list[dict]: """Execute a KQL query against App Insights and return rows as list of dicts.""" response = get_client().query_resource( RESOURCE_ID, kql, timespan=timedelta(days=days), ) if response.status == LogsQueryStatus.PARTIAL: table = response.partial_data[0] elif response.status == LogsQueryStatus.SUCCESS: table = response.tables[0] else: raise RuntimeError(f"Query failed: {response.partial_error}") columns = list(table.columns) rows = [] for row in table.rows: record = {} for col, val in zip(columns, row): if hasattr(val, "isoformat"): val = val.isoformat() record[col] = val rows.append(record) return rows def dim_filter(key: str, value: str) -> str: """KQL fragment that filters on a customDimensions key; no-op when value is 'All'.""" if value == "All": return "" return f'| where tostring(customDimensions["{key}"]) == "{value}"' def get_params() -> tuple[int, str, str]: days = int(request.args.get("days", 7)) mode = request.args.get("mode", "All") version = request.args.get("version", "All") if mode not in VALID_MODES: mode = "All" return days, mode, version @app.route("/") def index(): return render_template("index.html") @app.route("/api/versions") def api_versions(): days, _, _ = get_params() kql = """ customEvents | extend extVersion = tostring(customDimensions["common.extversion"]) | where isnotempty(extVersion) and extVersion != "undefined" | summarize by extVersion | order by extVersion desc | project extVersion """ rows = run_query(kql, days) return jsonify([r["extVersion"] for r in rows]) @app.route("/api/summary") def api_summary(): days, mode, version = get_params() kql = f""" customEvents | where {ERROR_FILTER} {dim_filter("dbtIntegrationMode", mode)} {dim_filter("common.extversion", version)} | extend instanceName = tostring(customDimensions["instanceName"]) | extend shortName = {SHORT_NAME_KQL} | summarize TotalErrors = count(), UniqueTypes = dcount(shortName), AffectedInstances = dcountif(instanceName, isnotempty(instanceName) and instanceName != "undefined" and instanceName != "") """ rows = run_query(kql, days) if rows: r = rows[0] return jsonify({ "totalErrors": r.get("TotalErrors", 0), "uniqueTypes": r.get("UniqueTypes", 0), "affectedInstances": r.get("AffectedInstances", 0), }) return jsonify({"totalErrors": 0, "uniqueTypes": 0, "affectedInstances": 0}) @app.route("/api/trend") def api_trend(): days, mode, version = get_params() grain = "1h" if days <= 2 else "1d" kql = f""" customEvents | where {ERROR_FILTER} {dim_filter("dbtIntegrationMode", mode)} {dim_filter("common.extversion", version)} | summarize Count = count() by bin(timestamp, {grain}) | order by timestamp asc """ rows = run_query(kql, days) return jsonify([{"timestamp": r["timestamp"], "count": r["Count"]} for r in rows]) @app.route("/api/top-errors") def api_top_errors(): days, mode, version = get_params() limit = int(request.args.get("limit", 15)) kql = f""" customEvents | where {ERROR_FILTER} {dim_filter("dbtIntegrationMode", mode)} {dim_filter("common.extversion", version)} | extend shortName = {SHORT_NAME_KQL} | extend integrationMode = tostring(customDimensions["dbtIntegrationMode"]) | summarize Count = count(), Modes = make_set(integrationMode, 10) by shortName | top {limit} by Count desc | extend ModeList = strcat_array(Modes, ", ") | project shortName, Count, ModeList | order by Count desc """ rows = run_query(kql, days) return jsonify([ {"name": r["shortName"], "count": r["Count"], "modes": r["ModeList"]} for r in rows ]) @app.route("/api/by-mode") def api_by_mode(): days, _, version = get_params() kql = f""" customEvents | where {ERROR_FILTER} {dim_filter("common.extversion", version)} | extend integrationMode = tostring(customDimensions["dbtIntegrationMode"]) | extend integrationMode = iif(isempty(integrationMode) or integrationMode == "undefined", "unknown", integrationMode) | summarize Count = count() by integrationMode | order by Count desc """ rows = run_query(kql, days) return jsonify([{"mode": r["integrationMode"], "count": r["Count"]} for r in rows]) @app.route("/api/by-platform") def api_by_platform(): days, mode, version = get_params() kql = f""" customEvents | where {ERROR_FILTER} {dim_filter("dbtIntegrationMode", mode)} {dim_filter("common.extversion", version)} | extend os = tostring(customDimensions["common.os"]) | extend arch = tostring(customDimensions["common.nodeArch"]) | extend os = iif(isempty(os) or os == "undefined", "unknown", os) | extend arch = iif(isempty(arch) or arch == "undefined", "unknown", arch) | extend platform = strcat(os, " / ", arch) | summarize Count = count() by platform | order by Count desc """ rows = run_query(kql, days) return jsonify([{"platform": r["platform"], "count": r["Count"]} for r in rows]) @app.route("/api/by-version") def api_by_version(): days, mode, _ = get_params() kql = f""" customEvents | where {ERROR_FILTER} {dim_filter("dbtIntegrationMode", mode)} | extend extVersion = tostring(customDimensions["common.extversion"]) | extend extVersion = iif(isempty(extVersion) or extVersion == "undefined", "unknown", extVersion) | summarize Count = count() by extVersion | order by Count desc """ rows = run_query(kql, days) return jsonify([{"version": r["extVersion"], "count": r["Count"]} for r in rows]) @app.route("/api/unhandled") def api_unhandled(): days, mode, version = get_params() kql = f""" customEvents | where name == "unhandlederror" {dim_filter("dbtIntegrationMode", mode)} {dim_filter("common.extversion", version)} | extend errorMessage = tostring(customDimensions["message"]), extVersion = tostring(customDimensions["common.extversion"]), integrationMode = tostring(customDimensions["dbtIntegrationMode"]) | extend messageGroup = case( errorMessage contains "Channel closed", "Channel closed (python-bridge)", errorMessage contains "ENOENT", "File not found (ENOENT)", errorMessage contains "ECONNREFUSED", "Connection refused (ECONNREFUSED)", errorMessage contains "ETIMEDOUT", "Connection timeout (ETIMEDOUT)", errorMessage contains "EPERM", "Permission denied (EPERM)", isempty(errorMessage) or errorMessage == "undefined", "(no message)", substring(errorMessage, 0, 80)) | summarize Count = count(), Versions = strcat_array(make_set(extVersion, 5), ", "), Modes = strcat_array(make_set(integrationMode, 5), ", ") by messageGroup | order by Count desc """ rows = run_query(kql, days) return jsonify([ {"group": r["messageGroup"], "count": r["Count"], "versions": r["Versions"], "modes": r["Modes"]} for r in rows ]) @app.route("/api/details") def api_details(): days, mode, version = get_params() limit = int(request.args.get("limit", 500)) kql = f""" customEvents | where {ERROR_FILTER} {dim_filter("dbtIntegrationMode", mode)} {dim_filter("common.extversion", version)} | extend shortName = {SHORT_NAME_KQL}, errorMessage = tostring(customDimensions["message"]), integrationMode = tostring(customDimensions["dbtIntegrationMode"]), extVersion = tostring(customDimensions["common.extversion"]), os = tostring(customDimensions["common.os"]), arch = tostring(customDimensions["common.nodeArch"]), ideApp = tostring(customDimensions["ide"]), stackTrace = tostring(customDimensions["stack"]) | project timestamp, shortName, errorMessage, integrationMode, extVersion, os, arch, ideApp, stackTrace | order by timestamp desc | take {limit} """ rows = run_query(kql, days) return jsonify([ { "timestamp": r["timestamp"], "name": r["shortName"], "message": r["errorMessage"], "mode": r["integrationMode"], "version": r["extVersion"], "os": r["os"], "arch": r["arch"], "ide": r["ideApp"], "stack": r["stackTrace"], } for r in rows ]) @app.route("/api/stack-analysis") def api_stack_analysis(): days, mode, version = get_params() kql = f""" customEvents | where {ERROR_FILTER} {dim_filter("dbtIntegrationMode", mode)} {dim_filter("common.extversion", version)} | extend stack = tostring(customDimensions["stack"]) | where isnotempty(stack) and stack != "undefined" | extend shortName = {SHORT_NAME_KQL} | extend integrationMode = tostring(customDimensions["dbtIntegrationMode"]) | extend extVersion = tostring(customDimensions["common.extversion"]) | extend RootCause = case( stack contains "no profile was specified for this dbt project", "No Profile Specified", stack contains "Could not find profile named", "Profile Not Found", stack contains "No module named 'dbt.adapters.", "dbt Adapters Module Missing", stack contains "No module named 'dbt'", "dbt Module Not Installed", stack contains "No module named", "Python Module Not Found", stack contains "dbt.exceptions.FailedToConnectError", "dbt: Failed to Connect", stack contains "dbt.exceptions.EnvVarMissingError", "dbt: Env Var Missing", stack contains "dbt.exceptions.UninstalledPackagesFoundError", "dbt: Uninstalled Packages", stack contains "dbt.exceptions.ParsingException", "dbt: Parsing Exception", stack contains "dbt.exceptions.ParsingError", "dbt: Parsing Error", stack contains "dbt.exceptions.CompilationException", "dbt: Compilation Exception", stack contains "dbt.exceptions.CompilationError", "dbt: Compilation Error", stack contains "dbt.exceptions.TargetNotFoundError", "dbt: Target Not Found", stack contains "dbt.exceptions.DbtProfileError", "dbt: Profile Error", stack contains "dbt.exceptions.DbtProjectError", "dbt: Project Error", stack contains "dbt.exceptions.DbtValidationError", "dbt: Validation Error", stack contains "dbt.exceptions.CaughtMacroErrorWithNodeError", "dbt: Caught Macro Error", stack contains "dbt.exceptions", "dbt: Generic Exception", stack contains "YAMLParseError", "YAML Parse Error", stack contains "DB::Exception", "Database Exception", stack contains "FileNotFoundError", "Python: File Not Found", stack contains "KeyError", "Python: Key Error", stack contains "ImportError", "Python: Import Error", stack contains "NotImplementedError", "Python: Not Implemented", stack contains "NameError:", "Python: Name Error", stack contains "RuntimeError: release unlocked lock", "Python: Unlocked Lock", stack contains "codec can't decode bytes", "Python: Codec Decode Error", stack contains "Python process closed with exit code", "Python Process Exited", stack contains "spawn ENOENT", "spawn: dbt Not Found (ENOENT)", stack contains "spawn EBADF", "spawn: Bad File Descriptor", stack contains "spawn ECONNRESET", "spawn: Connection Reset", stack contains "Error: ENOENT: no such file or directory, open", "ENOENT: File Not Found", stack contains "Cannot read properties of", "JS: Cannot Read Properties", stack contains "AbortError", "JS: Abort Error", stack contains "Channel closed", "python-bridge: Channel Closed", stack contains "not found: sqlfmt", "sqlfmt Not Found", stack contains "ENOTFOUND api.myaltimate.com", "Cannot Reach api.myaltimate.com", stack contains "FetchError", "JS: Fetch Error", stack contains "dbt found following issue: Compilation Error", "dbt CLI: Compilation Error", stack contains "DeprecationWarning", "Python: Deprecation Warning", stack contains "NotOpenSSLWarning", "Python: OpenSSL Warning", "Other / Unclassified") | summarize Count = count(), TopEvents = strcat_array(make_set(shortName, 5), ", "), TopModes = strcat_array(make_set(integrationMode, 4), ", "), TopVersions = strcat_array(make_set(extVersion, 4), ", ") by RootCause | order by Count desc | take 100 """ rows = run_query(kql, days) return jsonify([ { "rootCause": r["RootCause"], "count": r["Count"], "topEvents": r["TopEvents"], "topModes": r["TopModes"], "topVersions": r["TopVersions"], } for r in rows ]) if __name__ == "__main__": app.run(debug=True, port=5050) ================================================ FILE: monitoring/github_issues/app.py ================================================ """ GitHub Issues Dashboard Flask backend that fetches issues from the GitHub REST API with local file-based caching. Authentication: Set GITHUB_TOKEN env var for 5 000 req/hr (unauthenticated = 60 req/hr). AI analysis: Requires the `claude` CLI to be in PATH. Uses your existing Claude Code auth (Claude Code Max subscription works). No separate ANTHROPIC_API_KEY needed. Usage: cd monitoring/github_issues pip install -r requirements.txt GITHUB_TOKEN= python app.py # Open http://localhost:5051 """ import json import os import shutil import subprocess import time from datetime import datetime, timezone, timedelta from pathlib import Path import requests from flask import Flask, jsonify, render_template, request app = Flask(__name__) REPO = "AltimateAI/vscode-dbt-power-user" GITHUB_API = "https://api.github.com" CACHE_DIR = Path(__file__).parent / ".cache" ISSUES_CACHE = CACHE_DIR / "issues.json" AI_THEMES_CACHE = CACHE_DIR / "ai_themes.json" ISSUES_CACHE_TTL = 3600 # 1 hour AI_CACHE_TTL = 14400 # 4 hours # --------------------------------------------------------------------------- # GitHub data fetching + caching # --------------------------------------------------------------------------- def _github_headers() -> dict: token = os.environ.get("GITHUB_TOKEN") headers = {"Accept": "application/vnd.github.v3+json"} if token: headers["Authorization"] = f"Bearer {token}" return headers def _fetch_all_issues() -> list[dict]: """Fetch every issue (not PR) from GitHub with full pagination. GitHub's REST Issues API caps pagination at 1000 results (page 10 × per_page 100). A 422 on page > 10 means we've hit that ceiling — stop gracefully. """ issues: list[dict] = [] page = 1 while True: resp = requests.get( f"{GITHUB_API}/repos/{REPO}/issues", headers=_github_headers(), params={"state": "all", "per_page": 100, "page": page}, timeout=30, ) if resp.status_code == 422: # GitHub caps the Issues API at 1000 results; stop here print(f"[github] page {page} returned 422 (API cap reached), stopping") break if resp.status_code == 403: remaining = resp.headers.get("X-RateLimit-Remaining", "?") reset = resp.headers.get("X-RateLimit-Reset", "?") raise RuntimeError( f"GitHub API rate limit exceeded (remaining={remaining}, reset={reset}). " "Set the GITHUB_TOKEN environment variable to increase the limit to 5000 req/hr: " " GITHUB_TOKEN= python app.py" ) resp.raise_for_status() batch = resp.json() if not batch: break # GitHub's issues endpoint returns PRs too — exclude them issues.extend(i for i in batch if "pull_request" not in i) if len(batch) < 100: break page += 1 print(f"[github] fetched {len(issues)} issues across {page} page(s)") return issues def load_issues(force: bool = False) -> list[dict]: """Return cached issues, refreshing from GitHub if cache is stale.""" if not force and ISSUES_CACHE.exists(): data = json.loads(ISSUES_CACHE.read_text()) if time.time() - data["fetched_at"] < ISSUES_CACHE_TTL: return data["issues"] CACHE_DIR.mkdir(exist_ok=True) issues = _fetch_all_issues() ISSUES_CACHE.write_text(json.dumps({"fetched_at": time.time(), "issues": issues})) return issues # --------------------------------------------------------------------------- # Date helpers # --------------------------------------------------------------------------- def _parse(s: str | None) -> datetime | None: if not s: return None return datetime.fromisoformat(s.replace("Z", "+00:00")) def _now() -> datetime: return datetime.now(tz=timezone.utc) # --------------------------------------------------------------------------- # Filter helpers # --------------------------------------------------------------------------- def _get_params() -> tuple[int, str, str, str]: days = int(request.args.get("days", 90)) state = request.args.get("state", "all") # open | closed | all label = request.args.get("label", "All") milestone = request.args.get("milestone", "All") return days, state, label, milestone def _apply_filters( issues: list[dict], state: str, label: str, milestone: str, ) -> list[dict]: out = issues if state != "all": out = [i for i in out if i["state"] == state] if label != "All": out = [i for i in out if any(lb["name"] == label for lb in i.get("labels", []))] if milestone != "All": out = [i for i in out if i.get("milestone") and i["milestone"]["title"] == milestone] return out # --------------------------------------------------------------------------- # Routes # --------------------------------------------------------------------------- @app.route("/") def index(): return render_template("index.html") @app.route("/api/refresh", methods=["POST"]) def api_refresh(): load_issues(force=True) if AI_THEMES_CACHE.exists(): AI_THEMES_CACHE.unlink() return jsonify({"ok": True}) @app.route("/api/filter-options") def api_filter_options(): issues = load_issues() labels: dict[str, str] = {} milestones: set[str] = set() for issue in issues: for lb in issue.get("labels", []): labels.setdefault(lb["name"], lb.get("color", "888888")) ms = issue.get("milestone") if ms: milestones.add(ms["title"]) return jsonify({ "labels": [{"name": n, "color": c} for n, c in sorted(labels.items())], "milestones": sorted(milestones), }) @app.route("/api/summary") def api_summary(): days, state, label, milestone = _get_params() issues = load_issues() cutoff = _now() - timedelta(days=days) # "Open count" always reflects current open state (filtered by label/milestone only) open_issues = _apply_filters(issues, "open", label, milestone) # "Opened / closed in period" respects all filters all_filtered = _apply_filters(issues, state if state != "all" else "all", label, milestone) opened = [i for i in all_filtered if _parse(i["created_at"]) and _parse(i["created_at"]) >= cutoff] closed_in_period = [ i for i in all_filtered if i["state"] == "closed" and _parse(i.get("closed_at")) and _parse(i["closed_at"]) >= cutoff ] close_times = [] for i in closed_in_period: c = _parse(i["created_at"]) cl = _parse(i["closed_at"]) if c and cl: close_times.append((cl - c).total_seconds() / 86400) avg_close = round(sum(close_times) / len(close_times), 1) if close_times else None no_response = [ i for i in open_issues if i.get("comments", 0) == 0 and not i.get("assignee") ] return jsonify({ "openCount": len(open_issues), "openedInPeriod": len(opened), "closedInPeriod": len(closed_in_period), "avgDaysToClose": avg_close, "noResponseCount": len(no_response), }) @app.route("/api/trend") def api_trend(): days, _, label, milestone = _get_params() issues = load_issues() cutoff = _now() - timedelta(days=days) filtered = _apply_filters(issues, "all", label, milestone) use_weeks = days > 60 grain = timedelta(days=7 if use_weeks else 1) # Build ordered bucket keys buckets: dict[str, dict] = {} ptr = cutoff.replace(hour=0, minute=0, second=0, microsecond=0) end = _now().replace(hour=0, minute=0, second=0, microsecond=0) while ptr <= end: key = ptr.strftime("%Y-%m-%d") buckets[key] = {"date": key, "opened": 0, "closed": 0} ptr += grain def _bucket(dt: datetime) -> str: if use_weeks: delta_days = max(0, (dt - cutoff).days) bucket_start = cutoff + timedelta(days=(delta_days // 7) * 7) bucket_start = bucket_start.replace(hour=0, minute=0, second=0, microsecond=0) else: bucket_start = dt.replace(hour=0, minute=0, second=0, microsecond=0) return bucket_start.strftime("%Y-%m-%d") for issue in filtered: created = _parse(issue["created_at"]) if created and created >= cutoff: k = _bucket(created) if k in buckets: buckets[k]["opened"] += 1 if issue["state"] == "closed": closed_dt = _parse(issue.get("closed_at")) if closed_dt and closed_dt >= cutoff: k = _bucket(closed_dt) if k in buckets: buckets[k]["closed"] += 1 return jsonify(sorted(buckets.values(), key=lambda x: x["date"])) @app.route("/api/labels") def api_labels(): _, state, _, milestone = _get_params() issues = load_issues() # Show label distribution across all labels (don't filter by label here) filtered = _apply_filters(issues, state, "All", milestone) counts: dict[str, dict] = {} for issue in filtered: for lb in issue.get("labels", []): name = lb["name"] counts.setdefault(name, {"name": name, "color": lb.get("color", "888888"), "count": 0}) counts[name]["count"] += 1 result = sorted(counts.values(), key=lambda x: x["count"], reverse=True)[:20] return jsonify(result) @app.route("/api/age-buckets") def api_age_buckets(): _, _, label, milestone = _get_params() issues = load_issues() open_issues = _apply_filters(issues, "open", label, milestone) buckets = { "< 7 days": 0, "7–30 days": 0, "1–3 months": 0, "3–6 months": 0, "> 6 months": 0, } now = _now() for issue in open_issues: created = _parse(issue["created_at"]) if not created: continue age = (now - created).days if age < 7: buckets["< 7 days"] += 1 elif age < 30: buckets["7–30 days"] += 1 elif age < 90: buckets["1–3 months"] += 1 elif age < 180: buckets["3–6 months"] += 1 else: buckets["> 6 months"] += 1 return jsonify([{"bucket": k, "count": v} for k, v in buckets.items()]) @app.route("/api/time-to-close") def api_time_to_close(): days, _, label, milestone = _get_params() issues = load_issues() cutoff = _now() - timedelta(days=days) closed = [ i for i in issues if i["state"] == "closed" and _parse(i.get("closed_at")) is not None and _parse(i["closed_at"]) >= cutoff ] closed = _apply_filters(closed, "closed", label, milestone) buckets = { "< 1 day": 0, "1–7 days": 0, "1–4 weeks": 0, "1–3 months": 0, "> 3 months": 0, } for issue in closed: created = _parse(issue["created_at"]) closed_at = _parse(issue["closed_at"]) if not created or not closed_at: continue age = (closed_at - created).total_seconds() / 86400 if age < 1: buckets["< 1 day"] += 1 elif age < 7: buckets["1–7 days"] += 1 elif age < 28: buckets["1–4 weeks"] += 1 elif age < 90: buckets["1–3 months"] += 1 else: buckets["> 3 months"] += 1 return jsonify([{"bucket": k, "count": v} for k, v in buckets.items()]) @app.route("/api/milestones") def api_milestones(): _, state, label, _ = _get_params() issues = load_issues() if label != "All": issues = [i for i in issues if any(lb["name"] == label for lb in i.get("labels", []))] if state != "all": issues = [i for i in issues if i["state"] == state] ms_data: dict[str, dict] = {} for issue in issues: ms = issue.get("milestone") if not ms: continue title = ms["title"] ms_data.setdefault(title, { "title": title, "url": ms.get("html_url", ""), "open": 0, "closed": 0, }) ms_data[title][issue["state"]] = ms_data[title].get(issue["state"], 0) + 1 result = sorted(ms_data.values(), key=lambda x: x["open"] + x["closed"], reverse=True) return jsonify(result) @app.route("/api/attention") def api_attention(): _, _, label, milestone = _get_params() issues = load_issues() open_issues = _apply_filters(issues, "open", label, milestone) now = _now() stale_age_cutoff = now - timedelta(days=90) stale_activity_cutoff = now - timedelta(days=30) def _fmt(i: dict) -> dict: created = _parse(i["created_at"]) age_days = (now - created).days if created else 0 return { "number": i["number"], "title": i["title"], "url": i["html_url"], "age_days": age_days, "comments": i.get("comments", 0), "labels": [lb["name"] for lb in i.get("labels", [])], "assignee": i["assignee"]["login"] if i.get("assignee") else None, "updated_at": i.get("updated_at"), } no_response = sorted( [_fmt(i) for i in open_issues if i.get("comments", 0) == 0 and not i.get("assignee")], key=lambda x: x["age_days"], reverse=True, )[:30] no_labels = sorted( [_fmt(i) for i in open_issues if not i.get("labels")], key=lambda x: x["age_days"], reverse=True, )[:30] stale = sorted( [ _fmt(i) for i in open_issues if _parse(i["created_at"]) and _parse(i["created_at"]) <= stale_age_cutoff and _parse(i.get("updated_at") or i["created_at"]) <= stale_activity_cutoff ], key=lambda x: x["age_days"], reverse=True, )[:30] return jsonify({"noResponse": no_response, "noLabels": no_labels, "stale": stale}) @app.route("/api/details") def api_details(): _, state, label, milestone = _get_params() page = int(request.args.get("page", 1)) per_page = int(request.args.get("per_page", 50)) issues = load_issues() filtered = _apply_filters(issues, state, label, milestone) # Sort by most recently updated filtered = sorted( filtered, key=lambda i: i.get("updated_at") or i["created_at"], reverse=True, ) now = _now() total = len(filtered) start = (page - 1) * per_page page_issues = filtered[start : start + per_page] def _fmt(i: dict) -> dict: created = _parse(i["created_at"]) age_days = (now - created).days if created else 0 return { "number": i["number"], "title": i["title"], "url": i["html_url"], "state": i["state"], "created_at": i["created_at"], "age_days": age_days, "comments": i.get("comments", 0), "labels": [ {"name": lb["name"], "color": lb.get("color", "888888")} for lb in i.get("labels", []) ], "assignee": i["assignee"]["login"] if i.get("assignee") else None, "milestone": i["milestone"]["title"] if i.get("milestone") else None, } return jsonify({ "issues": [_fmt(i) for i in page_issues], "total": total, "page": page, "per_page": per_page, "pages": max(1, (total + per_page - 1) // per_page), }) @app.route("/api/ai-themes") def api_ai_themes(): if not shutil.which("claude"): return jsonify({"enabled": False, "reason": "claude CLI not found in PATH"}) issues = load_issues() open_issues = [i for i in issues if i["state"] == "open"] issue_count = len(open_issues) # Return cached result if still fresh and issue count matches if AI_THEMES_CACHE.exists(): cached = json.loads(AI_THEMES_CACHE.read_text()) if ( time.time() - cached["fetched_at"] < AI_CACHE_TTL and cached.get("issue_count") == issue_count ): return jsonify({"enabled": True, "themes": cached["themes"]}) # Build the prompt — cap at 80 issues to keep prompt size manageable sample = open_issues[:80] # Compact plain-text list is smaller than JSON and easier for the model issue_lines = "\n".join(f"#{i['number']}: {i['title']}" for i in sample) prompt = ( f"You are analyzing GitHub issues for the 'vscode-dbt-power-user' VS Code extension " f"(a dbt development tool by Altimate AI).\n\n" f"Here are {len(sample)} open issues:\n" f"{issue_lines}\n\n" f"Group these issues into 8-12 meaningful themes based on their titles. " f"Each theme should represent a distinct area of concern (bug category, feature area, etc.).\n\n" f"Respond with ONLY valid JSON — no markdown fences, no explanation text. " f"The output must be a JSON array where each element has exactly these keys:\n" f' "theme": short theme name (3-6 words),\n' f' "description": one sentence describing what these issues share,\n' f' "issue_numbers": array of integer issue numbers that belong to this theme.\n\n' f"Order themes from most issues to fewest." ) try: result = subprocess.run( ["claude", "--model", "haiku", "-p", prompt], capture_output=True, text=True, timeout=300, ) except subprocess.TimeoutExpired: return jsonify({"enabled": True, "error": "Claude CLI timed out after 300s", "themes": []}) if result.returncode != 0: err = (result.stderr or result.stdout)[:300] return jsonify({"enabled": True, "error": f"claude CLI exited {result.returncode}: {err}", "themes": []}) output = result.stdout.strip() # Strip markdown code fences if Claude wrapped the JSON anyway if output.startswith("```"): lines = output.splitlines() # Drop first line (```json or ```) and last line (```) inner = lines[1:-1] if lines[-1].strip() == "```" else lines[1:] output = "\n".join(inner).strip() try: themes = json.loads(output) except json.JSONDecodeError as exc: return jsonify({"enabled": True, "error": f"JSON parse error: {exc}", "themes": [], "raw": output[:500]}) # Enrich each theme with full issue details (title + URL) issue_map = {i["number"]: i["title"] for i in sample} # sample is still the list of dicts for theme in themes: numbers = theme.get("issue_numbers", []) theme["issues"] = [ { "number": n, "title": issue_map.get(n, ""), "url": f"https://github.com/{REPO}/issues/{n}", } for n in numbers[:10] # cap at 10 representative issues per theme ] CACHE_DIR.mkdir(exist_ok=True) AI_THEMES_CACHE.write_text(json.dumps({ "fetched_at": time.time(), "issue_count": issue_count, "themes": themes, })) return jsonify({"enabled": True, "themes": themes}) if __name__ == "__main__": app.run(debug=True, port=5051) ================================================ FILE: monitoring/github_issues/requirements.txt ================================================ flask>=3.0.0 requests>=2.31.0 ================================================ FILE: monitoring/github_issues/templates/index.html ================================================ GitHub Issues — dbt Power User

GitHub Issues Dashboard

Open Issues
currently open
Opened in Period
new issues
Closed in Period
resolved
Avg Days to Close
mean close time
No Response
open, 0 comments, unassigned

Issue Trend

Top Labels

Open Issue Age Distribution

Time to Close Distribution

Milestone Progress

Milestone Open Closed Progress
Loading…

Issues Needing Attention

# Title Age Labels
# Title Age Comments
# Title Age Labels
Loading…

AI Theme Clusters (Claude analysis of open issues)

Analyzing with Claude… this may take 30–60 s on first run

Issue Details

# Title State Labels Opened Age Comments Assignee Milestone
Loading…
================================================ FILE: monitoring/requirements.txt ================================================ flask>=3.0.0 azure-identity>=1.15.0 azure-monitor-query>=1.3.0 ================================================ FILE: monitoring/templates/index.html ================================================ dbt Power User — Error Dashboard

dbt Power User — Error Dashboard

1 — Summary
2 — Error Trend Over Time
3 — Top Errors
4–6 — Breakdowns
By Integration Mode
By OS / Architecture
By Extension Version
7 — Unhandled Errors
8 — Error Details
9 — Stack Trace Root Cause Analysis
================================================ FILE: package.json ================================================ { "name": "vscode-dbt-power-user", "displayName": "Power User for dbt", "description": "a.k.a. dbt power user makes vscode work seamlessly with dbt™ core and dbt™ cloud: auto-complete, preview, column lineage, AI docs generation, health checks, cost estimation etc", "icon": "media/images/dbt.png", "publisher": "innoverio", "license": "MIT", "homepage": "https://www.altimate.ai/vscode", "repository": { "type": "git", "url": "https://github.com/AltimateAI/vscode-dbt-power-user.git" }, "bugs": { "url": "https://github.com/AltimateAI/vscode-dbt-power-user/issues" }, "version": "0.60.6", "engines": { "vscode": "^1.95.0" }, "capabilities": { "hoverProvider": true }, "categories": [ "Other", "Programming Languages", "Snippets", "Data Science", "Formatters", "Testing" ], "keywords": [ "dbt", "sql", "jinja-sql", "dbt power user" ], "activationEvents": [ "workspaceContains:**/dbt_project.yml" ], "main": "./dist/extension", "contributes": { "notebookRenderer": [ { "id": "datapilot-notebook-perspective-renderer", "displayName": "Github Issues Notebook Renderer", "entrypoint": "./webview_panels/dist/assets/renderer.js", "mimeTypes": [ "application/perspective-json" ] } ], "notebooks": [ { "type": "datapilot-notebook", "displayName": "Datapilot Notebook", "selector": [ { "filenamePattern": "*.notebook" } ] } ], "snippets": [ { "language": "jinja-sql", "path": "./snippets/snippets_sql.json" }, { "language": "jinja-yaml", "path": "./snippets/snippets_yaml.json" }, { "language": "jinja-md", "path": "./snippets/snippets_markdown.json" } ], "configuration": [ { "title": "Altimate AI", "properties": { "dbt.altimateAiKey": { "type": "string", "displayName": "Altimate AI API Key", "markdownDescription": "Needed for features that require backend support. Sign up for a free Altimate AI account [here](https://app.myaltimate.com/register). API Key can be copied from Altimate AI UI (navigate to Settings->API Key)" }, "dbt.altimateUrl": { "type": "string", "displayName": "Altimate API endpoint", "description": "Select Altimate API endpoint", "enum": [ "https://api.myaltimate.com", "https://api.getaltimate.com" ], "enumDescriptions": [ "Community, Pro or Team Plan", "Enterprise Plan" ], "default": "https://api.myaltimate.com" }, "dbt.altimateInstanceName": { "type": "string", "displayName": "Altimate AI Instance Name", "markdownDescription": "Needed for features that require backend support. Sign up for a free Altimate AI account [here](https://app.myaltimate.com/register). If your Altimate AI instance URL is https://betatest.app.myaltimate.com/, The instance name is betatest" }, "dbt.enableMcpDataSourceQueryTools": { "type": "boolean", "description": "Enable data source query tools (EXECUTE_SQL_WITH_LIMIT and GET_COLUMN_VALUES)", "default": false }, "dbt.mcpSslCertKeyPath": { "type": "string", "description": "Path to the SSL certificate key file for MCP server", "displayName": "MCP Server SSL Certificate Key Path", "default": "" }, "dbt.mcpSslCertPath": { "type": "string", "description": "Path to the SSL certificate file for MCP server", "displayName": "MCP Server SSL Certificate Path", "default": "" }, "dbt.enableNewLineagePanel": { "type": "boolean", "description": "Enable the new lineage panel in dbt." }, "dbt.enableCollaboration": { "type": "boolean", "description": "Enable dbt docs collaboration.", "default": true }, "dbt.disableQueryHistory": { "type": "boolean", "description": "Disable Query history and bookmarks", "default": false }, "dbt.enableNotebooks": { "type": "boolean", "description": "Enable Datapilot notebooks feature", "default": true }, "dbt.lineage.showSelectEdges": { "type": "boolean", "description": "Show select edges in lineage panel.", "default": true }, "dbt.lineage.showNonSelectEdges": { "type": "boolean", "description": "Show non-select edges in lineage panel.", "default": false }, "dbt.lineage.defaultExpansion": { "type": "number", "description": "By default how much lineage should expand.", "default": 1 }, "dbt.installDepsOnProjectInitialization": { "type": "boolean", "description": "Install dbt deps on project initialization", "default": true } } }, { "title": "dbt Power User", "properties": { "dbt.unquotedCaseInsensitiveIdentifierRegex": { "type": "string", "markdownDescription": "Regex to identify unquoted identifiers. Example: `^([_A-Z]+[_A-Z0-9$]*)$`" }, "dbt.dbtIntegration": { "type": "string", "enum": [ "core", "cloud", "corecommand", "fusion" ], "default": "core", "description": "Select dbt core or dbt cloud" }, "dbt.dbtPythonPathOverride": { "type": "string", "description": "Path to a Python executable. Use this when the Python extension selects a different interpreter than the one where dbt is installed (e.g. dbt works in your terminal but not in the extension). You can set this manually or run the 'Detect Python from terminal' command to auto-detect it." }, "dbt.dbtCustomRunnerImport": { "type": "string", "description": "Python import string to import custom dbt runner. (Only supported for dbt core)", "default": "from dbt.cli.main import dbtRunner" }, "dbt.allowListFolders": { "type": "array", "items": { "type": "string" }, "default": [], "description": "Define workspace-relative paths to include. Only projects within these folders will be built. If empty, no filtering is applied. The path should be relative to the workspace. Example: [folder1, folder2]" }, "dbt.runModelCommandAdditionalParams": { "type": "array", "items": { "type": "string" }, "default": [], "description": "Add additional params to the dbt run model command. Ex: --select model1 --target prod needs to be added as four different entries \n--select \nmodel1 \n--target \nprod " }, "dbt.buildModelCommandAdditionalParams": { "type": "array", "items": { "type": "string" }, "default": [], "description": "Add additional params to the dbt build model command. Ex: --select model1 --target prod needs to be added as four different entries \n--select \nmodel1 \n--target \nprod " }, "dbt.queryLimit": { "type": "integer", "description": "The maximum number of rows the `Preview SQL Query` command returns.", "default": 500, "minimum": 1 }, "dbt.perspectiveTheme": { "type": "string", "description": "Theme for perspective viewer in query results panel", "default": "Vintage", "enum": [ "Vintage", "Pro Light", "Pro Dark", "Vaporwave", "Solarized", "Solarized Dark", "Monokai" ] }, "dbt.conversationsPollingInterval": { "type": "integer", "description": "Polling interval to fetch latest conversations (in seconds)", "default": 900, "minimum": 30 }, "dbt.queryTemplate": { "type": "string", "description": "Override the default query template that is used by the `Preview SQL Query` command. Use this if your database has special syntax to limit query results", "default": "select * from ({query}\n) as query limit {limit}" }, "dbt.queryScale": { "type": "string", "description": "Override the default scale of the result table that is used by the `Preview SQL Query` command.", "default": 1 }, "dbt.fileNameTemplateGenerateModel": { "type": "string", "enum": [ "{prefix}_{sourceName}_{tableName}", "{prefix}_{sourceName}__{tableName}", "{prefix}_{tableName}", "{tableName}" ], "default": "{prefix}_{sourceName}_{tableName}", "description": "Set the naming template that is used when generating a model from a source yml file." }, "dbt.prefixGenerateModel": { "type": "string", "default": "base", "description": "Set the prefix that is used when generating a model from a source yml file." }, "dbt.sqlFmtPath": { "type": "string", "description": "Path to `sqlfmt`. If not set, it will look on your PATH" }, "dbt.sqlFmtAdditionalParams": { "type": "array", "items": { "type": "string" }, "default": [], "description": "Add additional params to `sqlfmt`" }, "dbt.disableDepthsCalculation": { "type": "boolean", "description": "Disable DAG depth calculation and decoration", "default": false }, "dbt.deferConfigPerProject": { "type": "object", "description": "Run subset of models without building their parent models", "scope": "resource", "patternProperties": { ".*": { "type": "object", "properties": { "deferToProduction": { "type": "boolean", "description": "Run subset of models without building their parent models", "default": false }, "manifestPathForDeferral": { "type": "string", "description": "Manifest path to defer the parent models", "default": "" }, "favorState": { "type": "boolean", "description": "If a given model exists in both the current environment and the defined defer state, turn this flag on to use the latter always", "default": false } } } } } } } ], "customEditors": [ { "viewType": "dbtPowerUser.Mcp", "displayName": "MCP Walkthrough", "selector": [ { "filenamePattern": "*.mcpwalkthrough" } ] } ], "viewsContainers": { "activitybar": [ { "id": "dbt_view", "title": "dbt Power User", "icon": "./media/images/dbt_icon.svg", "contextualTitle": "dbt Power User" }, { "id": "datapilot_view", "title": "DataPilot Chat", "icon": "./media/images/altimate.svg", "contextualTitle": "Altimate Data pilot" } ], "panel": [ { "id": "dbt_preview_results", "title": "Query Results", "icon": "./media/images/dbt_icon.svg" }, { "id": "lineage_view", "title": "Lineage", "icon": "./media/images/dbt_icon.svg" }, { "id": "docs_edit_view", "title": "Documentation Editor", "icon": "./media/images/dbt_icon.svg" }, { "id": "insights_view", "title": "Actions", "icon": "./media/images/dbt_icon.svg" }, { "id": "run_history_view", "title": "Run History", "icon": "./media/images/dbt_icon.svg" } ] }, "views": { "dbt_view": [ { "id": "model_test_treeview", "name": "Model Tests" }, { "id": "parent_model_treeview", "name": "Parent Models" }, { "id": "children_model_treeview", "name": "Children Models" }, { "id": "documentation_treeview", "name": "Documentation" }, { "id": "icon_actions_treeview", "name": "Project Actions ✨" } ], "dbt_preview_results": [ { "id": "dbtPowerUser.PreviewResults", "name": "", "type": "webview" } ], "lineage_view": [ { "id": "dbtPowerUser.Lineage", "name": "", "type": "webview" } ], "docs_edit_view": [ { "id": "dbtPowerUser.DocsEdit", "name": "", "type": "webview" } ], "datapilot_view": [ { "type": "webview", "id": "dbtPowerUser.datapilot-webview", "name": "", "icon": "./media/images/dbt_icon.svg" } ], "insights_view": [ { "id": "dbtPowerUser.Insights", "name": "", "type": "webview" } ], "run_history_view": [ { "id": "run_history_treeview", "name": "Runs" } ] }, "commands": [ { "command": "dbtPowerUser.collectQueryResultsDebugInfo", "title": "Query Results Debug Info", "category": "dbt Power User" }, { "command": "dbtPowerUser.resolveConversation", "title": "Resolve", "category": "dbt Power User" }, { "command": "dbtPowerUser.createConversation", "title": "Publish", "category": "dbt Power User", "enablement": "!commentIsEmpty" }, { "command": "dbtPowerUser.viewInDbtDocs", "title": "View in dbt docs", "category": "dbt Power User" }, { "command": "dbtPowerUser.copyDbtDocsLink", "title": "Copy link", "category": "dbt Power User" }, { "command": "dbtPowerUser.viewInDocEditor", "title": "View in documentation editor", "category": "dbt Power User" }, { "command": "dbtPowerUser.createSqlFile", "title": "Create Power User SQL file" }, { "command": "dbtPowerUser.createDatapilotNotebook", "title": "Datapilot notebook", "enablement": "dbt.enableNotebooks == true" }, { "command": "dbtPowerUser.datapilotProfileYourQuery", "title": "Profile your query", "icon": { "light": "./media/images/sql-action-light.svg", "dark": "./media/images/sql-action-dark.svg" } }, { "command": "dbtPowerUser.replyToConversation", "title": "Reply", "enablement": "!commentIsEmpty", "category": "dbt Power User" }, { "command": "dbtPowerUser.puQuickPick", "category": "dbt Power User", "title": "dbt Power User Quick Pick" }, { "command": "dbtPowerUser.openInsights", "category": "dbt Power User", "title": "Open dbt Power User insights panel" }, { "command": "dbtPowerUser.openOnboarding", "category": "dbt Power User", "title": "Get Started with dbt Power User" }, { "command": "dbtPowerUser.sqlQuickPick", "title": "SQL Actions", "category": "dbt Power User", "icon": { "light": "./media/images/sql-action-light.svg", "dark": "./media/images/sql-action-dark.svg" } }, { "command": "dbtPowerUser.runCurrentModel", "title": "Run dbt Model", "category": "dbt Power User", "icon": { "light": "./media/images/run-light.svg", "dark": "./media/images/run-dark.svg" } }, { "command": "dbtPowerUser.rerunFromHistory", "title": "Re-run Command", "category": "dbt Power User", "icon": "$(debug-rerun)" }, { "command": "dbtPowerUser.clearRunHistory", "title": "Clear Run History", "category": "dbt Power User", "icon": "$(clear-all)" }, { "command": "dbtPowerUser.profileCtes", "title": "Profile CTEs", "category": "dbt Power User" }, { "command": "dbtPowerUser.cancelCteProfiling", "title": "Cancel CTE Profiling", "category": "dbt Power User" }, { "command": "dbtPowerUser.clearProfileResults", "title": "Clear CTE Profile Results", "category": "dbt Power User", "icon": "$(clear-all)" }, { "command": "dbtPowerUser.toggleProfileDecorations", "title": "Toggle CTE Profile Decorations", "category": "dbt Power User" }, { "command": "dbtPowerUser.openDatapilotWithQuery", "title": "Start Chat", "category": "dbt Power User" }, { "command": "dbtPowerUser.summarizeQuery", "title": "Explain", "category": "dbt Power User" }, { "command": "dbtPowerUser.changeQuery", "title": "Change", "category": "dbt Power User" }, { "command": "dbtPowerUser.translateQuery", "title": "Translate to other SQL dialect", "category": "dbt Power User" }, { "command": "dbtPowerUser.sqlToModel", "title": "Convert to dbt Model", "shortTitle": "Convert to dbt Model", "category": "dbt Power User", "icon": { "light": "./media/images/preview-dbt-light.svg", "dark": "./media/images/preview-dbt-dark.svg" } }, { "command": "dbtPowerUser.validateSql", "title": "Validate SQL", "category": "dbt Power User", "icon": "$(circuit-board)" }, { "command": "dbtPowerUser.altimateScan", "title": "Project Healthcheck", "category": "dbt Power User", "icon": "$(circuit-board)" }, { "command": "dbtPowerUser.clearAltimateScanResults", "category": "dbt Power User", "title": "Clear Health Check Results" }, { "command": "dbtPowerUser.bigqueryCostEstimate", "category": "dbt Power User", "title": "Estimate cost for BigQuery" }, { "command": "dbtPowerUser.compileCurrentModel", "title": "Compile dbt Model", "category": "dbt Power User", "icon": { "light": "./media/images/build_light.svg", "dark": "./media/images/build_dark.svg" } }, { "command": "dbtPowerUser.sqlPreview", "title": "Compiled dbt Preview", "category": "dbt Power User", "icon": "$(diff)" }, { "command": "dbtPowerUser.runTest", "title": "Run Test", "category": "dbt Power User", "icon": "$(pass)" }, { "command": "dbtPowerUser.testCurrentModel", "title": "Test dbt Model", "category": "dbt Power User", "icon": "$(pass)" }, { "command": "dbtPowerUser.runChildrenModels", "title": "Run Downstream Models", "category": "dbt Power User", "icon": "$(run-below)" }, { "command": "dbtPowerUser.runParentModels", "title": "Run Upstream Models", "category": "dbt Power User", "icon": "$(run-above)" }, { "command": "dbtPowerUser.copyModelName", "title": "Copy model name to clipboard", "category": "dbt Power User" }, { "command": "dbtPowerUser.showRunSQL", "title": "Open dbt target folder run SQL", "category": "dbt Power User", "icon": { "light": "./media/images/file-code_light.svg", "dark": "./media/images/file-code_dark.svg" } }, { "command": "dbtPowerUser.generateSchemaYML", "title": "Generate Documentation YML", "category": "dbt Power User", "icon": { "light": "./media/images/file-code_light.svg", "dark": "./media/images/file-code_dark.svg" } }, { "command": "dbtPowerUser.goToDocumentationEditor", "title": "Open Documentation Editor", "category": "dbt Power User", "icon": { "light": "./media/images/file-code_light.svg", "dark": "./media/images/file-code_dark.svg" } }, { "command": "dbtPowerUser.generateDBTDocs", "title": "Run Dbt Docs Generate", "category": "dbt Power User", "icon": { "light": "./media/images/run-light.svg", "dark": "./media/images/run-dark.svg" } }, { "command": "dbtPowerUser.resetDatapilot", "title": "Start New Chat", "icon": "$(add)", "category": "dbt Power User" }, { "command": "dbtPowerUser.maximizeDatapilot", "title": "Maximize", "category": "dbt Power User", "icon": { "light": "./media/images/fullscreen-light.svg", "dark": "./media/images/fullscreen-dark.svg" } }, { "command": "dbtPowerUser.showHelpDatapilot", "title": "Help", "category": "dbt Power User", "icon": "$(question)" }, { "command": "dbtPowerUser.showCompiledSQL", "title": "Open dbt target folder compiled SQL", "category": "dbt Power User", "icon": { "light": "./media/images/file-code_light.svg", "dark": "./media/images/file-code_dark.svg" } }, { "command": "dbtPowerUser.executeSQL", "title": "Execute dbt SQL", "category": "dbt Power User", "icon": "$(play)" }, { "command": "dbtPowerUser.buildCurrentModel", "title": "Build dbt Model", "category": "dbt Power User", "icon": { "light": "./media/images/build-model_light.svg", "dark": "./media/images/build-model_dark.svg" } }, { "command": "dbtPowerUser.buildChildrenModels", "title": "Build Downstream Models (model+)", "category": "dbt Power User", "icon": { "light": "./media/images/build-model_light.svg", "dark": "./media/images/build-model_dark.svg" } }, { "command": "dbtPowerUser.buildParentModels", "title": "Build Upstream Models (+model)", "category": "dbt Power User", "icon": { "light": "./media/images/build-model_light.svg", "dark": "./media/images/build-model_dark.svg" } }, { "command": "dbtPowerUser.buildChildrenParentModels", "title": "Build Upstream and Downstream Models (+model+)", "category": "dbt Power User", "icon": { "light": "./media/images/build-model_light.svg", "dark": "./media/images/build-model_dark.svg" } }, { "command": "dbtPowerUser.buildCurrentProject", "title": "Build dbt Project", "category": "dbt Power User", "icon": { "light": "./media/images/build-model_light.svg", "dark": "./media/images/build-model_dark.svg" } }, { "command": "dbtPowerUser.cleanCurrentProject", "title": "Clean dbt Project", "category": "dbt Power User", "icon": "$(clear-all)" }, { "command": "dbtPowerUser.printEnvVars", "title": "Print environment variables", "category": "dbt Power User", "icon": "$(gist-secret)" }, { "command": "dbtPowerUser.diagnostics", "title": "Collect diagnostics", "category": "dbt Power User", "icon": "$(debug-console)" }, { "command": "dbtPowerUser.detectPythonFromTerminal", "title": "Detect Python from terminal", "category": "dbt Power User", "icon": "$(terminal)" }, { "command": "dbtPowerUser.sqlLineage", "title": "Visualize SQL (Beta)", "category": "dbt Power User", "icon": "$(eye)" }, { "command": "dbtPowerUser.showDocumentation", "title": "Show documentation", "category": "dbt Power User" }, { "command": "dbtPowerUser.showDatapilotNotebooksQuickPick", "title": "Show Datapilot Notebooks Quick Pick", "category": "dbt Power User" }, { "command": "dbtPowerUser.showNotebookProfileQuery", "title": "Profile this query", "group": "notebooks@1" }, { "command": "dbtPowerUser.showNotebookTestSuggestions", "title": "Get test suggestions", "group": "notebooks@2" }, { "command": "dbtPowerUser.showNotebookGenerateBaseModelSql", "title": "Generate dbt base model sql", "group": "notebooks@3" }, { "command": "dbtPowerUser.showNotebookGenerateModelYaml", "title": "Generate dbt model yaml", "group": "notebooks@4" }, { "command": "dbtPowerUser.showNotebookGenerateModelCTE", "title": "Generate dbt model CTE", "group": "notebooks@5" }, { "command": "dbtPowerUser.applyDeferConfig", "category": "dbt Power User", "title": "Apply defer configuration" }, { "command": "dbtPowerUser.askAltimateAboutSelection", "category": "dbt Power User", "title": "Ask Altimate About Selection" }, { "command": "dbtPowerUser.explainWithAltimate", "category": "dbt Power User", "title": "Explain with Altimate" }, { "command": "dbtPowerUser.optimizeWithAltimate", "category": "dbt Power User", "title": "Optimize with Altimate" }, { "command": "dbtPowerUser.analyzeFileWithAltimate", "category": "dbt Power User", "title": "Analyze with Altimate" }, { "command": "dbtPowerUser.openAltimateChat", "category": "dbt Power User", "title": "Open Altimate Code Chat", "icon": "$(sparkle)" }, { "command": "dbtPowerUser.reviewCodeWithAltimate", "category": "dbt Power User", "title": "Review with Altimate" } ], "keybindings": [ { "key": "Ctrl+Enter", "mac": "Cmd+Enter", "command": "dbtPowerUser.executeSQL", "when": "editorFocus && resourceLangId =~ /^sql$|^jinja-sql$/" }, { "key": "Ctrl+'", "mac": "Cmd+'", "command": "dbtPowerUser.sqlPreview" } ], "menus": { "file/newFile": [ { "command": "dbtPowerUser.createSqlFile" }, { "command": "dbtPowerUser.createDatapilotNotebook", "when": "dbt.enableNotebooks == true", "group": "notebook" } ], "comments/commentThread/title": [ { "command": "dbtPowerUser.viewInDocEditor", "group": "navigation", "when": "commentController == altimate-conversations && (commentThread =~ /saved/) && commentThread =~ /description/" }, { "command": "dbtPowerUser.copyDbtDocsLink", "group": "navigation", "when": "commentController == altimate-conversations && (commentThread =~ /saved/)" }, { "command": "dbtPowerUser.viewInDbtDocs", "group": "navigation", "when": "commentController == altimate-conversations && (commentThread =~ /saved/)" }, { "command": "dbtPowerUser.resolveConversation", "group": "inline", "when": "commentController == altimate-conversations && !commentThreadIsEmpty && (commentThread =~ /saved/)" } ], "comments/commentThread/context": [ { "command": "dbtPowerUser.createConversation", "group": "inline@1", "when": "commentController == altimate-conversations && commentThreadIsEmpty" }, { "command": "dbtPowerUser.replyToConversation", "group": "inline", "when": "commentController == altimate-conversations && !commentThreadIsEmpty" } ], "dbtPowerUser.datapilotSubmenu": [ { "command": "dbtPowerUser.openDatapilotWithQuery", "group": "datapilot@1" }, { "command": "dbtPowerUser.summarizeQuery", "group": "datapilot@2" }, { "command": "dbtPowerUser.changeQuery", "group": "datapilot@3" }, { "command": "dbtPowerUser.translateQuery", "group": "datapilot@4" }, { "command": "dbtPowerUser.sqlLineage", "group": "datapilot@5" }, { "command": "dbtPowerUser.showNotebookProfileQuery", "group": "notebooks@1" }, { "command": "dbtPowerUser.showNotebookTestSuggestions", "group": "notebooks@2" }, { "command": "dbtPowerUser.showNotebookGenerateBaseModelSql", "group": "notebooks@3" }, { "command": "dbtPowerUser.showNotebookGenerateModelYaml", "group": "notebooks@4" }, { "command": "dbtPowerUser.showNotebookGenerateModelCTE", "group": "notebooks@5" } ], "dbtPowerUser.altimateCodeSubmenu": [ { "command": "dbtPowerUser.askAltimateAboutSelection", "group": "1@1", "when": "editorHasSelection" }, { "command": "dbtPowerUser.explainWithAltimate", "group": "1@2" }, { "command": "dbtPowerUser.optimizeWithAltimate", "group": "1@3", "when": "resourceLangId =~ /^sql$|^jinja-sql$/" }, { "command": "dbtPowerUser.reviewCodeWithAltimate", "group": "2@1", "when": "dbtPowerUser.fileHasGitChanges" } ], "explorer/context": [ { "command": "dbtPowerUser.analyzeFileWithAltimate", "group": "altimate@1", "when": "resourceLangId =~ /^sql$|^jinja-sql$|^yaml$|^jinja-yaml$/" } ], "editor/title": [ { "command": "dbtPowerUser.openAltimateChat", "when": "resourceLangId =~ /^sql$|^jinja-sql$|^yaml$|^jinja-yaml$/", "group": "navigation@0" }, { "command": "dbtPowerUser.executeSQL", "when": "resourceLangId =~ /^sql$|^jinja-sql$/", "group": "navigation@1" }, { "command": "dbtPowerUser.runCurrentModel", "when": "resourceLangId =~ /^sql$|^jinja-sql$/", "group": "navigation@3" }, { "command": "dbtPowerUser.testCurrentModel", "when": "resourceLangId =~ /^sql$|^jinja-sql$/", "group": "navigation@4" }, { "command": "dbtPowerUser.sqlPreview", "when": "resourceLangId =~ /^sql$|^jinja-sql$/", "group": "navigation@5" }, { "command": "dbtPowerUser.sqlQuickPick", "when": "resourceLangId =~ /^sql$|^jinja-sql$/", "group": "navigation@1" }, { "command": "dbtPowerUser.sqlToModel", "when": "resourceLangId =~ /^sql$|^jinja-sql$/", "group": "navigation@6" } ], "editor/context": [ { "submenu": "dbtPowerUser.altimateCodeSubmenu", "group": "altimate@1", "when": "resourceLangId =~ /^sql$|^jinja-sql$|^yaml$|^jinja-yaml$/" }, { "submenu": "dbtPowerUser.datapilotSubmenu", "group": "datapilot@1", "when": "resourceLangId =~ /^sql$|^jinja-sql$/" }, { "submenu": "dbtPowerUser.buildModel", "group": "dbt@5", "when": "resourceLangId =~ /^sql$|^jinja-sql$/" }, { "command": "dbtPowerUser.showCompiledSQL", "group": "dbt@5", "when": "resourceLangId =~ /^sql$|^jinja-sql$/" }, { "command": "dbtPowerUser.showRunSQL", "group": "dbt@6", "when": "resourceLangId =~ /^sql$|^jinja-sql$/" }, { "command": "dbtPowerUser.goToDocumentationEditor", "group": "dbt@7", "when": "resourceLangId =~ /^sql$|^jinja-sql$/" }, { "command": "dbtPowerUser.testCurrentModel", "group": "dbt@2", "when": "resourceLangId =~ /^sql$|^jinja-sql$/" }, { "command": "dbtPowerUser.runCurrentModel", "group": "dbt@1", "when": "resourceLangId =~ /^sql$|^jinja-sql$/" }, { "command": "dbtPowerUser.sqlToModel", "group": "dbt@3", "when": "resourceLangId =~ /^sql$|^jinja-sql$/" }, { "command": "dbtPowerUser.compileCurrentModel", "group": "dbt@4", "when": "resourceLangId =~ /^sql$|^jinja-sql$/" }, { "command": "dbtPowerUser.validateSql", "group": "dbt@4", "when": "resourceLangId =~ /^sql$|^jinja-sql$/" }, { "command": "dbtPowerUser.bigqueryCostEstimate", "group": "dbt@4", "when": "resourceLangId =~ /^sql$|^jinja-sql$/" } ], "editor/title/run": [ { "command": "dbtPowerUser.buildCurrentProject", "when": "resourceLangId =~ /^sql$|^jinja-sql$/", "group": "inline" }, { "command": "dbtPowerUser.buildCurrentModel", "when": "resourceLangId =~ /^sql$|^jinja-sql$/", "group": "inline" }, { "command": "dbtPowerUser.buildChildrenModels", "when": "resourceLangId =~ /^sql$|^jinja-sql$/", "group": "inline" }, { "command": "dbtPowerUser.buildParentModels", "when": "resourceLangId =~ /^sql$|^jinja-sql$/", "group": "inline" }, { "command": "dbtPowerUser.buildChildrenParentModels", "when": "resourceLangId =~ /^sql$|^jinja-sql$/", "group": "inline" }, { "command": "dbtPowerUser.runCurrentModel", "when": "resourceLangId =~ /^sql$|^jinja-sql$/", "group": "inline" }, { "command": "dbtPowerUser.cleanCurrentProject", "when": "resourceLangId =~ /^sql$|^jinja-sql$/", "group": "inline" } ], "view/title": [ { "command": "dbtPowerUser.testCurrentModel", "when": "view == model_test_treeview", "group": "navigation" }, { "command": "dbtPowerUser.runChildrenModels", "when": "view == children_model_treeview", "group": "navigation" }, { "command": "dbtPowerUser.runParentModels", "when": "view == parent_model_treeview", "group": "navigation" }, { "command": "dbtPowerUser.generateDBTDocs", "when": "view == documentation_treeview", "group": "navigation" }, { "command": "dbtPowerUser.resetDatapilot", "when": "view == dbtPowerUser.datapilot-webview", "group": "navigation@1" }, { "command": "dbtPowerUser.showHelpDatapilot", "when": "view == dbtPowerUser.datapilot-webview", "group": "navigation@3" }, { "command": "dbtPowerUser.clearRunHistory", "when": "view == run_history_treeview", "group": "2_clear" } ], "view/item/context": [ { "command": "dbtPowerUser.rerunFromHistory", "when": "view == run_history_treeview && viewItem == runHistoryEntry", "group": "inline" }, { "command": "dbtPowerUser.runTest", "when": "view == model_test_treeview && viewItem != source", "group": "inline" }, { "command": "dbtPowerUser.runChildrenModels", "when": "view == children_model_treeview && viewItem != source", "group": "inline" }, { "command": "dbtPowerUser.runParentModels", "when": "view == parent_model_treeview && viewItem != source", "group": "inline" }, { "command": "dbtPowerUser.copyModelName", "when": "(view == parent_model_treeview || view == children_model_treeview || view == documentation_treeview) && viewItem != source", "group": "navigation" }, { "command": "dbtPowerUser.goToDocumentationEditor", "when": "view == documentation_treeview", "group": "inline" } ] }, "dbtPowerUser.buildModel": [ { "command": "dbtPowerUser.buildCurrentModel", "when": "resourceLangId =~ /^sql$|^jinja-sql$/", "group": "inline" }, { "command": "dbtPowerUser.buildChildrenModels", "when": "resourceLangId =~ /^sql$|^jinja-sql$/", "group": "inline" }, { "command": "dbtPowerUser.buildParentModels", "when": "resourceLangId =~ /^sql$|^jinja-sql$/", "group": "inline" }, { "command": "dbtPowerUser.buildChildrenParentModels", "when": "resourceLangId =~ /^sql$|^jinja-sql$/", "group": "inline" } ], "submenus": [ { "id": "dbtPowerUser.buildModel", "label": "Build dbt Model", "icon": { "light": "./media/images/build-model_light.svg", "dark": "./media/images/build-model_dark.svg" } }, { "id": "dbtPowerUser.datapilotSubmenu", "label": "Datapilot" }, { "id": "dbtPowerUser.altimateCodeSubmenu", "label": "Altimate Code" } ], "languages": [ { "id": "jinja-sql", "icon": { "light": "./media/images/dbt_file_icon.svg", "dark": "./media/images/dbt_file_icon.svg" } } ], "grammars": [ { "language": "jinja-sql", "scopeName": "source.sql.jinja", "path": "./syntaxes/jinja-sql.tmLanguage.json" }, { "scopeName": "source.yaml.jinja", "path": "./syntaxes/jinja-yaml.tmLanguage.json", "injectTo": [ "source.yaml" ] } ] }, "scripts": { "postinstall": "node postInstall.js", "prepare": "husky", "vscode:prepublish": "npm run panel:webviews && rsbuild build && node prepareBuild.js", "build": "npm run panel:webviews && rsbuild build", "build-dev": "npm run panel:webviews && rsbuild build --mode development", "watch": "concurrently \"npm:watch --prefix ./webview_panels\" \"rsbuild build --mode development --watch\"", "test-compile": "tsc -p ./", "lint": "eslint src --ext ts", "lint:fix": "eslint src --ext ts --fix", "deploy-vscode": "vsce publish", "build-vsix": "vsce package", "deploy-openvsx": "ovsx publish", "panel:webviews": "npm run build --prefix ./webview_panels", "install:panels": "npm install --prefix ./webview_panels", "test": "jest", "test:unit": "jest", "test:integration": "npm run test-compile && node out/test/integration/runTests.js", "test:coverage": "jest --coverage", "pretest": "npm run clean && npm run compile", "clean": "rimraf out coverage", "compile": "tsc -p ./", "docker:deploy": "./docker-setup/deploy.sh", "docker:logs": "cd docker-setup && docker compose logs -f || docker-compose logs -f", "docker:stop": "cd docker-setup && docker compose down || docker-compose down" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", "@rsbuild/core": "^1.5.17", "@types/chai": "^4.3.9", "@types/glob": "^8.1.0", "@types/istanbul-lib-coverage": "^2.0.6", "@types/jest": "^29.5.0", "@types/js-yaml": "^4.0.9", "@types/mocha": "^10.0.7", "@types/node": "^25.6.0", "@types/vscode": "^1.95.0", "@types/which": "^3.0.4", "@types/ws": "^8.5.12", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "@vscode/debugadapter": "^1.68.0", "@vscode/debugprotocol": "^1.68.0", "@vscode/test-electron": "^2.4.1", "@vscode/zeromq": "^0.2.1", "chai": "^4.3.10", "concurrently": "^8.2.2", "cross-env": "^7.0.3", "eslint": "^8.57.0", "eslint-config-prettier": "^8.9.0", "eslint-plugin-prettier": "^5.2.1", "file-loader": "^6.1.0", "glob": "^10.4.2", "glob-parent": "^6.0.2", "husky": "^9.1.4", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-instrument": "^6.0.3", "jest": "^29.5.0", "lint-staged": "^15.2.5", "mocha": "^10.7.0", "ovsx": "^0.10.11", "patch-package": "^8.0.1", "prettier": "^3.3.3", "prettier-eslint": "^15.0.1", "prettier-plugin-organize-imports": "^3.2.4", "source-map-support": "^0.5.21", "ts-jest": "^29.1.0", "ts-loader": "^9.4.4", "ts-mockito": "^2.6.1", "ts-node": "^10.9.2", "typescript": "^5.9.3" }, "extensionDependencies": [ "samuelcolvin.jinjahtml", "ms-python.python", "altimateai.vscode-altimate-mcp-server" ], "dependencies": { "@altimateai/dbt-integration": "^0.2.14", "@jupyterlab/coreutils": "^6.2.4", "@jupyterlab/nbformat": "^4.2.4", "@jupyterlab/services": "^7.0.0", "@modelcontextprotocol/sdk": "^1.24.0", "@nteract/messaging": "^7.0.20", "@types/express": "^5.0.0", "@vscode/chat-extension-utils": "^0.0.0-alpha.5", "@vscode/codicons": "^0.0.36", "@vscode/extension-telemetry": "0.9.7", "@vscode/vsce": "^2.31.1", "@vscode/webview-ui-toolkit": "^1.4.0", "dayjs": "^1.11.13", "express": "^5.1.0", "inversify": "^6.0.2", "inversify-binding-decorators": "^4.0.0", "node-abort-controller": "^3.1.1", "node-fetch": "^3.3.2", "parse-diff": "^0.11.1", "python-bridge": "^1.1.0", "reflect-metadata": "^0.2.2", "semver": "^7.6.3", "which": "^6.0.1", "ws": "^8.18.0", "yaml": "^2.6.0", "zeromq": "^6.1.1", "zod": "^3.25.28", "zod-to-json-schema": "^3.25.2" }, "lint-staged": { "**/webview_panels/**/*.{ts,tsx}": [ "cross-env PRE_COMMIT=true npm run lint --fix --prefix webview_panels", "prettier -w" ], "*.ts": [ "eslint src --ext ts --fix", "eslint --max-warnings=0" ], "*.{js,css,md,html,json}": "prettier --cache --write" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } ================================================ FILE: package.nls.json ================================================ { "walkthrough..title": "Walkthrough Title", "walkthrough..description": "Walkthrough Description", "walkthrough..steps..title": "Walkthrough idx step idy title", "walkthrough..steps..description": "Walkthrough idx step idy description", "walkthrough.initialSetup.title": "Setup dbt Power User", "walkthrough.initialSetup.description": "Setup the dbt Power User extension for your project by following the steps shown below.", "walkthrough.initialSetup.steps.support.title": "Welcome to dbt Power User setup!", "walkthrough.initialSetup.steps.support.description": "Before you begin, know that our team is on standby to assist you. Should you encounter any issues or have questions, [contact us](https://www.altimate.ai/support) right away.", "walkthrough.initialSetup.steps.open.title": "Open dbt Project", "walkthrough.initialSetup.steps.open.description": "To start off, open a dbt project.\n[Open dbt Project](command:toSide:vscode.openFolder)", "walkthrough.initialSetup.steps.selectInterpreter.title": "Select Python Interpreter", "walkthrough.initialSetup.steps.selectInterpreter.description": "Choose the Python Interpreter where you have installed dbt.\n[Select Python Interpreter](command:python.setInterpreter)", "walkthrough.initialSetup.steps.installDbt.title": "Install dbt", "walkthrough.initialSetup.steps.installDbt.description": "Please install dbt in the selected Python Environment.\n[Install dbt](command:dbtPowerUser.installDbt)", "walkthrough.initialSetup.steps.updateExtension.title": "Update dbt Power User Extension", "walkthrough.initialSetup.steps.updateExtension.description": "To get the latest features, update the dbt Power User extension.\n[Update Extension](command:workbench.extensions.action.extensionUpdates)", "walkthrough.initialSetup.steps.associateFileExts.title": "Associate File Types", "walkthrough.initialSetup.steps.associateFileExts.description": "For dbt Power User to work, *.sql file types need to be associated with the value 'sql' or 'jinja-sql' and *.yml file types should be associated with the value 'yaml' or 'jinja-yaml'.\n[Associate File Types](command:toSide:dbtPowerUser.associateFileExts)", "walkthrough.initialSetup.steps.validateProjectCore.title": "Validate project setup", "walkthrough.initialSetup.steps.validateProjectCore.description": "Check if the dbt project is setup correctly. This will run dbt debug to ensure that dbt is able to locate the profile and connect to the database. You can validate multiple projects one by one as well\n[Validate Project](command:dbtPowerUser.validateProject)", "walkthrough.initialSetup.steps.validateProjectCloud.title": "Validate project setup", "walkthrough.initialSetup.steps.validateProjectCloud.description": "Check if the dbt project is setup correctly. This will run dbt environment show to ensure that dbt is able to locate the profile and connect to the database. You can validate multiple projects one by one as well\n[Validate Project](command:dbtPowerUser.validateProject)", "walkthrough.initialSetup.steps.pickProject.title": "Pick a dbt project", "walkthrough.initialSetup.steps.pickProject.description": "Now, let’s validate the dbt project that you are working on by checking for dbt dependencies and identifying any issues in the configuration.\n[Pick Project](command:dbtPowerUser.pickProject)", "walkthrough.initialSetup.steps.installDeps.title": "Run dbt deps", "walkthrough.initialSetup.steps.installDeps.description": "This step will run dbt deps and install or update any packages involved in the project. You should see the results in a terminal that opens up. Each package should say 'Up to Date' as they are installed/ updated.\n[Run dbt deps](command:dbtPowerUser.installDeps)", "walkthrough.initialSetup.steps.finish.title": "Finish setup", "walkthrough.initialSetup.steps.finish.description": "If you followed all the steps so far, the extension should be set up for your project.\nIf you work with multiple projects in a single workspace, please pick another project and run the steps to install packages and run validation for all of them.\nIf you still have issues, you can [contact us](https://www.altimate.ai/support) for further help", "walkthrough.tutorials.title": "dbt Power User Tutorials", "walkthrough.tutorials.description": "Learn about new dbt Power User features in these tutorials.", "walkthrough.tutorials.steps.autocompletion.title": "Auto-completion of models, macros and sources", "walkthrough.tutorials.steps.autocompletion.description": " ", "walkthrough.tutorials.steps.previews.title": "dbt model and SQL query results preview", "walkthrough.tutorials.steps.previews.description": " ", "walkthrough.tutorials.steps.documentation.title": "Generate or edit dbt documentation easily", "walkthrough.tutorials.steps.documentation.description": " ", "walkthrough.tutorials.steps.lineage.title": "Model Lineage and impact analysis using column level lineage", "walkthrough.tutorials.steps.lineage.description": " ", "walkthrough.tutorials.steps.dependants.title": "Click to build and run parent / children models, tests", "walkthrough.tutorials.steps.dependants.description": " ", "walkthrough.tutorials.steps.generate_models.title": "Generate dbt models from SQL or source files", "walkthrough.tutorials.steps.generate_models.description": " ", "walkthrough.tutorials.steps.healthcheck.title": "Project Health Check - find issues in your dbt project", "walkthrough.tutorials.steps.healthcheck.description": " ", "walkthrough.tutorials.steps.qsummary.title": "Query Summary", "walkthrough.tutorials.steps.qsummary.description": " " } ================================================ FILE: postInstall.js ================================================ // Copied from https://github.com/microsoft/vscode-jupyter/blob/main/build/ci/postInstall.js const fs = require("fs"); const { downloadZMQ } = require("@vscode/zeromq"); const path = require("path"); /** * In order to get raw kernels working, we reuse the default kernel that jupyterlab ships. * However it expects to be talking to a websocket which is serializing the messages to strings. * Our raw kernel is not a web socket and needs to do its own serialization. To do so, we make a copy * of the default kernel with the serialization stripped out. This is simpler than making a copy of the module * at runtime. */ function createJupyterKernelWithoutSerialization() { var relativePath = path.join( "node_modules", "@jupyterlab", "services", "lib", "kernel", "default.js", ); var filePath = path.join("", relativePath); if (!fs.existsSync(filePath)) { throw new Error( "Jupyter lab default kernel not found '" + filePath + "' (Jupyter Extension post install script)", ); } var fileContents = fs.readFileSync(filePath, { encoding: "utf8" }); // Strip websocket serialization by replacing serializer calls with pass-through. var replacedContents = fileContents .replace( /this\.serverSettings\.serializer\.deserialize\(([^,]+),\s*this\._ws\.protocol\)/g, "$1", ) .replace( /this\.serverSettings\.serializer\.serialize\(([^,]+),\s*this\._ws\.protocol\)/g, "$1", ); if (replacedContents === fileContents) { throw new Error( "Jupyter lab default kernel cannot be made non serializing", ); } var destPath = path.join(path.dirname(filePath), "nonSerializingKernel.js"); fs.writeFileSync(destPath, replacedContents); console.log(destPath + " file generated (by Jupyter VSC)"); } async function downloadZmqBinaries() { // if (common.getBundleConfiguration() === common.bundleConfiguration.web) { // // No need to download zmq binaries for web. // return; // } await downloadZMQ(); } /** * Install altimate-core platform binaries. * * When VSCE_TARGET is set (CI builds platform-specific VSIXs), only install the * matching platform binary. Otherwise (local dev), install all platforms so the * extension works regardless of which machine it runs on. * * npm only installs optional dependencies for the current platform. * We use npm pack + tar to manually extract the tarballs into node_modules. */ async function installAltimateCoreAllPlatforms() { const { execSync } = require("child_process"); // Read version from the main altimate-core package to keep platform // packages in sync (npm pack without a version would fetch "latest"). const corePkgPath = path.join( "node_modules", "@altimateai", "altimate-core", "package.json", ); if (!fs.existsSync(corePkgPath)) { console.warn( "@altimateai/altimate-core not found in node_modules, skipping platform binary install", ); return; } const { version: coreVersion } = JSON.parse( fs.readFileSync(corePkgPath, "utf8"), ); console.log(`altimate-core version: ${coreVersion}`); // Map VS Code target platforms to altimate-core npm package names const vsceTargetToPackage = { "darwin-arm64": "@altimateai/altimate-core-darwin-arm64", "darwin-x64": "@altimateai/altimate-core-darwin-x64", "linux-arm64": "@altimateai/altimate-core-linux-arm64-gnu", "linux-x64": "@altimateai/altimate-core-linux-x64-gnu", "win32-x64": "@altimateai/altimate-core-win32-x64-msvc", }; const allPackages = Object.values(vsceTargetToPackage); const vsceTarget = process.env.VSCE_TARGET; let packagesToInstall; if (vsceTarget) { const pkg = vsceTargetToPackage[vsceTarget]; if (!pkg) { console.warn( `Unknown VSCE_TARGET "${vsceTarget}", installing all platforms`, ); packagesToInstall = allPackages; } else { console.log(`VSCE_TARGET=${vsceTarget} — installing only ${pkg}`); packagesToInstall = [pkg]; } } else { packagesToInstall = allPackages; } const missing = packagesToInstall.filter((pkg) => { const pkgDir = path.join("node_modules", ...pkg.split("/")); return !fs.existsSync(pkgDir); }); if (missing.length === 0) { console.log( "All required altimate-core platform packages already installed", ); return; } console.log( `Installing altimate-core platform packages via npm pack: ${missing.join(", ")}`, ); const os = require("os"); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "altimate-core-")); for (const pkg of missing) { try { // npm pack downloads the tarball without os/cpu filtering const tgzFile = execSync( `npm pack ${pkg}@${coreVersion} --pack-destination ${tmpDir}`, { encoding: "utf8" }, ).trim(); const tgzPath = path.join(tmpDir, tgzFile); const destDir = path.join("node_modules", ...pkg.split("/")); fs.mkdirSync(destDir, { recursive: true }); execSync(`tar xzf "${tgzPath}" --strip-components=1 -C "${destDir}"`); console.log(`Installed ${pkg} via npm pack + tar`); } catch (ex) { console.error(`Failed to install ${pkg}: ${ex.message}`); } } // Clean up temp dir try { fs.rmSync(tmpDir, { recursive: true }); } catch {} // Verify required packages are present const stillMissing = packagesToInstall.filter((pkg) => { const pkgDir = path.join("node_modules", ...pkg.split("/")); return !fs.existsSync(pkgDir); }); if (stillMissing.length > 0) { console.error( `ERROR: These altimate-core packages are still missing: ${stillMissing.join(", ")}`, ); } else { console.log( "All required altimate-core platform packages installed successfully", ); } } /** * Map VSCE_TARGET to the zeromq prebuild folder names to keep. * Returns empty array when no target is set (keep all). * Mirrors getZeroMQPreBuildsFoldersToKeep() in prepareBuild.js. */ function getZeroMQPreBuildsFoldersToKeep(vsceTarget) { if (!vsceTarget) { return []; } else if (vsceTarget.includes("win32")) { if (vsceTarget.includes("x64")) { return ["win32-x64"]; } else if (vsceTarget.includes("arm64")) { return ["win32-arm64"]; } else { return ["win32-x64", "win32-arm64"]; } } else if (vsceTarget.includes("linux") || vsceTarget.includes("alpine")) { if (vsceTarget.includes("arm64")) { return ["linux-arm64"]; } else if (vsceTarget.includes("x64")) { return ["linux-x64"]; } else if (vsceTarget.includes("armhf") || vsceTarget.includes("arm")) { return ["linux-arm"]; } else { return ["linux-arm64", "linux-x64", "linux-arm"]; } } else if (vsceTarget.includes("darwin")) { if (vsceTarget.includes("arm64")) { return ["darwin-arm64"]; } else if (vsceTarget.includes("x64")) { return ["darwin-x64"]; } else { return ["darwin-x64", "darwin-arm64"]; } } else { console.warn( `Unknown VSCE_TARGET "${vsceTarget}", keeping all zeromq prebuilds`, ); return []; } } /** * Prune zeromq prebuilds for non-target platforms right after download. * This avoids webpack copying ~13MB of unused prebuilds into dist/. * prepareBuild.js still acts as a safety net for dist/. */ function pruneZeromqPrebuilds() { const vsceTarget = process.env.VSCE_TARGET; if (!vsceTarget) { return; } const keepFolders = getZeroMQPreBuildsFoldersToKeep(vsceTarget); if (keepFolders.length === 0) { return; } const prebuildsDir = path.join("node_modules", "zeromq", "prebuilds"); if (!fs.existsSync(prebuildsDir)) { return; } const entries = fs.readdirSync(prebuildsDir); for (const entry of entries) { if (!keepFolders.includes(entry)) { fs.rmSync(path.join(prebuildsDir, entry), { recursive: true }); console.log(`Pruned zeromq prebuild: ${entry}`); } } console.log( `Kept zeromq prebuilds for VSCE_TARGET=${vsceTarget}: ${keepFolders.join(", ")}`, ); } createJupyterKernelWithoutSerialization(); Promise.all([downloadZmqBinaries(), installAltimateCoreAllPlatforms()]) .then(() => { pruneZeromqPrebuilds(); process.exit(0); }) .catch((ex) => { console.error("Post-install failed", ex); process.exit(1); }); ================================================ FILE: prepareBuild.js ================================================ const path = require("path"); const fs = require("fs"); function getZeroMQPreBuildsFoldersToKeep() { // Possible values of 'VSC_VSCE_TARGET' include platforms supported by `vsce package --target` // See here https://code.visualstudio.com/api/working-with-extensions/publishing-extension#platformspecific-extensions const vsceTarget = process.env.VSC_VSCE_TARGET || process.env.VSCE_TARGET; console.log("vsceTarget", vsceTarget); if (!vsceTarget) { // Keep all of them, as we're not building platform specific bundles. return []; } else if (vsceTarget === "web") { throw new Error("Not supported when targeting the Web"); } else if (vsceTarget.includes("win32")) { if (vsceTarget.includes("x64")) { return ["win32-x64"]; } else if (vsceTarget.includes("arm64")) { return ["win32-arm64"]; } else { return ["win32-x64", "win32-arm64"]; } } else if (vsceTarget.includes("linux")) { if (vsceTarget.includes("arm64")) { return ["linux-arm64"]; } else if (vsceTarget.includes("x64")) { return ["linux-x64"]; } else if (vsceTarget.includes("arm")) { return ["linux-arm"]; } else { return ["linux-arm64", "linux-x64", "linux-arm"]; } } else if (vsceTarget.includes("alpine")) { if (vsceTarget.includes("arm64")) { return ["linux-arm64"]; } else if (vsceTarget.includes("x64")) { return ["linux-x64"]; } else if (vsceTarget.includes("armhf")) { return ["linux-arm"]; } else { return ["linux-arm64", "linux-x64", "linux-arm"]; } } else if (vsceTarget.includes("darwin")) { if (vsceTarget.includes("arm64")) { return ["darwin-arm64"]; } else if (vsceTarget.includes("x64")) { return ["darwin-x64"]; } else { return ["darwin-x64", "darwin-arm64"]; } } else { throw new Error(`Unknown platform '${vsceTarget}'}`); } } function shouldCopyFileFromZmqFolder(resourcePath) { const parentFolder = path.dirname(resourcePath); if (fs.statSync(resourcePath).isDirectory()) { return true; } // return true; const filename = path.basename(resourcePath); // Ensure the code is platform agnostic. resourcePath = (resourcePath || "") .toString() .toLowerCase() .replace(/\\/g, "/"); // We do not need to bundle these folders const foldersToIgnore = ["build", "script", "src", "node_modules", "vendor"]; if ( foldersToIgnore.some((folder) => resourcePath .toLowerCase() .startsWith( path.join(parentFolder, folder).replace(/\\/g, "/").toLowerCase(), ), ) ) { return false; } if ( resourcePath.endsWith(".js") || resourcePath.endsWith(".json") || resourcePath.endsWith(".md") || resourcePath.endsWith("license") ) { return true; } // if (!resourcePath.includes(path.join(parentFolder, 'prebuilds').replace(/\\/g, '/').toLowerCase())) { if (!parentFolder.includes(`${path.sep}prebuilds${path.sep}`)) { // We do not ship any other sub directory. return false; } if (filename.includes("electron.") && resourcePath.endsWith(".node")) { // We do not ship electron binaries. return false; } const preBuildsFoldersToCopy = getZeroMQPreBuildsFoldersToKeep(); console.log("preBuildsFoldersToCopy", preBuildsFoldersToCopy); if (preBuildsFoldersToCopy.length === 0) { // Copy everything from all prebuilds folder. return true; } // Copy if this is a prebuilds folder that needs to be copied across. // Use path.sep as the delimiter, as we do not want linux-arm64 to get compiled with search criteria is linux-arm. if ( preBuildsFoldersToCopy.some((folder) => resourcePath.includes(`${folder.toLowerCase()}/`), ) ) { return true; } return false; } const extensionFolder = path.join(__dirname); async function deleteUnnecessaryZeromqPrebuilts() { const vsceTarget = process.env.VSC_VSCE_TARGET || process.env.VSCE_TARGET; if (!vsceTarget) { // Keep all of them, as we're not building platform specific bundles. console.log("vsceTarget is not set"); return; } console.log("deleting ZeroMQ prebuilts"); const necessaryPrebuilds = getZeroMQPreBuildsFoldersToKeep(); const zmqPrebuildFolder = path.join( extensionFolder, "dist", "node_modules", "zeromq", "prebuilds", ); // delete all folders except the ones in necessaryPrebuilds const files = fs.readdirSync(zmqPrebuildFolder); for (const file of files) { if (!necessaryPrebuilds.includes(file)) { console.log("deleting", path.join(zmqPrebuildFolder, file)); fs.rmSync(path.join(zmqPrebuildFolder, file), { recursive: true }); } } console.log("copied ZeroMQ"); } /** * Map VSC_VSCE_TARGET to the altimate-core platform package suffix to keep. * Returns empty array if no target is set (keep all). */ function getAltimateCorePackagesToKeep() { const vsceTarget = process.env.VSC_VSCE_TARGET || process.env.VSCE_TARGET; if (!vsceTarget) { return []; } if (vsceTarget.includes("darwin")) { if (vsceTarget.includes("arm64")) return ["darwin-arm64"]; if (vsceTarget.includes("x64")) return ["darwin-x64"]; return ["darwin-arm64", "darwin-x64"]; } if (vsceTarget.includes("linux") || vsceTarget.includes("alpine")) { if (vsceTarget.includes("arm64")) return ["linux-arm64-gnu"]; if (vsceTarget.includes("x64")) return ["linux-x64-gnu"]; return ["linux-arm64-gnu", "linux-x64-gnu"]; } if (vsceTarget.includes("win32")) { return ["win32-x64-msvc"]; } return []; } function deleteUnnecessaryAltimateCorePackages() { const vsceTarget = process.env.VSC_VSCE_TARGET || process.env.VSCE_TARGET; if (!vsceTarget) { console.log( "vsceTarget is not set, keeping all altimate-core platform packages", ); return; } console.log( "pruning altimate-core platform packages for target:", vsceTarget, ); const keepSuffixes = getAltimateCorePackagesToKeep(); const altimateCoreDir = path.join( extensionFolder, "dist", "node_modules", "@altimateai", ); if (!fs.existsSync(altimateCoreDir)) { console.log("altimate-core dist directory not found, skipping"); return; } const entries = fs.readdirSync(altimateCoreDir); for (const entry of entries) { // Only prune platform-specific packages (altimate-core-) if (!entry.startsWith("altimate-core-")) continue; const suffix = entry.replace("altimate-core-", ""); if (!keepSuffixes.includes(suffix)) { const fullPath = path.join(altimateCoreDir, entry); console.log("deleting", fullPath); fs.rmSync(fullPath, { recursive: true }); } } // Also prune .node files from the altimate-core/ directory that don't // match the target platform (these are copied by webpack for direct resolution) const coreDir = path.join(altimateCoreDir, "altimate-core"); if (fs.existsSync(coreDir)) { const coreEntries = fs.readdirSync(coreDir); for (const file of coreEntries) { if (!file.endsWith(".node")) continue; // e.g. "altimate-core.darwin-arm64.node" → check if "darwin-arm64" is kept const match = file.match(/^altimate-core\.(.+)\.node$/); if (match && !keepSuffixes.includes(match[1])) { const fullPath = path.join(coreDir, file); console.log("deleting .node file", fullPath); fs.rmSync(fullPath); } } } console.log("pruned altimate-core platform packages"); } deleteUnnecessaryZeromqPrebuilts(); deleteUnnecessaryAltimateCorePackages(); ================================================ FILE: rsbuild.config.ts ================================================ import { defineConfig, RsbuildPlugin } from "@rsbuild/core"; import { cpSync, existsSync, readdirSync } from "fs"; import path from "path"; const DIST = path.resolve(__dirname, "dist"); const copyAssetsPlugin: RsbuildPlugin = { name: "copy-assets-plugin", setup(api) { api.onBeforeBuild(() => { const patterns = [ { from: path.resolve(__dirname, "altimate_notebook_kernel.py"), to: path.join( DIST, "altimate_python_packages/altimate_notebook_kernel.py", ), }, { from: path.resolve( __dirname, "node_modules/@altimateai/dbt-integration/dist/node_python_bridge.py", ), to: path.join(DIST, "node_python_bridge.py"), }, { from: path.resolve( __dirname, "node_modules/@altimateai/dbt-integration/dist/altimate_python_packages/dbt_core_integration.py", ), to: path.join( DIST, "altimate_python_packages/dbt_core_integration.py", ), }, { from: path.resolve( __dirname, "node_modules/@altimateai/dbt-integration/dist/altimate_python_packages/dbt_utils.py", ), to: path.join(DIST, "altimate_python_packages/dbt_utils.py"), }, { from: path.resolve( __dirname, "node_modules/@altimateai/dbt-integration/dist/altimate_python_packages/altimate_packages/", ), to: path.join(DIST, "altimate_python_packages/altimate_packages/"), }, ]; // These assets are runtime-required by the extension (Python bridge, // kernel, and altimate-dbt-integration Python packages). Abort the // build if any of them is missing — matches webpack's CopyPlugin // default (noErrorOnMissing: false) that this code replaced. for (const { from, to } of patterns) { try { cpSync(from, to, { recursive: true }); } catch (error) { throw new Error( `Required asset missing: failed to copy ${from} -> ${to}: ${ (error as Error).message }`, ); } } console.log("copying notebook modules"); try { cpSync( path.resolve(__dirname, "node_modules/zeromq"), path.join(DIST, "node_modules/zeromq"), { recursive: true }, ); } catch (e) { console.warn(`Skipping zeromq: ${(e as Error).message}`); } try { const altimateCoreDir = path.dirname( require.resolve("@altimateai/core/package.json"), ); cpSync( altimateCoreDir, path.join(DIST, "node_modules/@altimateai/core"), { recursive: true }, ); } catch (e) { console.warn(`Skipping @altimateai/core: ${(e as Error).message}`); } try { cpSync( path.resolve(__dirname, "node_modules/@altimateai/altimate-core"), path.join(DIST, "node_modules/@altimateai/altimate-core"), { recursive: true }, ); console.log("Copied @altimateai/altimate-core"); } catch (e) { console.warn( `Skipping @altimateai/altimate-core: ${(e as Error).message}`, ); } // Copy only the .node binary directly into the altimate-core/ directory — // skip the platform package directories entirely. napi-rs index.js falls back // to require('./altimate-core..node') when the platform package // isn't found, so this works and halves VSIX size. const altimatePlatformPackages = [ "@altimateai/altimate-core-darwin-arm64", "@altimateai/altimate-core-darwin-x64", "@altimateai/altimate-core-linux-arm64-gnu", "@altimateai/altimate-core-linux-x64-gnu", "@altimateai/altimate-core-win32-x64-msvc", ]; const coreDistDir = path.join( DIST, "node_modules/@altimateai/altimate-core", ); for (const pkg of altimatePlatformPackages) { const srcDir = path.resolve(__dirname, "node_modules", pkg); try { if (!existsSync(srcDir)) { continue; } for (const file of readdirSync(srcDir)) { if (file.endsWith(".node")) { cpSync(path.join(srcDir, file), path.join(coreDistDir, file)); console.log( `Copied ${file} into altimate-core/ (skipped platform package dir)`, ); } } } catch (e) { console.warn(`Skipping ${pkg}: ${(e as Error).message}`); } } try { cpSync( path.resolve(__dirname, "node_modules/@aminya/node-gyp-build"), path.join(DIST, "node_modules/@aminya/node-gyp-build"), { recursive: true }, ); cpSync( path.resolve(__dirname, "node_modules/node-gyp-build"), path.join(DIST, "node_modules/node-gyp-build"), { recursive: true }, ); } catch (e) { console.warn(`Skipping node-gyp-build: ${(e as Error).message}`); } console.log("copied notebook modules"); }); }, }; export default defineConfig({ source: { entry: { extension: "./src/extension.ts", }, }, output: { target: "node", filename: { js: "[name].js", }, distPath: { root: "dist", }, // Webview panels also emit into dist/, so don't wipe it. cleanDistPath: false, sourceMap: { js: "source-map", }, // Match the pre-rsbuild terser config (mangle: false, keep classnames/fnames) // so stack traces in VS Code remain actionable, while still getting dead-code // elimination and whitespace stripping. minify: { js: true, jsOptions: { minimizerOptions: { mangle: false, compress: { keep_classnames: true, keep_fnames: true, }, }, }, }, }, resolve: { alias: { "@extension": path.resolve(__dirname, "./src/modules.ts"), "@lib": path.resolve(__dirname, "./src/lib/index"), }, extensions: [".ts", ".js"], }, performance: { chunkSplit: { strategy: "all-in-one", }, }, dev: { writeToDisk: true, }, tools: { rspack: (config) => { config.externals = [ "vscode", // Ignored because we don't use them, and App Insights has try/catch // guarding their loading: https://github.com/microsoft/vscode-extension-telemetry/issues/41#issuecomment-598852991 "applicationinsights-native-metrics", "@opentelemetry/tracing", "@azure/opentelemetry-instrumentation-azure-sdk", "@opentelemetry/instrumentation", "@azure/functions-core", "zeromq", "@altimateai/core", "@altimateai/altimate-core", "@altimateai/altimate-core-darwin-arm64", "@altimateai/altimate-core-darwin-x64", "@altimateai/altimate-core-linux-arm64-gnu", "@altimateai/altimate-core-linux-x64-gnu", "@altimateai/altimate-core-win32-x64-msvc", ]; config.node = { __dirname: false }; config.output = { ...config.output, libraryTarget: "commonjs2", devtoolModuleFilenameTemplate: "../[resource-path]", }; // Use ts-loader so inversify's decorators + emitDecoratorMetadata keep working. config.module = config.module || {}; config.module.rules = (config.module.rules || []).filter((rule: any) => { if (rule && typeof rule === "object" && rule.test instanceof RegExp) { return !rule.test.test("file.ts"); } return true; }); config.module.rules.push({ test: /\.ts$/, exclude: /(node_modules|src\/test)/, use: [{ loader: "ts-loader" }], }); return config; }, }, plugins: [copyAssetsPlugin], }); ================================================ FILE: snippets/snippets_markdown.json ================================================ { "Docs": { "prefix": "docs", "body": [ "{% docs ${1:name} %}", "$2", "{% enddocs %}" ], "description": "Docs" } } ================================================ FILE: snippets/snippets_sql.json ================================================ { "block": { "prefix": "block", "body": [ "{% block ${1:name} %}", " $2", "{% endblock %}" ], "description": "Block" }, "inline block": { "prefix": "inblock", "body": [ "{% $1 %}" ], "description": "Inline Block" }, "Comment": { "prefix": "comment", "body": [ "{#- $1 -#}" ], "description": "Inline Comment" }, "Complete Variable": { "prefix": "cvar", "body": [ "{{ $1 }}" ], "description": "Complete Variable" }, "Do": { "prefix": "do", "body": [ "{% do $1 %}" ], "description": "Jinja Do" }, "Filter": { "prefix": "filter", "body": [ "{% filter ${1:filter} %}", " $2", "{% endfilter %}" ], "description": "Jinja Filter" }, "For": { "prefix": "for", "body": [ "{% for ${1:item} in ${2:sequence} %}", " $3", "{% endfor %}" ], "description": "Jinja For" }, "For (Dict)": { "prefix": "for dict", "body": [ "{% for ${1:key}, ${2:value} in ${3:dict}.items() %}", " $4", "{% endfor %}" ], "description": "Jinja For (Dict)" }, "For (List)": { "prefix": "for list", "body": [ "{% for ${1:item} in [${2:list}] %}", " $3", "{% endfor %}" ], "description": "Jinja For (List)" }, "If": { "prefix": "if", "body": [ "{% if ${1:expr} %}", " $2", "{% endif %}" ], "description": "Jinja If" }, "Macro": { "prefix": "macro", "body": [ "{% macro ${1:name}(${2:args}) %}", " $3", "{% endmacro %}" ], "description": "Jinja macro" }, "Set": { "prefix": "set", "body": "{% set ${1:var} = ${2:value} %}", "description": "Jinja set variable" }, "Set Block": { "prefix": "set_block", "body": [ "{% set ${1:name} %}", " $2", "{% endset %}" ] }, "Snapshot": { "prefix": [ "snapshot", "snap" ], "body": [ "{% snapshot ${1:snapshot_name} %}", "", "{{", " config(", " target_database='${2:target_database}',", " target_schema='${3:target_schema}',", " unique_key='${4:unique_key}',", "", " strategy='${5:strategy}',", " updated_at='${6:updated_at}',", " )", "}}", "", "", "{% endsnapshot %}" ], "description": "Dbt snapshot" }, "Dbt Config Block": { "prefix": "config", "body": [ "{{", " config(", " materialized = '${1:table}',", " )", "}}" ], "description": "Dbt Config Block" }, "Dbt Incremental Config Block": { "prefix": "config_incremental", "body": [ "{{", " config(", " materialized = 'incremental',", " unique_key = '${1:id}',", " )", "}}" ], "description": "Dbt Incremental Config Block" }, "Env Var": { "prefix": "env_var", "body": [ "{{ env_var(\"${1:env_var_name}\") }}" ], "description": "Env Var" }, "Model Overview": { "prefix": "model_overview", "body": [ "{#-", "Goal: ${1:high-level objective of the model}", "Granularity: ${2:level of detail of the model}", "Assumptions/Caveats:", " ${3:- assumptions/caveats if any}", "-#}" ], "description": "Model Overview" }, "Load Result": { "prefix": "load_result", "body": [ "load_result('${1:statement_name}')" ], "description": "Load Result" }, "Log": { "prefix": "log", "body": [ "{{ log(${1:var}, info=${2|True,False|}) }}" ], "description": "Log" }, "Log a variable": { "prefix": "log_var", "body": [ "{{ log('${1:var}: ' ~ ${1:var}, info=${2|True,False|}) }}" ], "description": "Log a single variable's name and value" }, "Statement": { "prefix": "statement", "body": [ "{% call statement(${1:name}, fetch_result=${2|True,False|}) %}", " ${3:select 1}", "{% endcall %}" ], "description": "Statement" }, "Ref": { "prefix": "ref", "body": [ "{{ ref('${1:model_name}') }}" ], "description": "Ref" }, "Return": { "prefix": "return", "body": [ "{{ return(${1}) }}" ], "description": "Return" }, "Var": { "prefix": "var", "body": [ "{{ var('${1:var_name}') }}" ], "description": "Var" }, "Source": { "prefix": "source", "body": [ "{{ source('${1:source}', '${2:table_name}') }}" ], "description": "Source" }, "Incremental DBT block": { "prefix": "is_incremental", "body": [ "{% if is_incremental() %}", " and ${1:prefix}.${2:date_col} >= coalesce((select max(${2:date_col}) from {{ this }}), '1900-01-01')", "{% else %}", " ${3:optional_cond}", "{% endif %}" ], "description": "Incremental DBT Block" } } ================================================ FILE: snippets/snippets_yaml.json ================================================ { "Doc": { "prefix": "doc", "body": [ "{{ doc('${1:docs_name}') }}" ], "description": "Doc" }, "Env Var": { "prefix": "env_var", "body": [ "{{ env_var('${1:env_var_name}') }}" ], "description": "Env Var" }, "Ref": { "prefix": "ref", "body": [ "ref('${1:model_name}')" ], "description": "Ref" }, "Source": { "prefix": "source", "body": [ "source('${1:source}', '${2:table_name}')" ], "description": "Source" }, "Var": { "prefix": "var", "body": [ "{{ var('${1:var_name}') }}" ], "description": "Var" } } ================================================ FILE: src/altimate.ts ================================================ import { AltimateHttpClient, ColumnMetaData, DBTConfiguration, DBTTerminal, NodeMetaData, SourceMetaData, } from "@altimateai/dbt-integration"; import { NotebookItem, NotebookSchema, PreconfiguredNotebookItem } from "@lib"; import { inject } from "inversify"; import type { RequestInit } from "node-fetch"; import * as vscode from "vscode"; export class UserInputError extends Error {} export interface ColumnLineage { source: { uniqueId: string; column_name: string }; target: { uniqueId: string; column_name: string }; type: string; views_type?: string; views_code?: string[]; } interface Schemas { [key: string]: { [key: string]: unknown }; } export type ModelNode = { database: string; schema: string; name: string; alias: string; uniqueId: string; columns: { [columnName: string]: ColumnMetaData }; path: string | undefined; }; export type ModelInfo = { model_node: ModelNode; compiled_sql?: string; raw_sql?: string; }; export interface DBTColumnLineageRequest { model_dialect: string; targets: { uniqueId: string; column_name: string }[]; model_info: ModelInfo[]; upstream_expansion: boolean; upstream_models: string[]; selected_column: { model_node?: ModelNode; column: string }; session_id: string; show_indirect_edges: boolean; event_type: string; } export interface DBTColumnLineageResponse { column_lineage: ColumnLineage[]; confidence?: { confidence: string; message?: string }; errors?: string[]; errors_dict?: Record; } interface SQLLineageRequest { model_dialect: string; model_info: { model_node: ModelNode }[]; compiled_sql: string; session_id: string; } export type SqlLineageDetails = Record< string, { name: string; type: string; nodeType?: string; nodeId?: string; sql: string; columns: { name: string; datatype?: string; expression?: string }[]; } >; type SqlLineageResponse = { tableEdges: [string, string][]; details: SqlLineageDetails; nodePositions?: Record; }; interface SQLToModelRequest { sql: string; adapter: string; models: NodeMetaData[]; sources: SourceMetaData[]; } interface DBTProjectHealthConfig { id: number; name: string; description: string; created_on: string; config: Record; config_schema: unknown[]; } interface DBTProjectHealthConfigResponse { items: DBTProjectHealthConfig[]; } export interface SQLToModelResponse { sql: string; } interface NotebooksResponse { notebooks: PreconfiguredNotebookItem[]; } interface AddNotebookRequest { name: string; description: string; tags_list: string[]; data?: NotebookSchema; } interface UpdateNotebookRequest { name: string; description?: string; tags_list?: string[]; data?: NotebookSchema; } interface OnewayFeedback { feedback_value: "good" | "bad"; feedback_text: string; feedback_src: "dbtpu-extension"; data: any; } export enum QueryAnalysisType { EXPLAIN = "explain", FIX = "fix", MODIFY = "modify", TRANSLATE = "translate", } export enum QueryAnalysisChatType { SYSTEM = "SystemMessage", HUMAN = "HumanMessage", } interface QueryAnalysisChat { type: QueryAnalysisChatType; content: string; additional_kwargs?: Record; } export interface QueryTranslateRequest { sql: string; target_dialect: string; source_dialect: string; } export interface QueryTranslateExplanationRequest { user_sql: string; translated_sql: string; target_dialect: string; source_dialect: string; } interface DbtModel { model_name: string; model_description?: string; compiled_sql?: string; columns: { column_name: string; description?: string; data_type?: string; }[]; adapter?: string; } export interface QueryBookmark { id: number; compiled_sql: string; raw_sql: string; name: string; adapter_type: string; created_on: string; updated_on: string; tags: { id: number; tag: string }[]; } export interface QueryAnalysisRequest { session_id: string; job_type: QueryAnalysisType; model: DbtModel; user_request?: string; // required for modify query history?: QueryAnalysisChat[]; } export interface CreateDbtTestRequest { session_id: string; model: DbtModel; column_name?: string; user_request?: string; } interface DocsGenerateModelRequestV2 { columns: string[]; dbt_model: DbtModel; user_instructions?: { prompt_hint: string; language: string; persona: string; }; follow_up_instructions?: { instruction: string; }; prompt_hint?: string; gen_model_description: boolean; column_index_count: number | undefined; session_id: string | undefined; is_bulk_gen: boolean; } export interface DocsGenerateResponse { column_descriptions?: { column_name: string; column_description: string; column_citations?: { id: string; content: string }[]; }[]; model_description?: string; model_citations?: { id: string; content: string }[]; } export interface DBTCoreIntegrationEnvironment { id: number; environment_type: string; created_at: string; } export interface SyncHistoryItem { type: "Completed" | "In Progress" | "Failed"; time: string; log_file: string | null; } export interface DBTCoreIntegration { id: number; name: string; environments: DBTCoreIntegrationEnvironment[]; created_at: string; last_modified_at: string; last_file_upload_time: string | null; is_deleted: boolean; integration_type: "dbt_core" | "dbt_cloud"; } export interface DBTCoreIntegrationWithSync extends DBTCoreIntegration { sync_history: SyncHistoryItem[]; } export interface TenantUser { id: number; uuid: string; display_name: string; first_name: string; last_name: string; email: string; phone: string; is_active: boolean; is_verified: boolean; is_invited: boolean; is_onboarded: boolean; created_at: string; role_title: string; } interface FeedbackResponse { ok: boolean; } interface BulkDocsPropRequest { num_columns: number; session_id: string; } export interface SharedDoc { share_id: number; name: string; description: string; project_name: string; conversation_group: [ConversationGroup]; } export interface Conversation { conversation_id: number; message: string; timestamp: string; user_id: number; } export interface ConversationGroup { conversation_group_id: number; owner: number; status: "Pending" | "Resolved"; meta: { field?: "description"; column?: string; highlight: string; uniqueId?: string; filePath: string; resource_type?: string; range: | { end: vscode.Range["end"]; start: vscode.Range["start"]; } | undefined; }; conversations: Conversation[]; } export class AltimateRequest { constructor( private dbtTerminal: DBTTerminal, @inject("DBTConfiguration") private dbtConfiguration: DBTConfiguration, private altimateHttpClient: AltimateHttpClient, ) {} public getAltimateUrl(): string { return this.altimateHttpClient.getAltimateUrl(); } private async internalFetch(url: string, init?: RequestInit) { return this.altimateHttpClient.internalFetch(url, init); } getInstanceName() { return this.dbtConfiguration.getAltimateInstanceName(); } getAIKey() { return this.dbtConfiguration.getAltimateAiKey(); } public enabled(): boolean { return !!this.altimateHttpClient.getConfig(); } async fetchAsStream( endpoint: string, request: R, onProgress: (response: string) => void, timeout: number = 120000, ) { return this.altimateHttpClient.fetchAsStream( endpoint, request, onProgress, timeout, ); } async uploadToS3( endpoint: string, fetchArgs: Record, filePath: string, ) { return this.altimateHttpClient.uploadToS3(endpoint, fetchArgs, filePath); } private throwIfLocalMode(endpoint: string) { try { this.altimateHttpClient.throwIfLocalMode(endpoint); } catch (error) { // Apply additional local mode exceptions specific to AltimateRequest endpoint = endpoint.split("?")[0]; if ( [ /^dbtconfig\/datapilot_version\/.*$/, /^dbtconfig\/.*\/download$/, ].some((regex) => regex.test(endpoint)) ) { return; } if ( [ "auth_health", "dbtconfig", "dbt/v1/fetch_artifact_url", "dbtconfig/extension/start_scan", "dbt/v1/project_integrations", "dbt/v1/defer_to_prod_event", "dbt/v3/validate-credentials", ].includes(endpoint) ) { return; } throw error; } } async fetch( endpoint: string, fetchArgs = {}, timeout: number = 120000, ): Promise { this.dbtTerminal.debug("network:request", endpoint, fetchArgs); this.throwIfLocalMode(endpoint); return this.altimateHttpClient.fetch( endpoint, fetchArgs as RequestInit, timeout, ); } private getQueryString = ( params: Record, ): string => { const queryString = Object.keys(params) .map( (key) => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`, ) .join("&"); return queryString ? `?${queryString}` : ""; }; async isAuthenticated() { try { await this.fetch("auth_health", { method: "POST", }); } catch (error) { return false; } return true; } async generateModelDocsV2(docsGenerate: DocsGenerateModelRequestV2) { return this.fetch("dbt/v2", { method: "POST", body: JSON.stringify(docsGenerate), }); } async sendFeedback(feedback: OnewayFeedback) { return await this.fetch("feedbacks/ai/fb", { method: "POST", body: JSON.stringify(feedback), }); } async getColumnLevelLineage(req: DBTColumnLineageRequest) { return this.fetch("dbt/v4/lineage", { method: "POST", body: JSON.stringify(req), }); } async runModeller(req: SQLToModelRequest) { return this.fetch("dbt/v1/sqltomodel", { method: "POST", body: JSON.stringify(req), }); } async validateCredentials(instance: string, key: string, baseUrl?: string) { const url = `${baseUrl || this.getAltimateUrl()}/dbt/v3/validate-credentials`; try { const response = await fetch(url, { method: "GET", headers: { "x-tenant": instance, Authorization: "Bearer " + key, "Content-Type": "application/json", }, }); if (!response.ok) { if (response.status === 401 || response.status === 403) { return { error: "Invalid credentials. Please check your API key and instance name.", }; } return { error: `Validation failed with status ${response.status}`, }; } return (await response.json()) as Record | undefined; } catch (error) { return { error: error instanceof Error ? error.message : "Failed to validate credentials. Please check your network connection.", }; } } async createDbtIntegration( instanceName: string, apiKey: string, name: string, environment: string, integrationType: "dbt_core" | "dbt_cloud", ) { const url = `${this.getAltimateUrl()}/dbt/v1/project_integration`; const response = await fetch(url, { method: "POST", headers: { "x-tenant": instanceName, Authorization: "Bearer " + apiKey, "Content-Type": "application/json", }, body: JSON.stringify({ name, environment, integration_type: integrationType, }), }); if (!response.ok) { const errorData = (await response.json().catch(() => ({}))) as Record< string, unknown >; throw new Error( (errorData.message as string) || `Failed to create dbt integration: ${response.statusText}`, ); } return (await response.json()) as { dbt_core_integration_id?: number; dbt_cloud_integration_id?: number; integration_type: string; }; } async checkApiConnectivity() { const url = `${this.getAltimateUrl()}/health`; try { const response = await this.internalFetch(url, { method: "GET" }); const { status } = (await response.json()) as { status: string }; return { status }; } catch (e) { this.dbtTerminal.error( "checkApiConnectivity", "Unable to connect to backend", e, true, { url }, ); const errorMsg = e instanceof Error ? e.message : JSON.stringify(e); return { status: "not-ok", errorMsg }; } } async fetchProjectIntegrations() { return this.fetch("dbt/v1/project_integrations"); } async fetchProjectIntegrationWithSync( integrationId: number, environment: string, ) { return this.fetch( `dbt/v1/project_integrations/${integrationId}/${environment}`, ); } async getHealthcheckConfigs() { return this.fetch( `dbtconfig${this.getQueryString({ size: "100" })}`, ); } async logDBTHealthcheckConfig(configId: string) { return this.fetch(`dbtconfig/${configId}/download`); } async logDBTHealthcheckStartScan() { return this.fetch(`dbtconfig/extension/start_scan`); } async getDatapilotVersion(extension_version: string) { return this.fetch<{ altimate_datapilot_version: string }>( `dbtconfig/datapilot_version/${extension_version}`, ); } async getUsersInTenant() { return await this.fetch("users/chat"); } async getCurrentUser() { return await this.fetch("dbt/dbt_docs_share/user/details"); } async getAllSharedDbtDocs(projectNames: string[]) { const params = new URLSearchParams(); projectNames.forEach((p) => params.append("projects", p)); return await this.fetch( `dbt/dbt_docs_share/all?${params.toString()}`, ); } async getAppUrlByShareId(shareId: SharedDoc["share_id"]) { return await this.fetch<{ name: string; app_url: string; }>(`dbt/dbt_docs_share/${shareId}`, { method: "GET", }); } async addConversationToGroup( shareId: SharedDoc["share_id"], conversationGroupId: ConversationGroup["conversation_group_id"], message: string, ) { return await this.fetch<{ ok: boolean }>( `dbt/dbt_docs_share/${shareId}/conversation_group/${conversationGroupId}/conversation`, { method: "POST", body: JSON.stringify({ message, }), }, ); } async createConversationGroup( shareId: SharedDoc["share_id"], data: Partial & { message: string }, ) { return await this.fetch<{ conversation_group_id: ConversationGroup["conversation_group_id"]; conversation_id: Conversation["conversation_id"]; }>(`dbt/dbt_docs_share/${shareId}/conversation_group`, { method: "POST", body: JSON.stringify(data), }); } async resolveConversation( shareId: SharedDoc["share_id"], conversationGroupId: ConversationGroup["conversation_group_id"], ) { return await this.fetch<{ ok: boolean }>( `dbt/dbt_docs_share/${shareId}/conversation_group/${conversationGroupId}/resolve`, { method: "POST", body: JSON.stringify({ resolved: true }) }, ); } async loadConversationsByShareId(shareId: SharedDoc["share_id"]) { return await this.fetch<{ dbt_docs_share_conversations: ConversationGroup[]; }>(`dbt/dbt_docs_share/${shareId}/conversations`); } async createDbtDocsShare( data: { name: string; description?: string; }, projectName: string, ) { return await this.fetch<{ share_id: number; manifest_presigned_url: string; catalog_presigned_url: string; }>("dbt/dbt_docs_share", { method: "POST", body: JSON.stringify({ description: data.description, name: data.name, project_name: projectName, }), }); } async verifyDbtDocsUpload(share_id: number) { return this.fetch<{ dbt_docs_share_url: string; }>("dbt/dbt_docs_share/verify_upload/", { method: "POST", body: JSON.stringify({ share_id }), }); } async getQueryBookmarks() { return await this.fetch(`query/bookmark`); } async sqlLineage(req: SQLLineageRequest) { return this.fetch("dbt/v3/sql_lineage", { method: "POST", body: JSON.stringify(req), }); } async bulkDocsPropCredit(req: BulkDocsPropRequest) { return this.fetch("dbt/v4/bulk-docs-prop-credits", { method: "POST", body: JSON.stringify(req), }); } async getPreConfiguredNotebooks() { return this.fetch( "notebook/preconfigured/list", { method: "GET", }, ); } async getNotebooks( name: string = "", tags_list: string[] = [], privacy: string = "private", ) { const params = new URLSearchParams({ name, privacy, ...(tags_list.length > 0 && { tags_list: tags_list.join(",") }), }); return this.fetch(`notebook/list?${params.toString()}`, { method: "GET", }); } async addNotebook(req: AddNotebookRequest) { return this.fetch("notebook", { method: "POST", body: JSON.stringify(req), }); } async deleteNotebook(id: number) { return this.fetch(`notebook/${id}`, { method: "DELETE", }); } async updateNotebook(id: number, req: UpdateNotebookRequest) { return this.fetch(`notebook/${id}`, { method: "PUT", body: JSON.stringify(req), }); } async updateNotebookPrivacy(id: number, privacy: string) { const params = new URLSearchParams({ privacy: privacy }); return this.fetch( `notebook/privacy/${id}?${params.toString()}`, { method: "PUT", }, ); } async trackBulkTestGen(sessionId: string) { return this.fetch<{ ok: boolean }>(`dbt/v2/bulk_test_gen`, { method: "POST", body: JSON.stringify({ session_id: sessionId }), }); } } ================================================ FILE: src/autocompletion_provider/docAutocompletionProvider.ts ================================================ import { CancellationToken, CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, Disposable, Position, ProviderResult, TextDocument, Uri, } from "vscode"; import { DBTProjectContainer } from "../dbt_client/dbtProjectContainer"; import { ManifestCacheChangedEvent } from "../dbt_client/event/manifestCacheChangedEvent"; import { TelemetryService } from "../telemetry"; import { isEnclosedWithinCodeBlock } from "../utils"; export class DocAutocompletionProvider implements CompletionItemProvider, Disposable { private static readonly ENDS_WITH_DOC = /doc\(['|"]$/; private docAutocompleteNameItemsMap: Map = new Map(); private disposables: Disposable[] = []; constructor( private dbtProjectContainer: DBTProjectContainer, private telemetry: TelemetryService, ) { this.disposables.push( dbtProjectContainer.onManifestChanged((event) => this.onManifestCacheChanged(event), ), ); } dispose() { while (this.disposables.length) { const x = this.disposables.pop(); if (x) { x.dispose(); } } } provideCompletionItems( document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext, ): ProviderResult> { const linePrefix = document .lineAt(position) .text.substr(0, position.character); if (!isEnclosedWithinCodeBlock(document, position)) { return undefined; } const projectRootpath = this.dbtProjectContainer.getProjectRootpath( document.uri, ); if (projectRootpath === undefined) { return; } if (linePrefix.match(DocAutocompletionProvider.ENDS_WITH_DOC)) { return this.showDocNameAutocompletionItems(projectRootpath); } return undefined; } private onManifestCacheChanged(event: ManifestCacheChangedEvent): void { event.added?.forEach((added) => { this.docAutocompleteNameItemsMap.set( added.project.projectRoot.fsPath, Array.from(added.docMetaMap.keys()).map( (docName) => new CompletionItem(docName, CompletionItemKind.File), ), ); }); event.removed?.forEach((removed) => { this.docAutocompleteNameItemsMap.delete(removed.projectRoot.fsPath); }); } private showDocNameAutocompletionItems(projectRootpath: Uri) { this.telemetry.sendTelemetryEvent("provideDocAutocompletion"); return this.docAutocompleteNameItemsMap.get(projectRootpath.fsPath); } } ================================================ FILE: src/autocompletion_provider/index.ts ================================================ import { Disposable, languages } from "vscode"; import { DBTPowerUserExtension } from "../dbtPowerUserExtension"; import { DocAutocompletionProvider } from "./docAutocompletionProvider"; import { MacroAutocompletionProvider } from "./macroAutocompletionProvider"; import { ModelAutocompletionProvider } from "./modelAutocompletionProvider"; import { SourceAutocompletionProvider } from "./sourceAutocompletionProvider"; import { UserCompletionProvider } from "./usercompletion_provider"; export class AutocompletionProviders implements Disposable { private disposables: Disposable[] = []; constructor( private macroAutocompletionProvider: MacroAutocompletionProvider, private modelAutocompletionProvider: ModelAutocompletionProvider, private sourceAutocompletionProvider: SourceAutocompletionProvider, private docAutocompletionProvider: DocAutocompletionProvider, private userCompletionProvider: UserCompletionProvider, ) { this.disposables.push( languages.registerCompletionItemProvider( DBTPowerUserExtension.DBT_SQL_SELECTOR, this.macroAutocompletionProvider, ), languages.registerCompletionItemProvider( DBTPowerUserExtension.DBT_SQL_SELECTOR, this.modelAutocompletionProvider, ".", "(", '"', "'", ), languages.registerCompletionItemProvider( DBTPowerUserExtension.DBT_SQL_SELECTOR, this.sourceAutocompletionProvider, ".", "(", '"', "'", ), languages.registerCompletionItemProvider( DBTPowerUserExtension.DBT_YAML_SELECTOR, this.docAutocompletionProvider, ".", "(", '"', "'", ), // enabled only for markdowns - to work for comment inputs languages.registerCompletionItemProvider( { language: "markdown" }, this.userCompletionProvider, "@", ), ); } dispose() { while (this.disposables.length) { const x = this.disposables.pop(); if (x) { x.dispose(); } } } } ================================================ FILE: src/autocompletion_provider/macroAutocompletionProvider.ts ================================================ import { CancellationToken, CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, Disposable, Position, ProviderResult, TextDocument, Uri, } from "vscode"; import { DBTProjectContainer } from "../dbt_client/dbtProjectContainer"; import { ManifestCacheChangedEvent } from "../dbt_client/event/manifestCacheChangedEvent"; import { TelemetryService } from "../telemetry"; import { isEnclosedWithinCodeBlock } from "../utils"; // TODO autocomplete doesn't work when mistype, delete and retype export class MacroAutocompletionProvider implements CompletionItemProvider, Disposable { private macrosAutocompleteMap: Map = new Map(); private disposables: Disposable[] = []; constructor( private dbtProjectContainer: DBTProjectContainer, private telemetry: TelemetryService, ) { this.disposables.push( dbtProjectContainer.onManifestChanged((event) => this.onManifestCacheChanged(event), ), ); } dispose() { while (this.disposables.length) { const x = this.disposables.pop(); if (x) { x.dispose(); } } } provideCompletionItems( document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext, ): ProviderResult> { const range = document.getWordRangeAtPosition(position); if (range && isEnclosedWithinCodeBlock(document, range)) { return this.getAutoCompleteItems(document.uri); } return undefined; } private onManifestCacheChanged(event: ManifestCacheChangedEvent): void { event.added?.forEach((added) => { this.macrosAutocompleteMap.set( added.project.projectRoot.fsPath, Array.from(added.macroMetaMap.keys()).map((macro) => ({ label: macro, insertText: macro, kind: CompletionItemKind.Value, detail: "Macro", })), ); }); event.removed?.forEach((removed) => { this.macrosAutocompleteMap.delete(removed.projectRoot.fsPath); }); } private getAutoCompleteItems = (currentFilePath: Uri) => { const projectRootpath = this.dbtProjectContainer.getProjectRootpath(currentFilePath); if (projectRootpath === undefined) { return; } this.telemetry.sendTelemetryEvent("provideMacroAutocompletion"); return this.macrosAutocompleteMap.get(projectRootpath.fsPath); }; } ================================================ FILE: src/autocompletion_provider/modelAutocompletionProvider.ts ================================================ import { RESOURCE_TYPE_ANALYSIS } from "@altimateai/dbt-integration"; import { CancellationToken, CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, Disposable, Position, ProviderResult, TextDocument, Uri, } from "vscode"; import { DBTProjectContainer } from "../dbt_client/dbtProjectContainer"; import { ManifestCacheChangedEvent } from "../dbt_client/event/manifestCacheChangedEvent"; import { TelemetryService } from "../telemetry"; import { isEnclosedWithinCodeBlock } from "../utils"; // TODO autocomplete doesn't work when mistype, delete and retype export class ModelAutocompletionProvider implements CompletionItemProvider, Disposable { private static readonly MODEL_PATTERN = /ref\s*\(\s*(['"])?\s*\w*$/; private static readonly PACKAGE_PATTERN = /ref\s*\(\s*('[^)']*'|"[^)"]*")\s*,\s*(['"])?\s*\w*$/; private modelAutocompleteMap: Map< string, { projectName: string; packageName: string; modelName: string; }[] > = new Map(); private disposables: Disposable[] = []; constructor( private dbtProjectContainer: DBTProjectContainer, private telemetry: TelemetryService, ) { this.disposables.push( dbtProjectContainer.onManifestChanged((event) => this.onManifestCacheChanged(event), ), ); } dispose() { while (this.disposables.length) { const x = this.disposables.pop(); if (x) { x.dispose(); } } } provideCompletionItems( document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext, ): ProviderResult> { const line = document.lineAt(position).text; const linePrefix = line.substring(0, position.character); if (!isEnclosedWithinCodeBlock(document, position)) { return undefined; } const modelMatch = linePrefix.match( ModelAutocompletionProvider.MODEL_PATTERN, ); const packageMatch = linePrefix.match( ModelAutocompletionProvider.PACKAGE_PATTERN, ); if (!modelMatch && !packageMatch) { return undefined; } let autoCompleteItems = this.getAutoCompleteItems(document.uri); if (!autoCompleteItems) { return undefined; } if (modelMatch) { // capture group for first quote after parenthesis // can be one of [`'`, `"`, undefined] if (!modelMatch[1]) { // if no quotes surround insertText by " return autoCompleteItems.map((completionItem) => ({ label: `(${completionItem.packageName}) ${completionItem.modelName}`, kind: CompletionItemKind.Value, detail: "Model", insertText: completionItem.projectName === completionItem.packageName ? `"${completionItem.modelName}"` : `"${completionItem.packageName}", "${completionItem.modelName}"`, })); } // if quotes then add end quote to match start quote const endQuote = line[position.character] === modelMatch[1] ? "" : modelMatch[1]; return autoCompleteItems.map((completionItem) => ({ label: `(${completionItem.packageName}) ${completionItem.modelName}`, kind: CompletionItemKind.Value, detail: "Model", insertText: completionItem.projectName === completionItem.packageName ? `${completionItem.modelName}${endQuote}` : `${completionItem.packageName}${modelMatch[1]}, ${modelMatch[1]}${completionItem.modelName}${endQuote}`, })); } if (packageMatch) { const packageName = packageMatch[1].replace(/['"]/g, ""); autoCompleteItems = autoCompleteItems.filter( (completionItem) => completionItem.packageName === packageName, ); // capture group for second quote after parenthesis // can be one of [`'`, `"`, undefined] if (!packageMatch[2]) { return autoCompleteItems.map((completionItem) => ({ label: completionItem.modelName, kind: CompletionItemKind.Value, detail: "Model", insertText: `"${completionItem.modelName}"`, })); } // if quotes then add end quote to match start quote const endQuote = line[position.character] === packageMatch[2] ? "" : packageMatch[2]; return autoCompleteItems.map((completionItem) => ({ label: completionItem.modelName, kind: CompletionItemKind.Value, detail: "Model", insertText: `${completionItem.modelName}${endQuote}`, })); } return undefined; } private onManifestCacheChanged(event: ManifestCacheChangedEvent): void { event.added?.forEach((added) => { const project = added.project; const projectName = project.getProjectName(); const models = added.nodeMetaMap.nodes(); const autocompleteItems = Array.from(models) .filter((model) => model.resource_type !== RESOURCE_TYPE_ANALYSIS) .map((model) => ({ projectName, packageName: model.package_name, // TODO: fix this autocomplete to support for model version modelName: model.name, })); const uniqueItems: Record< string, { projectName: string; packageName: string; modelName: string; } > = {}; for (const item of autocompleteItems) { const key = `${item.projectName}|${item.packageName}|${item.modelName}`; if (!uniqueItems[key]) { uniqueItems[key] = item; } } this.modelAutocompleteMap.set( added.project.projectRoot.fsPath, Object.values(uniqueItems), ); }); event.removed?.forEach((removed) => { this.modelAutocompleteMap.delete(removed.projectRoot.fsPath); }); } private getAutoCompleteItems = (currentFilePath: Uri) => { const projectRootpath = this.dbtProjectContainer.getProjectRootpath(currentFilePath); if (projectRootpath === undefined) { const project = this.dbtProjectContainer.getFromWorkspaceState( "dbtPowerUser.projectSelected", ); if (!project?.uri) { return; } return this.modelAutocompleteMap.get(project.uri.fsPath); } this.telemetry.sendTelemetryEvent("provideModelAutocompletion"); return this.modelAutocompleteMap.get(projectRootpath.fsPath); }; } ================================================ FILE: src/autocompletion_provider/sourceAutocompletionProvider.ts ================================================ import { CancellationToken, CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, Disposable, Position, ProviderResult, TextDocument, Uri, } from "vscode"; import { DBTProjectContainer } from "../dbt_client/dbtProjectContainer"; import { ManifestCacheChangedEvent } from "../dbt_client/event/manifestCacheChangedEvent"; import { TelemetryService } from "../telemetry"; import { isEnclosedWithinCodeBlock } from "../utils"; // TODO autocomplete doesn't work when mistype, delete and retype export class SourceAutocompletionProvider implements CompletionItemProvider, Disposable { private static readonly GET_SOURCE_NAME = /(?!['"])(\w+)(?=['"])/; private static readonly ENDS_WITH_SOURCE = /source\(['|"]$/; private sourceAutocompleteNameItemsMap: Map = new Map(); private sourceAutocompleteTableMap: Map< string, Map > = new Map(); private disposables: Disposable[] = []; constructor( private dbtProjectContainer: DBTProjectContainer, private telemetry: TelemetryService, ) { this.disposables.push( dbtProjectContainer.onManifestChanged((event) => this.onManifestCacheChanged(event), ), ); } dispose() { while (this.disposables.length) { const x = this.disposables.pop(); if (x) { x.dispose(); } } } provideCompletionItems( document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext, ): ProviderResult> { const linePrefix = document .lineAt(position) .text.substr(0, position.character); if (!isEnclosedWithinCodeBlock(document, position)) { return undefined; } const projectRootpath = this.dbtProjectContainer.getProjectRootpath( document.uri, ); if (projectRootpath === undefined) { return; } if (linePrefix.match(SourceAutocompletionProvider.ENDS_WITH_SOURCE)) { return this.showSourceNameAutocompletionItems(projectRootpath); } if ( linePrefix.match(SourceAutocompletionProvider.GET_SOURCE_NAME) && linePrefix.includes("source") ) { const autoCompleteItems = this.showTableNameAutocompletionItems( linePrefix, projectRootpath, ); return autoCompleteItems; } return undefined; } private onManifestCacheChanged(event: ManifestCacheChangedEvent): void { event.added?.forEach((added) => { this.sourceAutocompleteNameItemsMap.set( added.project.projectRoot.fsPath, Array.from(added.sourceMetaMap.keys()).map( (source) => new CompletionItem(source, CompletionItemKind.File), ), ); const sourceTableMap: Map = new Map(); added.sourceMetaMap.forEach((value, key) => { const autocompleteItems = value.tables.map((item) => { return new CompletionItem(item.name, CompletionItemKind.File); }); sourceTableMap.set(key, autocompleteItems); }); this.sourceAutocompleteTableMap.set( added.project.projectRoot.fsPath, sourceTableMap, ); }); event.removed?.forEach((removed) => { this.sourceAutocompleteNameItemsMap.delete(removed.projectRoot.fsPath); this.sourceAutocompleteTableMap.delete(removed.projectRoot.fsPath); }); } private showSourceNameAutocompletionItems(projectRootpath: Uri) { return this.sourceAutocompleteNameItemsMap.get(projectRootpath.fsPath); } private showTableNameAutocompletionItems( linePrefix: string, projectRootpath: Uri, ) { const sourceNameMatch = linePrefix.match( SourceAutocompletionProvider.GET_SOURCE_NAME, ); if (sourceNameMatch !== null) { const sourceTableMap = this.sourceAutocompleteTableMap.get( projectRootpath.fsPath, ); if (sourceTableMap === undefined) { return; } this.telemetry.sendTelemetryEvent("provideSourceAutocompletion"); return sourceTableMap.get(sourceNameMatch[0]); } } } ================================================ FILE: src/autocompletion_provider/usercompletion_provider.ts ================================================ import { CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, Disposable, ProviderResult, } from "vscode"; import { UsersService } from "../services/usersService"; export class UserCompletionProvider implements CompletionItemProvider, Disposable { private disposables: Disposable[] = []; constructor(private usersService: UsersService) {} dispose() { while (this.disposables.length) { const x = this.disposables.pop(); if (x) { x.dispose(); } } } provideCompletionItems(): ProviderResult< CompletionItem[] | CompletionList > { return this.usersService.users.map((user) => ({ label: { label: `${user.display_name}`, // description: "Altimate", detail: " (Altimate)", }, kind: CompletionItemKind.User, keepWhitespace: true, insertText: `${user.display_name} `, })); } } ================================================ FILE: src/code_lens_provider/cteCodeLensProvider.ts ================================================ import { DBTTerminal } from "@altimateai/dbt-integration"; import { inject } from "inversify"; import { CancellationToken, CodeLens, CodeLensProvider, Command, Disposable, Range, TextDocument, } from "vscode"; export interface CteInfo { name: string; range: Range; queryRange: Range; index: number; // Order of CTE in the WITH clause withClauseStart: number; // Start position of the WITH clause } export class CteCodeLensProvider implements CodeLensProvider, Disposable { private disposables: Disposable[] = []; // Regex bounds constants to prevent catastrophic backtracking // These limits are based on realistic SQL formatting expectations and database constraints /** Maximum characters in quoted identifiers ("name", `name`, [name]) * Rationale: Most databases limit identifier length to 128-255 chars. * 500 chars covers most real-world cases while preventing excessive backtracking. */ private static readonly MAX_QUOTED_IDENTIFIER_LENGTH = 500; /** Maximum characters in CTE column list (id, name, description, etc.) * Rationale: Column lists with types and constraints can be lengthy. * 1000 chars accommodates complex column definitions in most practical scenarios. */ private static readonly MAX_COLUMN_LIST_LENGTH = 1000; constructor( @inject("DBTTerminal") private dbtTerminal: DBTTerminal, ) {} dispose() { while (this.disposables.length) { const x = this.disposables.pop(); if (x) { x.dispose(); } } } public provideCodeLenses( document: TextDocument, _token: CancellationToken, ): CodeLens[] | Thenable { try { // Only provide code lenses for SQL files if (!document.languageId.includes("sql")) { this.dbtTerminal.debug( "CteCodeLensProvider", `Skipping non-SQL file: ${document.languageId}`, ); return []; } this.dbtTerminal.debug( "CteCodeLensProvider", `Starting CTE detection for ${document.uri.fsPath}`, ); const ctes = this.detectCtes(document); const codeLenses: CodeLens[] = []; this.dbtTerminal.debug( "CteCodeLensProvider", `Found ${ctes.length} CTEs in document`, ); // Render both CodeLens actions on every CTE start line: // • Execute CTE: — per-CTE query preview // • ⏱ Profile CTEs — profiles all CTEs cumulatively // Profile is duplicated across lines so users don't have to scroll to // the top to trigger it; every invocation runs the full profile. for (const cte of ctes) { const runCteCommand: Command = { title: `$(play) Execute CTE: ${cte.name}`, command: "dbtPowerUser.runCteWithDependencies", arguments: [document.uri, cte.index, ctes], }; codeLenses.push(new CodeLens(cte.range, runCteCommand)); const profileCommand: Command = { title: "⏱ Profile CTEs", command: "dbtPowerUser.profileCtes", arguments: [document.uri, ctes], }; codeLenses.push(new CodeLens(cte.range, profileCommand)); this.dbtTerminal.debug( "CteCodeLensProvider", `Created code lens for CTE: ${cte.name} at index ${cte.index}`, ); } return codeLenses; } catch (error) { this.dbtTerminal.error( "CteCodeLensProvider", "Error in provideCodeLenses", error, ); return []; } } private detectCtes(document: TextDocument): CteInfo[] { const text = document.getText(); const ctes: CteInfo[] = []; this.dbtTerminal.debug( "CteCodeLensProvider", `Document length: ${text.length} characters`, ); // Find all WITH clauses by manually parsing to avoid comment confusion // This ensures we don't match 'with' inside comments while allowing unlimited comment length const withPositions = this.findWithKeywords(text); let withClauseCount = 0; for (const withPos of withPositions) { withClauseCount++; // Start parsing after the WITH keyword, skipping any comments and whitespace manually let withStartPos = withPos + 4; // 'with'.length = 4 // Skip whitespace and comments after WITH keyword using the existing comment parsing logic while (withStartPos < text.length) { // Skip whitespace while (withStartPos < text.length && /\s/.test(text[withStartPos])) { withStartPos++; } if (withStartPos >= text.length) { break; } // Check for comments using existing handleSqlComment method const commentEndPos = this.handleSqlComment(text, withStartPos); if (commentEndPos !== withStartPos) { // Found a comment, skip over it withStartPos = commentEndPos + 1; continue; } // No more whitespace or comments, we found the start of CTEs break; } this.dbtTerminal.debug( "CteCodeLensProvider", `Found WITH clause #${withClauseCount} at position ${withPos}`, ); // Find the end of this WITH clause (before the main SELECT) const withClauseEnd = this.findWithClauseEnd(text, withStartPos); if (withClauseEnd === -1) { this.dbtTerminal.warn( "CteCodeLensProvider", `Could not find end of WITH clause #${withClauseCount} starting at ${withStartPos}`, ); continue; } this.dbtTerminal.debug( "CteCodeLensProvider", `WITH clause #${withClauseCount} ends at position ${withClauseEnd}`, ); // Extract the WITH clause content const withClauseContent = text.substring(withStartPos, withClauseEnd); this.dbtTerminal.debug( "CteCodeLensProvider", `WITH clause content length: ${withClauseContent.length} characters`, ); // Find all CTEs within this WITH clause const beforeCteCount = ctes.length; this.extractCtesFromWithClause( withClauseContent, withStartPos, withPos, document, ctes, ); const ctesFound = ctes.length - beforeCteCount; this.dbtTerminal.debug( "CteCodeLensProvider", `Found ${ctesFound} CTEs in WITH clause #${withClauseCount}`, ); } this.dbtTerminal.debug( "CteCodeLensProvider", `Total WITH clauses found: ${withClauseCount}, Total CTEs found: ${ctes.length}`, ); return ctes; } /** * Find all WITH keywords in the text while properly skipping comments and strings * This prevents matching 'with' inside comments or strings */ private findWithKeywords(text: string): number[] { const withPositions: number[] = []; let pos = 0; let inString = false; let stringChar = ""; while (pos < text.length) { // Handle comments first - skip over them entirely const commentEndPos = this.handleSqlComment(text, pos); if (commentEndPos !== pos) { pos = commentEndPos + 1; continue; } // Handle string literals using helper function const stringResult = this.handleSqlStringLiteral( text, pos, inString, stringChar, ); pos = stringResult.newPos; inString = stringResult.inString; stringChar = stringResult.stringChar; // Only look for WITH keyword outside of strings and comments if (!inString) { const remainingText = text.substring(pos); const withMatch = remainingText.match(/^with\b/i); if (withMatch) { // Found a WITH keyword - verify it's not part of a larger identifier const charBefore = pos > 0 ? text[pos - 1] : " "; if (!/[a-zA-Z0-9_]/.test(charBefore)) { withPositions.push(pos); } pos += withMatch[0].length; continue; } } pos++; } return withPositions; } /** * Helper function to handle SQL string literal parsing with proper quote escaping * Returns updated position and string state */ private handleSqlStringLiteral( text: string, pos: number, inString: boolean, stringChar: string, ): { newPos: number; inString: boolean; stringChar: string } { const char = text[pos]; const nextChar = pos < text.length - 1 ? text[pos + 1] : ""; // Handle string literals with SQL-style quote escaping if (!inString && (char === "'" || char === '"')) { return { newPos: pos, inString: true, stringChar: char, }; } else if (inString && char === stringChar) { // Check for doubled quotes (SQL escape sequence) if (nextChar === stringChar) { // This is an escaped quote - skip the next character this.dbtTerminal.debug( "CteCodeLensProvider", `Found escaped quote (${stringChar}${stringChar}) at position ${pos}`, ); return { newPos: pos + 1, // Skip the second quote inString: true, stringChar: stringChar, }; } else { // This is the end of the string return { newPos: pos, inString: false, stringChar: "", }; } } return { newPos: pos, inString, stringChar }; } /** * Helper function to handle SQL and Jinja comment parsing * Returns updated position if currently at start of a comment, otherwise returns original position */ private handleSqlComment(text: string, pos: number): number { const char = text[pos]; const nextChar = pos < text.length - 1 ? text[pos + 1] : ""; // Handle line comments (-- comment) if (char === "-" && nextChar === "-") { this.dbtTerminal.debug( "CteCodeLensProvider", `Found line comment starting at position ${pos}`, ); // Skip to end of line let endPos = pos + 2; while ( endPos < text.length && text[endPos] !== "\n" && text[endPos] !== "\r" ) { endPos++; } this.dbtTerminal.debug( "CteCodeLensProvider", `Line comment ends at position ${endPos}`, ); // Return position at the newline (or end of text) return endPos; } // Handle block comments (/* comment */) if (char === "/" && nextChar === "*") { this.dbtTerminal.debug( "CteCodeLensProvider", `Found block comment starting at position ${pos}`, ); // Skip to end of block comment let endPos = pos + 2; while (endPos < text.length - 1) { if (text[endPos] === "*" && text[endPos + 1] === "/") { this.dbtTerminal.debug( "CteCodeLensProvider", `Block comment ends at position ${endPos + 1}`, ); return endPos + 1; // Return position after the closing */ } endPos++; } this.dbtTerminal.warn( "CteCodeLensProvider", `Unterminated block comment starting at position ${pos}`, ); return text.length; // Return end of text if comment is not closed } // Handle Jinja comments ({# comment #}) if (char === "{" && nextChar === "#") { this.dbtTerminal.debug( "CteCodeLensProvider", `Found Jinja comment starting at position ${pos}`, ); // Skip to end of Jinja comment let endPos = pos + 2; while (endPos < text.length - 1) { if (text[endPos] === "#" && text[endPos + 1] === "}") { this.dbtTerminal.debug( "CteCodeLensProvider", `Jinja comment ends at position ${endPos + 1}`, ); return endPos + 1; // Return position after the closing #} } endPos++; } this.dbtTerminal.warn( "CteCodeLensProvider", `Unterminated Jinja comment starting at position ${pos}`, ); return text.length; // Return end of text if comment is not closed } return pos; // Not a comment, return original position } /** * Check if a given position is inside a comment (line, block, or Jinja) * This helps filter out false positive identifier matches within comments */ private isPositionInsideComment(content: string, position: number): boolean { // Scan backwards from the position to see if we're inside a comment let pos = 0; let inBlockComment = false; let inJinjaComment = false; while (pos < position && pos < content.length) { const char = content[pos]; const nextChar = pos < content.length - 1 ? content[pos + 1] : ""; // Check for start of block comment if ( !inBlockComment && !inJinjaComment && char === "/" && nextChar === "*" ) { inBlockComment = true; pos += 2; continue; } // Check for end of block comment if (inBlockComment && char === "*" && nextChar === "/") { inBlockComment = false; pos += 2; continue; } // Check for start of Jinja comment if ( !inBlockComment && !inJinjaComment && char === "{" && nextChar === "#" ) { inJinjaComment = true; pos += 2; continue; } // Check for end of Jinja comment if (inJinjaComment && char === "#" && nextChar === "}") { inJinjaComment = false; pos += 2; continue; } // Check for line comment (only if not in other comments) if ( !inBlockComment && !inJinjaComment && char === "-" && nextChar === "-" ) { // Line comment - check if our position is on this line const lineStart = pos; let lineEnd = pos + 2; while ( lineEnd < content.length && content[lineEnd] !== "\n" && content[lineEnd] !== "\r" ) { lineEnd++; } // If position is within this line comment, return true if (position >= lineStart && position < lineEnd) { return true; } // Skip to end of line pos = lineEnd; continue; } pos++; } // If we're still in a block or Jinja comment when we reach the position, it's inside a comment return inBlockComment || inJinjaComment; } /** * Phase 1: Simple regex to find CTE identifiers without complex comment parsing * This avoids catastrophic backtracking by focusing only on identifier patterns */ private findCteIdentifiersOnly( withClauseContent: string, ): Array<{ index: number; identifierName: string; fullMatch: string }> { // Simplified regex that matches only the identifier part // This regex finds potential CTE identifiers followed by anything that looks like it could lead to AS const simpleIdentifierRegex = new RegExp( `((?:[a-zA-Z_][a-zA-Z0-9_]*|"[^"]{1,${CteCodeLensProvider.MAX_QUOTED_IDENTIFIER_LENGTH}}"|` + `\`[^\`]{1,${CteCodeLensProvider.MAX_QUOTED_IDENTIFIER_LENGTH}}\`|` + `\\[[^\\]]{1,${CteCodeLensProvider.MAX_QUOTED_IDENTIFIER_LENGTH}}\\])` + `(?:\\.(?:[a-zA-Z_][a-zA-Z0-9_]*|"[^"]{1,${CteCodeLensProvider.MAX_QUOTED_IDENTIFIER_LENGTH}}"|` + `\`[^\`]{1,${CteCodeLensProvider.MAX_QUOTED_IDENTIFIER_LENGTH}}\`|` + `\\[[^\\]]{1,${CteCodeLensProvider.MAX_QUOTED_IDENTIFIER_LENGTH}}\\]))*` + `(?:\\s*\\([^)]{0,${CteCodeLensProvider.MAX_COLUMN_LIST_LENGTH}}\\))?)`, "gi", ); const potentialMatches: Array<{ index: number; identifierName: string; fullMatch: string; }> = []; let match; while ((match = simpleIdentifierRegex.exec(withClauseContent)) !== null) { const identifierName = match[1]; const matchIndex = match.index; // First check: Skip if this identifier is inside a comment if (this.isPositionInsideComment(withClauseContent, matchIndex)) { continue; } // Phase 2: Manually validate this match by checking for comments between identifier and AS const validationResult = this.validateCteMatchWithComments( withClauseContent, matchIndex, match[0], identifierName, ); if (validationResult.isValid) { potentialMatches.push({ index: matchIndex, identifierName: identifierName, fullMatch: validationResult.fullMatch || match[0], }); } } return potentialMatches; } /** * Phase 2: Manually parse comments between identifier and AS keyword * Uses existing handleSqlComment method to safely skip over comments */ private validateCteMatchWithComments( content: string, startIndex: number, fullMatch: string, identifierName: string, ): { isValid: boolean; fullMatch?: string } { // Find the end of the identifier (including column list if present) const identifierEndIndex = startIndex + identifierName.length; let pos = identifierEndIndex; // Skip any column list while (pos < content.length && /\s/.test(content[pos])) { pos++; } if (pos < content.length && content[pos] === "(") { const columnListEnd = this.findMatchingClosingParen(content, pos); if (columnListEnd !== -1) { pos = columnListEnd + 1; } } // Now manually parse comments and whitespace until we find 'as' while (pos < content.length) { // Skip whitespace while (pos < content.length && /\s/.test(content[pos])) { pos++; } if (pos >= content.length) { break; } // Check for comments using existing handleSqlComment method const commentEndPos = this.handleSqlComment(content, pos); if (commentEndPos !== pos) { // Found a comment, skip over it pos = commentEndPos + 1; continue; } // Check if we've reached the 'as' keyword const remainingText = content.substring(pos); const asMatch = remainingText.match(/^as\s*\(/i); if (asMatch) { // Calculate the full match including comments and AS const fullMatchEnd = pos + asMatch[0].length; const actualFullMatch = content.substring(startIndex, fullMatchEnd); return { isValid: true, fullMatch: actualFullMatch, }; } // If we hit something that's not whitespace, comment, or 'as', this is not a valid CTE break; } return { isValid: false }; } private findWithClauseEnd(text: string, withStartPos: number): number { // Look for the main SELECT that comes after all CTEs // This is a simplified approach - we look for SELECT that's not inside parentheses let pos = withStartPos; let parenCount = 0; let inString = false; let stringChar = ""; let selectsChecked = 0; this.dbtTerminal.debug( "CteCodeLensProvider", `Searching for WITH clause end starting at position ${withStartPos}`, ); while (pos < text.length) { const char = text[pos]; // Handle comments first - skip over them entirely const commentEndPos = this.handleSqlComment(text, pos); if (commentEndPos !== pos) { pos = commentEndPos; // Don't increment pos here - commentEndPos already points to the position after the comment continue; } // Handle string literals using helper function const stringResult = this.handleSqlStringLiteral( text, pos, inString, stringChar, ); pos = stringResult.newPos; inString = stringResult.inString; stringChar = stringResult.stringChar; // Only count parentheses and look for SELECT outside of strings if (!inString) { if (char === "(") { parenCount++; } else if (char === ")") { parenCount--; } else if (parenCount === 0) { // Check for nested WITH keyword at top level const remainingText = text.substring(pos); const nestedWithMatch = remainingText.match(/^\s*with\b/i); if (nestedWithMatch) { this.dbtTerminal.warn( "CteCodeLensProvider", `Found nested WITH clause at position ${pos}, bailing out - nested WITH clauses are not supported`, ); return -1; // Signal failure due to nested WITH } // Check for SELECT keyword at top level const selectMatch = remainingText.match(/^\s*select\b/i); if (selectMatch) { selectsChecked++; this.dbtTerminal.debug( "CteCodeLensProvider", `Found top-level SELECT #${selectsChecked} at position ${pos}`, ); return pos; } } } pos++; } this.dbtTerminal.warn( "CteCodeLensProvider", `No main SELECT found after WITH clause starting at ${withStartPos}, checked ${selectsChecked} SELECT statements`, ); return text.length; // End of file if no main SELECT found } private extractCtesFromWithClause( withClauseContent: string, withStartPos: number, withClauseStart: number, document: TextDocument, ctes: CteInfo[], ): void { this.dbtTerminal.debug( "CteCodeLensProvider", `Extracting CTEs from WITH clause content starting at ${withStartPos}`, ); // Two-phase approach: Use simplified regex for identifier matching, then parse comments manually // This eliminates catastrophic backtracking by avoiding complex nested quantifiers in regex const cteMatches = this.findCteIdentifiersOnly(withClauseContent); let cteIndex = 0; for (const cteMatch of cteMatches) { const cteName = cteMatch.identifierName; const cteStartPos = withStartPos + cteMatch.index; const cteNameEndPos = withStartPos + cteMatch.index + cteMatch.identifierName.length; this.dbtTerminal.debug( "CteCodeLensProvider", `Found CTE: ${cteName} at position ${cteStartPos}`, ); // Find the opening parenthesis position const openParenPos = withStartPos + cteMatch.index + cteMatch.fullMatch.length - 1; // Find the matching closing parenthesis const cteQueryEnd = this.findMatchingClosingParen( withClauseContent, cteMatch.index + cteMatch.fullMatch.length - 1, ); if (cteQueryEnd === -1) { this.dbtTerminal.warn( "CteCodeLensProvider", `Could not find matching closing parenthesis for CTE: ${cteName}`, ); continue; // Skip if we can't find matching paren } // Adjust position back to full text const absoluteCteQueryEnd = withStartPos + cteQueryEnd; this.dbtTerminal.debug( "CteCodeLensProvider", `CTE ${cteName} query range: ${openParenPos + 1} to ${absoluteCteQueryEnd}`, ); // Create ranges const cteNameRange = new Range( document.positionAt(cteStartPos), document.positionAt(cteNameEndPos), ); const cteQueryRange = new Range( document.positionAt(openParenPos + 1), document.positionAt(absoluteCteQueryEnd), ); ctes.push({ name: cteName, range: cteNameRange, queryRange: cteQueryRange, index: cteIndex, withClauseStart: withClauseStart, }); this.dbtTerminal.debug( "CteCodeLensProvider", `Successfully extracted CTE: ${cteName} (index: ${cteIndex})`, ); cteIndex++; } this.dbtTerminal.debug( "CteCodeLensProvider", `Extracted ${cteIndex} CTEs from WITH clause`, ); } private findMatchingClosingParen(text: string, openParenPos: number): number { let parenCount = 1; let pos = openParenPos + 1; let inString = false; let stringChar = ""; let maxDepth = 0; this.dbtTerminal.debug( "CteCodeLensProvider", `Searching for matching closing paren starting at position ${openParenPos}`, ); while (pos < text.length && parenCount > 0) { const char = text[pos]; // Handle comments first - skip over them entirely const commentEndPos = this.handleSqlComment(text, pos); if (commentEndPos !== pos) { pos = commentEndPos; // Don't increment pos here - commentEndPos already points to the position after the comment continue; } // Handle string literals using helper function const stringResult = this.handleSqlStringLiteral( text, pos, inString, stringChar, ); pos = stringResult.newPos; inString = stringResult.inString; stringChar = stringResult.stringChar; // Only count parentheses outside of strings if (!inString) { if (char === "(") { parenCount++; maxDepth = Math.max(maxDepth, parenCount); } else if (char === ")") { parenCount--; } } pos++; } if (parenCount === 0) { this.dbtTerminal.debug( "CteCodeLensProvider", `Found matching closing paren at position ${pos - 1}, max depth: ${maxDepth}`, ); return pos - 1; } else { this.dbtTerminal.warn( "CteCodeLensProvider", `Could not find matching closing paren, remaining open parens: ${parenCount}, max depth reached: ${maxDepth}`, ); return -1; } } } ================================================ FILE: src/code_lens_provider/index.ts ================================================ import { Disposable, languages } from "vscode"; import { DBTPowerUserExtension } from "../dbtPowerUserExtension"; import { DBTProjectContainer } from "../dbt_client/dbtProjectContainer"; import { CteCodeLensProvider } from "./cteCodeLensProvider"; import { SourceModelCreationCodeLensProvider } from "./sourceModelCreationCodeLensProvider"; import { SqlActionsCodeLensProvider } from "./sqlActionsCodeLensProvider"; import { VirtualSqlCodeLensProvider } from "./virtualSqlCodeLensProvider"; export class CodeLensProviders implements Disposable { private disposables: Disposable[] = []; constructor( private dbtProjectContainer: DBTProjectContainer, private sourceModelCreationCodeLensProvider: SourceModelCreationCodeLensProvider, private virtualSqlCodeLensProvider: VirtualSqlCodeLensProvider, private cteCodeLensProvider: CteCodeLensProvider, private sqlActionsCodeLensProvider: SqlActionsCodeLensProvider, ) { // Add codelens after projects are initialized to avoid race conditions in executing notebook cells this.dbtProjectContainer.onDBTProjectsInitialization(() => { this.disposables.push( languages.registerCodeLensProvider( DBTPowerUserExtension.DBT_YAML_SELECTOR, this.sourceModelCreationCodeLensProvider, ), ); this.disposables.push( languages.registerCodeLensProvider( DBTPowerUserExtension.DBT_SQL_SELECTOR, this.virtualSqlCodeLensProvider, ), ); this.disposables.push( languages.registerCodeLensProvider( DBTPowerUserExtension.DBT_SQL_SELECTOR, this.cteCodeLensProvider, ), ); this.disposables.push( languages.registerCodeLensProvider( DBTPowerUserExtension.DBT_SQL_SELECTOR, this.sqlActionsCodeLensProvider, ), languages.registerCodeLensProvider( DBTPowerUserExtension.DBT_YAML_SELECTOR, this.sqlActionsCodeLensProvider, ), ); }); } dispose() { this.sqlActionsCodeLensProvider.dispose(); while (this.disposables.length) { const x = this.disposables.pop(); if (x) { x.dispose(); } } } } ================================================ FILE: src/code_lens_provider/sourceModelCreationCodeLensProvider.ts ================================================ import { CancellationToken, CodeLens, CodeLensProvider, Event, EventEmitter, Range, TextDocument, Uri, } from "vscode"; import { CST, LineCounter, Parser } from "yaml"; interface Position { line: number; col: number; } export interface GenerateModelFromSourceParams { currentDoc: Uri; sourceName: string; database: string; schema: string; tableName: string; tableIdentifier?: string; } export class SourceModelCreationCodeLensProvider implements CodeLensProvider { private codeLenses: CodeLens[] = []; private _onDidChangeCodeLenses: EventEmitter = new EventEmitter(); public readonly onDidChangeCodeLenses: Event = this._onDidChangeCodeLenses.event; public provideCodeLenses( document: TextDocument, token: CancellationToken, ): CodeLens[] | Thenable { this.codeLenses = []; const lineCounter = new LineCounter(); let currentSource: string | undefined = undefined; let currentDatabase: string | undefined = undefined; let currentSchema: string | undefined = undefined; let currentTables: { tableName: string; tableIdentifier?: string; pos: Position; }[]; for (const token of new Parser(lineCounter.addNewLine).parse( document.getText(), )) { if (token.type === "document" && CST.isCollection(token.value)) { for (const i in token.value.items) { const item = token.value.items[i]; if ( CST.isScalar(item.key) && item.key.source === "sources" && CST.isCollection(item.value) ) { // inside sources for (const j in item.value.items) { // inside a source currentTables = []; const source = item.value.items[j]; if (CST.isCollection(source.value)) { // for (const k in source.value.items) { const sourceProperty = source.value.items[k]; if ( CST.isScalar(sourceProperty.key) && CST.isScalar(sourceProperty.value) ) { if (sourceProperty.key.source === "name") { currentSource = sourceProperty.value.source; } if (sourceProperty.key.source === "database") { currentDatabase = sourceProperty.value.source; } if (sourceProperty.key.source === "schema") { currentSchema = sourceProperty.value.source; } } if ( CST.isScalar(sourceProperty.key) && CST.isCollection(sourceProperty.value) && sourceProperty.key.source === "tables" ) { // inside tables let tableName: string | undefined = undefined; let tableIdentifier: string | undefined = undefined; let position: Position | undefined = undefined; for (const l in sourceProperty.value.items) { const table = sourceProperty.value.items[l]; if (CST.isCollection(table.value)) { for (const m in table.value.items) { position = lineCounter.linePos(table.value.offset); const tableProperty = table.value.items[m]; if ( CST.isScalar(tableProperty.value) && CST.isScalar(tableProperty.key) ) { if (tableProperty.key.source === "name") { tableName = tableProperty.value.source; } if (tableProperty.key.source === "identifier") { tableIdentifier = tableProperty.value.source; } } } } if (tableName !== undefined && position !== undefined) { currentTables.push({ tableName: tableName, tableIdentifier: tableIdentifier, pos: position, }); tableName = undefined; tableIdentifier = undefined; position = undefined; } } } } } // add all tables for (const i in currentTables) { const table = currentTables[i]; const params: GenerateModelFromSourceParams = { currentDoc: document.uri, sourceName: currentSource!, database: currentDatabase!, schema: currentSchema!, tableName: table.tableName, tableIdentifier: table.tableIdentifier, }; this.codeLenses.push( new CodeLens( new Range( table.pos.line - 1, table.pos.col, table.pos.line - 1, table.pos.col, ), { title: "Generate model", tooltip: "Generate model based on source configuration", command: "dbtPowerUser.createModelBasedonSourceConfig", arguments: [params], }, ), ); } currentDatabase = undefined; currentSchema = undefined; currentSource = undefined; } } } } } return this.codeLenses; } } ================================================ FILE: src/code_lens_provider/sqlActionsCodeLensProvider.ts ================================================ import * as path from "path"; import { CancellationToken, CodeLens, CodeLensProvider, commands, Disposable, Event, EventEmitter, extensions, ProviderResult, Range, TextDocument, Uri, window, } from "vscode"; import { CST, LineCounter, Parser } from "yaml"; import { AltimateCodeChatService } from "../services/altimateCodeChatService"; interface GitChange { uri: Uri; } interface GitRepoState { workingTreeChanges: GitChange[]; indexChanges: GitChange[]; onDidChange: Event; } interface GitRepository { state: GitRepoState; } interface GitAPI { repositories: GitRepository[]; onDidOpenRepository: Event; } export class SqlActionsCodeLensProvider implements CodeLensProvider, Disposable { private _onDidChangeCodeLenses = new EventEmitter(); readonly onDidChangeCodeLenses: Event = this._onDidChangeCodeLenses.event; private disposables: Disposable[] = []; private changedFiles = new Set(); constructor(private altimateCodeChatService: AltimateCodeChatService) { this.initGitWatcher(); this.disposables.push( window.onDidChangeActiveTextEditor((editor) => { if (editor) { const isChanged = this.changedFiles.has(editor.document.uri.fsPath); commands.executeCommand( "setContext", "dbtPowerUser.fileHasGitChanges", isChanged, ); } }), ); this.disposables.push( commands.registerCommand( "dbtPowerUser.reviewCodeWithAltimate", async (uri?: Uri) => { const docUri = uri ?? window.activeTextEditor?.document.uri; if (!docUri) { return; } const filename = path.basename(docUri.fsPath); const relativePath = this.altimateCodeChatService.getRelativePath(docUri); await this.altimateCodeChatService.openChat({ initialMessage: `Review \`@${relativePath}\` for dbt best practices, performance, and maintainability.`, title: `Review: ${filename}`, }); }, ), ); } private initGitWatcher() { const gitExt = extensions.getExtension("vscode.git"); if (!gitExt) { return; } if (!gitExt.isActive) { gitExt.activate().then(() => this.watchGitState()); return; } this.watchGitState(); } private watchGitState() { const git: GitAPI | undefined = extensions .getExtension("vscode.git") ?.exports?.getAPI(1); if (!git) { return; } for (const repo of git.repositories) { this.attachRepoWatcher(repo); } this.disposables.push( git.onDidOpenRepository((repo: GitRepository) => { this.attachRepoWatcher(repo); }), ); } private attachRepoWatcher(repo: GitRepository) { this.updateChangedFiles(repo); this.disposables.push( repo.state.onDidChange(() => { this.updateChangedFiles(repo); this._onDidChangeCodeLenses.fire(); // Update context key for the active editor const activeUri = window.activeTextEditor?.document.uri; if (activeUri) { commands.executeCommand( "setContext", "dbtPowerUser.fileHasGitChanges", this.changedFiles.has(activeUri.fsPath), ); } }), ); } private updateChangedFiles(repo: GitRepository) { const allChanges = [ ...repo.state.workingTreeChanges, ...repo.state.indexChanges, ]; for (const change of allChanges) { this.changedFiles.add(change.uri.fsPath); } const currentPaths = new Set(allChanges.map((c) => c.uri.fsPath)); for (const p of this.changedFiles) { if (!currentPaths.has(p)) { this.changedFiles.delete(p); } } } provideCodeLenses( document: TextDocument, _token: CancellationToken, ): ProviderResult { if (document.fileName.endsWith(".sql")) { return this.provideSqlCodeLenses(document); } return this.provideYamlCodeLenses(document); } private provideSqlCodeLenses(document: TextDocument): CodeLens[] { const codeLenses: CodeLens[] = [ new CodeLens(new Range(0, 0, 0, 0), { title: "$(play) Execute Query", tooltip: "Execute this SQL query", command: "dbtPowerUser.executeSQL", arguments: [], }), new CodeLens(new Range(0, 0, 0, 0), { title: "$(sparkle) Explain", tooltip: "Explain this code with Altimate", command: "dbtPowerUser.explainWithAltimate", arguments: [], }), new CodeLens(new Range(0, 0, 0, 0), { title: "$(zap) Optimize", tooltip: "Optimize this SQL with Altimate", command: "dbtPowerUser.optimizeWithAltimate", arguments: [], }), new CodeLens(new Range(0, 0, 0, 0), { title: "$(book) Document", tooltip: "Add documentation or tests for this model", command: "dbtPowerUser.DocsEdit.focus", arguments: [], }), ]; if (this.changedFiles.has(document.uri.fsPath)) { codeLenses.push( new CodeLens(new Range(0, 0, 0, 0), { title: "$(sparkle) Review", tooltip: "Review code changes with Altimate", command: "dbtPowerUser.reviewCodeWithAltimate", arguments: [document.uri], }), ); } return codeLenses; } private provideYamlCodeLenses(document: TextDocument): CodeLens[] { const codeLenses: CodeLens[] = []; const lineCounter = new LineCounter(); for (const token of new Parser(lineCounter.addNewLine).parse( document.getText(), )) { if (!(token.type === "document" && CST.isCollection(token.value))) { continue; } for (const item of token.value.items) { if ( !( CST.isScalar(item.key) && item.key.source === "models" && CST.isCollection(item.value) ) ) { continue; } for (const modelItem of item.value.items) { if (!CST.isCollection(modelItem.value)) { continue; } for (const properties of modelItem.value.items) { if ( CST.isScalar(properties.key) && CST.isScalar(properties.value) && properties.key.source === "name" ) { const position = lineCounter.linePos(properties.key.offset); const lensRange = new Range( position.line - 1, position.col, position.line - 1, position.col, ); codeLenses.push( new CodeLens(lensRange, { title: "$(play) Run", tooltip: `Run model ${properties.value.source}`, command: "dbtPowerUser.yamlRunModel", arguments: [document.uri, properties.value.source], }), new CodeLens(lensRange, { title: "$(beaker) Test", tooltip: `Run tests for model ${properties.value.source}`, command: "dbtPowerUser.yamlTestModel", arguments: [document.uri, properties.value.source], }), new CodeLens(lensRange, { title: "$(book) Document", tooltip: "Add documentation or tests for this model", command: "dbtPowerUser.showDocumentation", arguments: [properties.value.source], }), ); } } } } } if (this.changedFiles.has(document.uri.fsPath)) { codeLenses.unshift( new CodeLens(new Range(0, 0, 0, 0), { title: "$(sparkle) Review", tooltip: "Review code changes with Altimate", command: "dbtPowerUser.reviewCodeWithAltimate", arguments: [document.uri], }), ); } return codeLenses; } dispose() { this._onDidChangeCodeLenses.dispose(); while (this.disposables.length) { this.disposables.pop()?.dispose(); } } } ================================================ FILE: src/code_lens_provider/virtualSqlCodeLensProvider.ts ================================================ import { NotebookService } from "@lib"; import { CancellationToken, CodeLens, CodeLensProvider, Command, Disposable, Range, TextDocument, window, } from "vscode"; import { DBTProjectContainer } from "../dbt_client/dbtProjectContainer"; import { QueryManifestService } from "../services/queryManifestService"; export class VirtualSqlCodeLensProvider implements CodeLensProvider, Disposable { private disposables: Disposable[] = []; constructor( private dbtProjectContainer: DBTProjectContainer, private queryManifestService: QueryManifestService, private notebookService: NotebookService, ) {} dispose() { while (this.disposables.length) { const x = this.disposables.pop(); if (x) { x.dispose(); } } } private getProjectName() { const project = this.dbtProjectContainer.getFromWorkspaceState( "dbtPowerUser.projectSelected", ); if (project?.label) { return project.label; } // Find the project name from the current active editor return this.queryManifestService.getProject()?.getProjectName(); } public provideCodeLenses( document: TextDocument, token: CancellationToken, ): CodeLens[] | Thenable { // Enable this code lens only for adhoc query files created using command: dbtPowerUser.createSqlFile if ( (document.uri.scheme !== "untitled" && document.uri.scheme !== "vscode-notebook-cell") || document.languageId !== "jinja-sql" ) { return []; } const topOfDocument = new Range(0, 0, 0, 0); const projectName = this.getProjectName(); const projectSelectorCommand: Command = { title: `Project: ${projectName || "Select a project"}`, command: "dbtPowerUser.pickProject", arguments: [document.uri], }; const projectSelectorCodeLens = new CodeLens( topOfDocument, projectSelectorCommand, ); // Cell id code lens for notebook cells if ( document.uri.scheme === "vscode-notebook-cell" && window.activeNotebookEditor?.notebook ) { const cells = this.notebookService .getCellByNotebookAutocompleteMap() .get(window.activeNotebookEditor?.notebook.uri.fsPath); const cell = cells?.find((c) => c.fragment === document.uri.fragment); if (cell) { const cellIdLens = new CodeLens(topOfDocument, { title: `Cell id: cell_${cell.cellId}`, command: "", // TODO: Add command to allow user to modify cell id arguments: [document.uri], }); return [cellIdLens]; } return []; } return [projectSelectorCodeLens]; } } ================================================ FILE: src/commands/altimateScan.ts ================================================ import { DBTTerminal } from "@altimateai/dbt-integration"; import { inject } from "inversify"; import { commands, ProgressLocation, Uri, window } from "vscode"; import { AltimateRequest } from "../altimate"; import { DBTProjectContainer } from "../dbt_client/dbtProjectContainer"; import { ManifestCacheChangedEvent, ManifestCacheProjectAddedEvent, } from "../dbt_client/event/manifestCacheChangedEvent"; import { TelemetryService } from "../telemetry"; import { InitCatalog } from "./tests/initCatalog"; import { MissingSchemaTest } from "./tests/missingSchemaTest"; import { ScanContext } from "./tests/scanContext"; import { StaleModelColumnTest } from "./tests/staleModelColumnTest"; import { AltimateScanStep } from "./tests/step"; import { UndocumentedModelColumnTest } from "./tests/undocumentedModelColumnTest"; import { UnmaterializedModelTest } from "./tests/unmaterializedModelTest"; export class AltimateScan { private eventMap: Map = new Map(); private offlineAltimateScanSteps: AltimateScanStep[]; private onlineAltimateScanSteps: AltimateScanStep[]; private altimateScanSteps: AltimateScanStep[]; constructor( private dbtProjectContainer: DBTProjectContainer, private telemetry: TelemetryService, private altimate: AltimateRequest, private missingSchemaTest: MissingSchemaTest, private undocumentedModelColumnTest: UndocumentedModelColumnTest, private unmaterializedModelTest: UnmaterializedModelTest, private staleModelColumnTest: StaleModelColumnTest, @inject("DBTTerminal") private dbtTerminal: DBTTerminal, ) { dbtProjectContainer.onManifestChanged((event) => this.onManifestCacheChanged(event), ); this.offlineAltimateScanSteps = [missingSchemaTest]; // online tests rely on a connection to database this.onlineAltimateScanSteps = [ unmaterializedModelTest, undocumentedModelColumnTest, staleModelColumnTest, // feel free to add more tests ]; //TODO // altimate tests rely on an altimate account. these are assumed to be online as well. this.altimateScanSteps = []; } private async onManifestCacheChanged(event: ManifestCacheChangedEvent) { event.added?.forEach((added) => { this.eventMap.set(added.project.projectRoot.fsPath, added); }); event.removed?.forEach((removed) => { this.eventMap.delete(removed.projectRoot.fsPath); }); } async clearProblems() { this.telemetry.sendTelemetryEvent("altimateScan:Clear"); window.withProgress( { location: ProgressLocation.Notification, title: "Clearing problems...", cancellable: false, }, async () => { const projects = this.dbtProjectContainer.getProjects(); for (const project of projects) { project.projectHealth.clear(); } }, ); } async getProblems() { this.telemetry.sendTelemetryEvent("altimateScan:Start"); let totalProblems: number = 0; window.withProgress( { location: ProgressLocation.Notification, title: "Scanning for problems...", cancellable: true, }, async () => { const projects = this.dbtProjectContainer.getProjects(); for (const project of projects) { try { const scanContext: ScanContext = new ScanContext( project, this.eventMap.get(project.projectRoot.fsPath), ); await this.runSteps(scanContext); totalProblems += this.showDiagnostics(scanContext); } catch (err) { this.dbtTerminal.debug( "altimateScane:getProblems", `Error occurred for ${project.getProjectName()}`, err, ); } } // we can select problem tab as soon as the first project is done maybe await commands.executeCommand("workbench.actions.view.problems"); this.telemetry.sendTelemetryEvent("altimateScan:Done", { problemsFound: totalProblems.toString(), }); }, ); } async runSteps(scanContext: ScanContext) { // run all the offline steps first, no need to get the catalog yet await Promise.all( this.offlineAltimateScanSteps.map( async (stepof) => await stepof.run(scanContext), ), ); // get catalog before continuing to online steps // errors are caught on python side and returned as a catalog with 0 length // telemetry is sent from dbtproject.ts if there is an error await this.initCatalog(scanContext); // if there was some error in getting the catalog, we dont get anything back. // stop the remaining tests in that case. if ( scanContext.scanResults["missingCatalog"] !== undefined && scanContext.scanResults["missingCatalog"][ scanContext.project.getProjectName() + scanContext.project.projectRoot ] === true ) { return; } await Promise.all( this.onlineAltimateScanSteps.map( async (stepon) => await stepon.run(scanContext), ), ); } public async initCatalog(scanContext: ScanContext): Promise { if (scanContext === undefined) { throw new Error("Scan Context has not been set"); } const projectCatalog = await new InitCatalog().run(scanContext); scanContext.catalog[ scanContext.project.getProjectName() + scanContext.project.projectRoot ] = projectCatalog; } showDiagnostics(scanContext: ScanContext) { if (scanContext === undefined) { throw new Error("Scan Context has not been set"); } scanContext.project.projectHealth.clear(); let totalProblems = 0; for (const [filePath, fileDiagnostics] of Object.entries( scanContext.diagnostics, )) { scanContext.project.projectHealth.set( Uri.file(filePath), fileDiagnostics, ); totalProblems += fileDiagnostics.length; } return totalProblems; } } ================================================ FILE: src/commands/bigQueryCostEstimate.ts ================================================ import { DBTTerminal } from "@altimateai/dbt-integration"; import { inject } from "inversify"; import { PythonException } from "python-bridge"; import { window } from "vscode"; import { DBTProjectContainer } from "../dbt_client/dbtProjectContainer"; import { TelemetryService } from "../telemetry"; import { extendErrorWithSupportLinks } from "../utils"; import path = require("path"); export class BigQueryCostEstimate { constructor( private dbtProjectContainer: DBTProjectContainer, @inject("DBTTerminal") private dbtTerminal: DBTTerminal, private telemetry: TelemetryService, ) {} async estimateCost({ returnResult }: { returnResult?: boolean }) { const modelName = path.basename( window.activeTextEditor!.document.fileName, ".sql", ); if (!returnResult) { await this.dbtTerminal.show(true); } try { const query = window.activeTextEditor?.document.getText(); if (!query) { window.showErrorMessage( "We need a valid query to get a cost estimate.", ); return; } const compiledQuery = await this.getProject()?.unsafeCompileQuery( query, modelName, ); if (!compiledQuery) { window.showErrorMessage( "We need a valid query to get a cost estimate.", ); return; } const result = await this.getProject()?.validateSQLDryRun(compiledQuery); if (!result) { return; } this.dbtTerminal.log( `The query for ${modelName} will process ${result.bytes_processed}.\r\n`, ); if (returnResult) { return { modelName, result }; } } catch (error) { if (error instanceof PythonException) { window.showErrorMessage( extendErrorWithSupportLinks( `An error occured while trying to compile your node: ${modelName}` + error.exception.message + ".", ), ); this.telemetry.sendTelemetryError( "bigqueryCostEstimatePythonError", error, ); return; } window.showErrorMessage( "Could not perform bigquery cost estimate: " + (error as Error).message, ); } } private getProject() { const currentFilePath = window.activeTextEditor?.document.uri; if (!currentFilePath) { return; } return this.dbtProjectContainer.findDBTProject(currentFilePath); } } ================================================ FILE: src/commands/index.ts ================================================ import { DBTTerminal, RunModelType } from "@altimateai/dbt-integration"; import { DatapilotNotebookController, OpenNotebookRequest } from "@lib"; import { existsSync, readFileSync } from "fs"; import { inject } from "inversify"; import { CancellationTokenSource, CodeLens, commands, CommentReply, CommentThread, DecorationRangeBehavior, Disposable, env, extensions, languages, ProgressLocation, Range, TextEditor, TextEditorDecorationType, Uri, version, ViewColumn, window, workspace, } from "vscode"; import { AltimateRequest } from "../altimate"; import { CteCodeLensProvider, CteInfo, } from "../code_lens_provider/cteCodeLensProvider"; import { ConversationCommentThread, ConversationProvider, } from "../comment_provider/conversationProvider"; import { SqlPreviewContentProvider } from "../content_provider/sqlPreviewContentProvider"; import { CteProfilerDecorationProvider } from "../cte_profiler/cteProfilerDecorationProvider"; import { CteProfilerService } from "../cte_profiler/cteProfilerService"; import { DBTClient } from "../dbt_client"; import { DBTProject } from "../dbt_client/dbtProject"; import { DBTProjectContainer } from "../dbt_client/dbtProjectContainer"; import { PythonEnvironment } from "../dbt_client/pythonEnvironment"; import { NotebookQuickPick } from "../quickpick/notebookQuickPick"; import { ProjectQuickPickItem } from "../quickpick/projectQuickPick"; import { AltimateCodeChatService } from "../services/altimateCodeChatService"; import { DiagnosticsOutputChannel } from "../services/diagnosticsOutputChannel"; import { QueryManifestService } from "../services/queryManifestService"; import { RunHistoryService } from "../services/runHistoryService"; import { SharedStateService } from "../services/sharedStateService"; import { TelemetryService } from "../telemetry"; import { TelemetryEvents } from "../telemetry/events"; import { RunTreeItem } from "../treeview_provider/runHistoryTreeItems"; import { deepEqual, extendErrorWithSupportLinks, getFirstWorkspacePath, getFormattedDateTime, } from "../utils"; import { SQLLineagePanel } from "../webview_provider/sqlLineagePanel"; import { AltimateScan } from "./altimateScan"; import { BigQueryCostEstimate } from "./bigQueryCostEstimate"; import { RunModel } from "./runModel"; import { RunTest } from "./runTest"; import { SqlToModel } from "./sqlToModel"; import { ValidateSql } from "./validateSql"; import { WalkthroughCommands } from "./walkthroughCommands"; export class VSCodeCommands implements Disposable { private disposables: Disposable[] = []; constructor( private dbtProjectContainer: DBTProjectContainer, private runModel: RunModel, private runTest: RunTest, private sqlToModel: SqlToModel, private validateSql: ValidateSql, private altimateScan: AltimateScan, private walkthroughCommands: WalkthroughCommands, private bigQueryCostEstimate: BigQueryCostEstimate, @inject("DBTTerminal") private dbtTerminal: DBTTerminal, private diagnosticsOutputChannel: DiagnosticsOutputChannel, private eventEmitterService: SharedStateService, private conversationController: ConversationProvider, @inject(PythonEnvironment) private pythonEnvironment: PythonEnvironment, private dbtClient: DBTClient, private sqlLineagePanel: SQLLineagePanel, private queryManifestService: QueryManifestService, private altimate: AltimateRequest, private notebookController: DatapilotNotebookController, private runHistoryService: RunHistoryService, private altimateCodeChatService: AltimateCodeChatService, private cteProfilerService: CteProfilerService, private cteProfilerDecorationProvider: CteProfilerDecorationProvider, private cteCodeLensProvider: CteCodeLensProvider, private telemetry: TelemetryService, ) { this.disposables.push( this.cteProfilerService, this.cteProfilerDecorationProvider, commands.registerCommand( "dbtPowerUser.checkIfDbtIsInstalled", async () => { await this.dbtProjectContainer.detectDBT(); this.dbtProjectContainer.initialize(); }, ), commands.registerCommand("dbtPowerUser.installDbt", () => this.walkthroughCommands.installDbt(), ), commands.registerCommand("dbtPowerUser.runCurrentModel", () => { // `dbt run` on a singular test file is never meaningful; route it // to `dbt test --select ` instead. See #1720. if (this.runTest.runSingularTestOnActiveWindowIfApplicable()) { return; } this.runModel.runModelOnActiveWindow(); }), commands.registerCommand( "dbtPowerUser.rerunFromHistory", (item: RunTreeItem) => { this.dbtProjectContainer.rerunFromHistory(item.entry); }, ), commands.registerCommand("dbtPowerUser.clearRunHistory", async () => { const confirm = await window.showWarningMessage( "Clear all run history entries?", { modal: true }, "Clear", ); if (confirm === "Clear") { this.runHistoryService.clear(); } }), commands.registerCommand( "dbtPowerUser.profileCtes", async (uri?: Uri, ctes?: CteInfo[]) => { // When called from command palette, args are undefined — use active editor const source = uri ? "codeLens" : "commandPalette"; const activeEditor = window.activeTextEditor; const docUri = uri ?? activeEditor?.document.uri; if (!docUri) { window.showErrorMessage("No active SQL file to profile."); return; } let document = workspace.textDocuments.find( (doc) => doc.uri.toString() === docUri.toString(), ); if (!document) { try { document = await workspace.openTextDocument(docUri); } catch (error) { this.dbtTerminal.error( "CteProfiler", "Failed to open document", error, ); window.showErrorMessage("Document not found"); return; } } // If ctes not provided (command palette), re-detect from CodeLens provider if (!ctes) { const cts = new CancellationTokenSource(); // `provideCodeLenses` returns `CodeLens[] | Thenable`; // `await` handles all three (sync array, Promise, custom Thenable). const resolved = await this.cteCodeLensProvider.provideCodeLenses( document, cts.token, ); cts.dispose(); // Extract CteInfo from CodeLens arguments (index 1 is the ctes array) const profileLens = resolved.find( (cl: CodeLens) => cl.command?.command === "dbtPowerUser.profileCtes", ); ctes = profileLens?.command?.arguments?.[1] as | CteInfo[] | undefined; if (!ctes || ctes.length === 0) { window.showInformationMessage( "No CTEs found in this file to profile.", ); return; } } const telemetryEvent = TelemetryEvents["CteProfiler/Profile"]; const totalCtes = ctes.length; this.telemetry.startTelemetryEvent( telemetryEvent, { source }, { cteCount: totalCtes }, ); await window.withProgress( { location: ProgressLocation.Notification, title: `Profiling ${totalCtes} CTE${totalCtes === 1 ? "" : "s"}`, cancellable: true, }, async (progress, token) => { // Forward notification cancel to the service's own token. token.onCancellationRequested(() => { this.telemetry.sendTelemetryEvent( TelemetryEvents["CteProfiler/Cancel"], { source: "progressNotification" }, ); this.cteProfilerService.cancel(); }); // Report per-CTE increments as the service fires result updates. let lastCount = 0; const progressSub = this.cteProfilerService.onResultChanged( (result) => { if (!result || result.uri !== docUri.toString()) { return; } const done = result.ctes.length; if (done <= lastCount) { return; } const delta = done - lastCount; lastCount = done; progress.report({ increment: (delta / totalCtes) * 100, message: `${done}/${totalCtes} — ${result.ctes[done - 1]?.name ?? ""}`, }); }, ); try { await this.cteProfilerService.profileModel( docUri, document!, ctes!, ); const result = this.cteProfilerService.getResult( docUri.toString(), ); this.telemetry.endTelemetryEvent( telemetryEvent, undefined, { source, status: result?.status ?? "unknown" }, { cteCount: totalCtes, totalTimeMs: result?.totalTimeMs ?? 0, profiledCount: result?.ctes.length ?? 0, }, ); } catch (error) { this.telemetry.endTelemetryEvent( telemetryEvent, error, { source }, { cteCount: totalCtes }, ); } finally { progressSub.dispose(); } }, ); }, ), commands.registerCommand("dbtPowerUser.cancelCteProfiling", () => { this.telemetry.sendTelemetryEvent( TelemetryEvents["CteProfiler/Cancel"], { source: "commandPalette" }, ); this.cteProfilerService.cancel(); }), commands.registerCommand("dbtPowerUser.clearProfileResults", () => this.cteProfilerService.clearResults(), ), commands.registerCommand("dbtPowerUser.toggleProfileDecorations", () => this.cteProfilerDecorationProvider.toggle(), ), commands.registerCommand("dbtPowerUser.testCurrentModel", () => { // Singular data tests must be selected by their own test name, not // the surrounding model. See #1720. if (this.runTest.runSingularTestOnActiveWindowIfApplicable()) { return; } this.runModel.runTestsOnActiveWindow(); }), commands.registerCommand("dbtPowerUser.compileCurrentModel", () => this.runModel.compileModelOnActiveWindow(), ), commands.registerCommand( "dbtPowerUser.bigqueryCostEstimate", ({ returnResult }: { returnResult?: boolean }) => this.bigQueryCostEstimate.estimateCost({ returnResult }), ), commands.registerTextEditorCommand( "dbtPowerUser.sqlPreview", async (editor: TextEditor) => { const uri = editor.document.uri.with({ scheme: SqlPreviewContentProvider.SCHEME, }); const doc = await workspace.openTextDocument(uri); const isOpen = window.visibleTextEditors.some( (e) => e.document.uri === uri, ); await window.showTextDocument(doc, ViewColumn.Beside, false); await languages.setTextDocumentLanguage(doc, "sql"); if (!isOpen) { await commands.executeCommand("workbench.action.lockEditorGroup"); await commands.executeCommand( "workbench.action.focusPreviousGroup", ); } else { await commands.executeCommand("workbench.action.closeActiveEditor"); return; } }, ), commands.registerCommand( "dbtPowerUser.goToDocumentationEditor", async () => { await commands.executeCommand( "workbench.view.extension.docs_edit_view", ); }, ), commands.registerCommand("dbtPowerUser.runTest", (model) => { // Tree-item invocation (from the test treeview): run the selected // test node — never a singular test, always a generic test. if (model !== undefined) { this.runModel.runModelOnNodeTreeItem(RunModelType.TEST)(model); return; } // Command-palette invocation (no tree item): route singular test // files to `dbt test --select `; otherwise fall back to // running the generic tests attached to the active model. See #1720. if (this.runTest.runSingularTestOnActiveWindowIfApplicable()) { return; } this.runModel.runModelOnNodeTreeItem(RunModelType.TEST)(model); }), commands.registerCommand("dbtPowerUser.runChildrenModels", (model) => this.runModel.runModelOnNodeTreeItem(RunModelType.RUN_CHILDREN)(model), ), commands.registerCommand( "dbtPowerUser.yamlRunModel", (uri: Uri, modelName: string) => { this.dbtProjectContainer.runModelByName(uri, modelName); }, ), commands.registerCommand( "dbtPowerUser.yamlTestModel", (uri: Uri, modelName: string) => { this.dbtProjectContainer.runModelTest(uri, modelName); }, ), commands.registerCommand("dbtPowerUser.runParentModels", (model) => this.runModel.runModelOnNodeTreeItem(RunModelType.RUN_PARENTS)(model), ), commands.registerCommand("dbtPowerUser.copyModelName", (model) => env.clipboard.writeText(model.label.toString()), ), commands.registerCommand("dbtPowerUser.showRunSQL", () => this.runModel.showRunSQLOnActiveWindow(), ), commands.registerCommand("dbtPowerUser.showCompiledSQL", () => this.runModel.showCompiledSQLOnActiveWindow(), ), commands.registerCommand("dbtPowerUser.generateSchemaYML", () => this.runModel.generateSchemaYMLOnActiveWindow(), ), commands.registerCommand("dbtPowerUser.generateDBTDocs", () => this.runModel.generateDBTDocsOnActiveWindow(), ), commands.registerCommand("dbtPowerUser.executeSQL", () => this.runModel.executeQueryOnActiveWindow(), ), commands.registerCommand( "dbtPowerUser.runSelectedQuery", (uri: Uri, range: Range) => this.runSelectedQuery(uri, range), ), commands.registerCommand( "dbtPowerUser.runCteWithDependencies", (uri: Uri, cteIndex: number, ctes: CteInfo[]) => this.runCteWithDependencies(uri, cteIndex, ctes), ), commands.registerCommand("dbtPowerUser.summarizeQuery", () => this.eventEmitterService.fire({ command: "dbtPowerUser.summarizeQuery", payload: {}, }), ), commands.registerCommand("dbtPowerUser.changeQuery", () => this.eventEmitterService.fire({ command: "dbtPowerUser.changeQuery", payload: {}, }), ), commands.registerCommand("dbtPowerUser.translateQuery", () => this.eventEmitterService.fire({ command: "dbtPowerUser.translateQuery", payload: {}, }), ), commands.registerCommand( "dbtPowerUser.createModelBasedonSourceConfig", (params) => { this.runModel.createModelBasedonSourceConfig(params); }, ), commands.registerCommand("dbtPowerUser.buildCurrentModel", () => this.runModel.buildModelOnActiveWindow(), ), commands.registerCommand("dbtPowerUser.buildCurrentProject", () => { if (!window.activeTextEditor) { return; } const activeFileUri = window.activeTextEditor.document.uri; if (!activeFileUri) { this.dbtTerminal.debug( "buildCurrentProject", "skipping buildCurrentProject without active file", ); return; } const dbtProject = this.dbtProjectContainer.findDBTProject(activeFileUri); if (!dbtProject) { this.dbtTerminal.debug( "buildCurrentProject", `buildCurrentProject unable to find dbtproject by active file: ${activeFileUri.path}`, ); return; } this.dbtTerminal.debug( "buildCurrentProject", `building current project: ${dbtProject.getProjectName()} with active file: ${ activeFileUri.path }`, ); dbtProject.buildProject(); }), commands.registerCommand("dbtPowerUser.cleanCurrentProject", () => { if (!window.activeTextEditor) { return; } const activeFileUri = window.activeTextEditor.document.uri; if (!activeFileUri) { this.dbtTerminal.debug( "cleanCurrentProject", "skipping cleanCurrentProject without active file", ); return; } const dbtProject = this.dbtProjectContainer.findDBTProject(activeFileUri); if (!dbtProject) { this.dbtTerminal.debug( "cleanCurrentProject", `cleanCurrentProject unable to find dbtproject by active file: ${activeFileUri.path}`, ); return; } this.dbtTerminal.debug( "cleanCurrentProject", `cleaning current project: ${dbtProject.getProjectName()} with active file: ${ activeFileUri.path }`, ); dbtProject.clean(); }), commands.registerCommand("dbtPowerUser.buildChildrenModels", () => this.runModel.buildModelOnActiveWindow(RunModelType.BUILD_CHILDREN), ), commands.registerCommand("dbtPowerUser.buildParentModels", () => this.runModel.buildModelOnActiveWindow(RunModelType.BUILD_PARENTS), ), commands.registerCommand("dbtPowerUser.buildChildrenParentModels", () => this.runModel.buildModelOnActiveWindow( RunModelType.BUILD_CHILDREN_PARENTS, ), ), commands.registerCommand("dbtPowerUser.sqlToModel", () => this.sqlToModel.getModelFromSql(), ), commands.registerCommand("dbtPowerUser.validateSql", () => this.validateSql.validateSql(), ), commands.registerCommand("dbtPowerUser.altimateScan", () => this.altimateScan.getProblems(), ), commands.registerCommand("dbtPowerUser.clearAltimateScanResults", () => this.altimateScan.clearProblems(), ), commands.registerCommand("dbtPowerUser.validateProject", () => { const pickedProject: ProjectQuickPickItem | undefined = this.dbtProjectContainer.getFromWorkspaceState( "dbtPowerUser.projectSelected", ); this.walkthroughCommands.validateProjects(pickedProject); }), commands.registerCommand("dbtPowerUser.installDeps", () => { this.dbtProjectContainer.setToGlobalState( "showSetupWalkthrough", false, ); const pickedProject: ProjectQuickPickItem | undefined = this.dbtProjectContainer.getFromWorkspaceState( "dbtPowerUser.projectSelected", ); this.walkthroughCommands.installDeps(pickedProject); }), commands.registerCommand( "dbtPowerUser.openSetupWalkthrough", async () => { this.eventEmitterService.eventEmitter.fire({ command: "onboarding:render", payload: { initialStep: "prerequisites" }, }); }, ), commands.registerCommand( "dbtPowerUser.openTutorialWalkthrough", async () => { this.eventEmitterService.eventEmitter.fire({ command: "onboarding:render", payload: { initialStep: "finish" }, }); }, ), commands.registerCommand("dbtPowerUser.associateFileExts", async () => { commands.executeCommand( "workbench.action.openSettings", "@id:files.associations", ); }), commands.registerCommand("dbtPowerUser.openDatapilotWithQuery", () => this.eventEmitterService.fire({ command: "dbtPowerUser.openDatapilotWithQuery", payload: {}, }), ), commands.registerCommand("dbtPowerUser.showHelpDatapilot", () => this.eventEmitterService.fire({ command: "dbtPowerUser.openHelpInDatapilot", payload: {}, }), ), commands.registerCommand( "dbtPowerUser.createConversation", (reply: CommentReply) => { try { this.conversationController.createConversation(reply); } catch (err) { window.showErrorMessage( extendErrorWithSupportLinks((err as Error).message), ); } }, ), commands.registerCommand( "dbtPowerUser.replyToConversation", (reply: CommentReply) => { try { this.conversationController.replyToConversation(reply); } catch (err) { window.showErrorMessage( extendErrorWithSupportLinks((err as Error).message), ); } }, ), commands.registerCommand( "dbtPowerUser.resolveConversation", (thread: CommentThread) => { try { this.conversationController.resolveConversation( thread as ConversationCommentThread, ); } catch (err) { window.showErrorMessage( extendErrorWithSupportLinks((err as Error).message), ); } }, ), commands.registerCommand( "dbtPowerUser.copyDbtDocsLink", (thread: CommentThread) => { try { this.conversationController.copyThreadLink( thread as ConversationCommentThread, ); } catch (err) { window.showErrorMessage( extendErrorWithSupportLinks((err as Error).message), ); } }, ), commands.registerCommand( "dbtPowerUser.viewInDocEditor", (thread: CommentThread) => { try { this.conversationController.viewInDocEditor( thread as ConversationCommentThread, ); } catch (err) { window.showErrorMessage( extendErrorWithSupportLinks((err as Error).message), ); } }, ), commands.registerCommand( "dbtPowerUser.viewInDbtDocs", (thread: CommentThread) => { try { this.conversationController.viewInDbtDocs( thread as ConversationCommentThread, ); } catch (err) { window.showErrorMessage( extendErrorWithSupportLinks((err as Error).message), ); } }, ), commands.registerCommand("dbtPowerUser.printEnvVars", () => { const activeFolder = window.activeTextEditor ? workspace.getWorkspaceFolder(window.activeTextEditor.document.uri) : undefined; return this.pythonEnvironment.printEnvVars(activeFolder); }), commands.registerCommand( "dbtPowerUser.detectPythonFromTerminal", async () => { // Check if there's a terminal open that we can detect from if ( !window.activeTerminal && !window.terminals.some((t) => t.shellIntegration) ) { const action = await window.showWarningMessage( "No terminal is open. Please open a terminal with your dbt environment activated, then try again.", "Open Terminal", ); if (action === "Open Terminal") { await commands.executeCommand( "workbench.action.terminal.toggleTerminal", ); } return; } const detectedPath = await this.pythonEnvironment.detectPythonFromShell(); if (!detectedPath) { window.showWarningMessage( "Could not find a Python interpreter with dbt installed in your terminal. " + "Make sure dbt is installed and the correct environment is activated, then try again.", ); return; } const currentOverride = workspace .getConfiguration("dbt") .get("dbtPythonPathOverride", ""); if (currentOverride === detectedPath) { window.showInformationMessage( `Python path is already set to: ${detectedPath}`, ); return; } const action = await window.showInformationMessage( `Found Python with dbt at: ${detectedPath}. Use this as the Python interpreter?`, "Yes", "No", ); if (action === "Yes") { await workspace .getConfiguration("dbt") .update("dbtPythonPathOverride", detectedPath); window.showInformationMessage( `Python path set to: ${detectedPath}. The extension will reload.`, ); // Re-detect dbt with the new path await this.dbtProjectContainer.detectDBT(); await this.dbtProjectContainer.initialize(); } }, ), commands.registerCommand("dbtPowerUser.diagnostics", async () => { try { this.diagnosticsOutputChannel.show(); this.diagnosticsOutputChannel.logLine("Diagnostics started..."); this.diagnosticsOutputChannel.logNewLine(); // Printing env vars per project const dbtProjects = this.dbtProjectContainer.getProjects(); if (dbtProjects.length > 0) { for (const project of dbtProjects) { const projectFolder = workspace.getWorkspaceFolder( project.projectRoot, ); this.diagnosticsOutputChannel.logBlockWithHeader( [ `Printing environment variables for project: ${project.getProjectName()}`, ` (root: ${project.projectRoot.fsPath})`, "* Please remove any sensitive information before sending it to us", ], Object.entries( this.pythonEnvironment.getEnvironmentVariables(projectFolder), ).map(([key, value]) => `${key}=${value}`), ); this.diagnosticsOutputChannel.logNewLine(); } } else { const activeFolder = window.activeTextEditor ? workspace.getWorkspaceFolder( window.activeTextEditor.document.uri, ) : undefined; this.diagnosticsOutputChannel.logBlockWithHeader( [ "Printing environment variables...", "* Please remove any sensitive information before sending it to us", ], Object.entries( this.pythonEnvironment.getEnvironmentVariables(activeFolder), ).map(([key, value]) => `${key}=${value}`), ); this.diagnosticsOutputChannel.logNewLine(); } // Printing python paths this.diagnosticsOutputChannel.logBlockWithHeader( [ "Printing all python paths...", "* Please remove any sensitive information before sending it to us", ], this.pythonEnvironment.allPythonPaths.map(({ path }) => path), ); this.diagnosticsOutputChannel.logNewLine(); // Printing extension settings const dbtSettings = workspace.getConfiguration().inspect("dbt"); const globalValue: any = dbtSettings?.globalValue || {}; const defaultValue: any = dbtSettings?.defaultValue || {}; const workspaceValue: any = dbtSettings?.workspaceValue || {}; const settingKeys = [ ...Object.keys(globalValue), ...Object.keys(defaultValue), ...Object.keys(workspaceValue), ]; this.diagnosticsOutputChannel.logBlockWithHeader( [ "Printing extension settings...", "* Please remove any sensitive information before sending it to us", ], settingKeys.map((key) => { const value = workspace.getConfiguration("dbt").get(key); let overridenText = ""; if (!deepEqual(value, defaultValue[key])) { if (deepEqual(value, workspaceValue[key])) { overridenText = `${key} is overridden in workspace settings`; } else if (deepEqual(value, globalValue[key])) { overridenText = `${key} is overridden in user settings`; } } const valueText = Array.isArray(value) || typeof value === "object" ? JSON.stringify(value) : value; return `${key}=${valueText}\t\t${overridenText}`; }), ); this.diagnosticsOutputChannel.logNewLine(); // Printing extension and setup info const dbtIntegrationMode = workspace .getConfiguration("dbt") .get("dbtIntegration", "core"); const allowListFolders = workspace .getConfiguration("dbt") .get("allowListFolders", []); const apiConnectivity = await this.altimate.checkApiConnectivity(); this.diagnosticsOutputChannel.logBlock([ `Python Path=${this.pythonEnvironment.pythonPath}`, `VSCode version=${version}`, `Extension version=${ extensions.getExtension("innoverio.vscode-dbt-power-user") ?.packageJSON?.version }`, `DBT integration mode=${dbtIntegrationMode}`, `First workspace path=${getFirstWorkspacePath()}`, `Altimate API connectivity=${apiConnectivity.status}`, apiConnectivity.errorMsg ? `Altimate API connectivity error=${apiConnectivity.errorMsg}` : "", `AllowList Folders=${allowListFolders}`, ]); this.diagnosticsOutputChannel.logNewLine(); if (!this.dbtClient.pythonInstalled) { this.diagnosticsOutputChannel.logLine("Python is not installed"); this.diagnosticsOutputChannel.logLine( "Can't proceed further without fixing python installation", ); return; } this.diagnosticsOutputChannel.logLine("Python is installed"); if (!this.dbtClient.dbtInstalled) { this.diagnosticsOutputChannel.logLine("DBT is not installed"); this.diagnosticsOutputChannel.logLine( "Can't proceed further without fixing dbt installation", ); return; } this.diagnosticsOutputChannel.logLine("DBT is installed"); const dbtWorkspaces = this.dbtProjectContainer.dbtWorkspaceFolders; this.diagnosticsOutputChannel.logLine( `Number of workspaces=${dbtWorkspaces.length}`, ); for (const w of dbtWorkspaces) { this.diagnosticsOutputChannel.logHorizontalRule(); this.diagnosticsOutputChannel.logLine( `Workspace Path=${w.workspaceFolder.uri.fsPath}`, ); this.diagnosticsOutputChannel.logLine( `Adapters=${w.getAdapters()}`, ); this.diagnosticsOutputChannel.logLine( `AllowList Folders=${w.getAllowListFolders()}`, ); w.projectDiscoveryDiagnostics.forEach((uri, diagnostics) => { this.diagnosticsOutputChannel.logLine( `Problems for ${uri.fsPath}`, ); diagnostics.forEach((d) => { this.diagnosticsOutputChannel.logLine( `source=${d.source}\tmessage=${d.message}`, ); }); }); this.diagnosticsOutputChannel.logHorizontalRule(); } const projects = this.dbtProjectContainer.getProjects(); this.diagnosticsOutputChannel.logLine( `Number of projects=${projects.length}`, ); if (projects.length === 0) { this.diagnosticsOutputChannel.logLine("No project detected"); this.diagnosticsOutputChannel.logLine( "Can't proceed further without project", ); return; } this.diagnosticsOutputChannel.logNewLine(); for (const project of projects) { try { this.diagnosticsOutputChannel.logHorizontalRule(); this.diagnosticsOutputChannel.logLine( `Printing information for ${project.getProjectName()}`, ); this.diagnosticsOutputChannel.logHorizontalRule(); await this.printProjectInfo(project); } catch (e) { this.diagnosticsOutputChannel.logNewLine(); this.diagnosticsOutputChannel.logLine( "Failed to print all the info for the project...", ); this.diagnosticsOutputChannel.logLine(`Error=${e}`); } finally { this.diagnosticsOutputChannel.logHorizontalRule(); } } this.diagnosticsOutputChannel.logNewLine(); this.diagnosticsOutputChannel.logLine( "Diagnostics completed successfully...", ); } catch (e) { this.diagnosticsOutputChannel.logNewLine(); this.diagnosticsOutputChannel.logLine( "Diagnostics ended with error...", ); this.diagnosticsOutputChannel.logLine(`Error=${e}`); } }), commands.registerCommand( "dbtPowerUser.createDatapilotNotebook", async (args: OpenNotebookRequest | undefined) => { this.notebookController.createNotebook(args); }, ), commands.registerCommand( "dbtPowerUser.openTargetSelector", async ( targets, project: DBTProject, statusBar, currentTarget?: string, ) => { try { if (!targets) { return; } this.dbtTerminal.debug( "OpenTargetSelector", "Showing following targets", targets, ); const sortedTargets = (targets as string[]).sort((a, b) => { if (a === currentTarget) { return -1; } if (b === currentTarget) { return 1; } return 0; }); const items = sortedTargets.map((t) => ({ label: t, description: t === currentTarget ? "(current)" : "", })); const selected = await window.showQuickPick(items, { title: "Select your target", canPickMany: false, }); const target = selected?.label; if (target) { await project.setSelectedTarget(target); await statusBar.updateStatusBar(); this.dbtTerminal.info( "OpenTargetSelector", "Selecting target", true, target, ); } } catch (error) { this.dbtTerminal.error( "OpenTargetSelector", "An error occurred while changing target", error, ); window.showErrorMessage( "An error occurred while changing target: " + error, ); } }, ), commands.registerCommand( "dbtPowerUser.createSqlFile", async (args: { code?: string; fileName?: string } | undefined) => { const { code, fileName } = args || {}; try { const project = await this.queryManifestService.getOrPickProjectFromWorkspace(); if (!project) { window.showErrorMessage("No dbt project selected."); return; } // Open a new untitled sql file by default let docOpenPromise = workspace.openTextDocument({ language: "jinja-sql", }); // If file name is provided, open the file in the project if (fileName) { const uri = Uri.parse( `${project.projectRoot}/${fileName}-${getFormattedDateTime()}.sql`, ).with({ scheme: "untitled" }); docOpenPromise = workspace.openTextDocument(uri); } const annotationDecoration: TextEditorDecorationType = window.createTextEditorDecorationType({ rangeBehavior: DecorationRangeBehavior.OpenOpen, }); const contentText = "Enter your query here and execute it just like any dbt model file. This file is unsaved, you can either save it to your project or save it as a bookmark for later usage or share it with your team members."; const decorations = [ { renderOptions: { before: { color: "#666666", contentText, // hacking to add more css properties width: "90%;display: block;white-space: pre-line;", }, }, range: new Range(2, 0, 2, 0), }, ]; docOpenPromise.then((doc) => { // set this to sql language so we can bind codelens and other features languages.setTextDocumentLanguage(doc, "jinja-sql"); window.showTextDocument(doc).then((editor) => { editor.edit((editBuilder) => { const entireDocumentRange = new Range( doc.positionAt(0), doc.positionAt(doc.getText().length), ); editBuilder.replace(entireDocumentRange, code || "\n"); editor.setDecorations(annotationDecoration, decorations); setTimeout(() => { commands.executeCommand("cursorMove", { to: "up", by: "line", value: 1, }); }, 0); const disposable = workspace.onDidChangeTextDocument((e) => { const activeEditor = window.activeTextEditor; if (activeEditor && e.document === editor.document) { if (activeEditor.document.getText().trim()) { activeEditor.setDecorations(annotationDecoration, []); disposable.dispose(); } } }); }); }); }); } catch (e) { const message = (e as Error).message; this.dbtTerminal.error("createSqlFile", message, e, true); window.showErrorMessage(message); } }, ), commands.registerCommand("dbtPowerUser.sqlLineage", async () => { window.withProgress( { title: "Retrieving SQL visualization", location: ProgressLocation.Notification, cancellable: false, }, async (_, token) => { try { const modelName = this.sqlLineagePanel.getActiveEditorFilename(); const lineage = await this.sqlLineagePanel.getSQLLineage(token); const panel = window.createWebviewPanel( SQLLineagePanel.viewType, `${modelName} - visualization`, ViewColumn.Two, { retainContextWhenHidden: true, enableScripts: true }, ); this.sqlLineagePanel.renderSqlVisualizer(panel, lineage); } catch (e) { const errorMessage = (e as Error)?.message; this.dbtTerminal.error("sqlLineage", errorMessage, e, true); window.showErrorMessage(errorMessage); } }, ); }), commands.registerCommand( "dbtPowerUser.showDocumentation", async (modelName) => { const result = queryManifestService.getEventByCurrentProject(); if (!result) { return; } const { event } = result; if (!event) { return; } const { nodeMetaMap } = event; const model = nodeMetaMap.lookupByBaseName(modelName); if (!model?.path) { return; } const doc = await workspace.openTextDocument(Uri.file(model.path)); await window.showTextDocument(doc); await commands.executeCommand("dbtPowerUser.DocsEdit.focus"); }, ), commands.registerCommand( "dbtPowerUser.showDatapilotNotebooksQuickPick", async () => { const notebookQuickPick = new NotebookQuickPick(); await notebookQuickPick.showNotebookPicker(); }, ), commands.registerCommand( "dbtPowerUser.showNotebookProfileQuery", async () => { await commands.executeCommand( "dbtPowerUser.createDatapilotNotebook", { template: "Profile your query", }, ); }, ), commands.registerCommand( "dbtPowerUser.showNotebookTestSuggestions", async () => { await commands.executeCommand( "dbtPowerUser.createDatapilotNotebook", { template: "Get test suggestions", }, ); }, ), commands.registerCommand( "dbtPowerUser.showNotebookGenerateBaseModelSql", async () => { await commands.executeCommand( "dbtPowerUser.createDatapilotNotebook", { template: "Generate dbt base model sql", }, ); }, ), commands.registerCommand( "dbtPowerUser.showNotebookGenerateModelYaml", async () => { await commands.executeCommand( "dbtPowerUser.createDatapilotNotebook", { template: "Generate dbt model yaml", }, ); }, ), commands.registerCommand( "dbtPowerUser.showNotebookGenerateModelCTE", async () => { await commands.executeCommand( "dbtPowerUser.createDatapilotNotebook", { template: "Generate dbt model CTE", }, ); }, ), commands.registerCommand("dbtPowerUser.applyDeferConfig", async () => { const projects = this.dbtProjectContainer.getProjects(); try { await Promise.all( projects.map((project) => project.applyDeferConfig()), ); window.showInformationMessage("Applied defer configuration"); } catch (error) { this.dbtTerminal.error( "applyDeferConfig", "Failed to apply defer configuration", error, ); window.showErrorMessage( `Failed to apply defer configuration: ${error}`, ); } }), commands.registerCommand( "dbtPowerUser.askAltimateAboutSelection", async () => { const context = this.altimateCodeChatService.getEditorContext(); if (!context) { return; } await this.altimateCodeChatService.openChat({ initialMessage: `Regarding this code from \`@${context.relativePath}\`:\n\`\`\`\n${context.code}\n\`\`\``, title: `Ask: ${context.fileName}`, }); }, ), commands.registerCommand("dbtPowerUser.explainWithAltimate", async () => { const context = this.altimateCodeChatService.getEditorContext(); if (!context) { return; } await this.altimateCodeChatService.openChat({ initialMessage: `Explain the following code from \`@${context.relativePath}\`:\n\`\`\`sql\n${context.code}\n\`\`\``, title: `Explain: ${context.fileName}`, }); }), commands.registerCommand( "dbtPowerUser.optimizeWithAltimate", async () => { const context = this.altimateCodeChatService.getEditorContext(); if (!context) { return; } await this.altimateCodeChatService.openChat({ initialMessage: `Optimize the following SQL from \`@${context.relativePath}\` for performance and readability:\n\`\`\`sql\n${context.code}\n\`\`\``, title: `Optimize: ${context.fileName}`, }); }, ), commands.registerCommand( "dbtPowerUser.analyzeFileWithAltimate", async (uri?: Uri) => { const fileUri = uri ?? window.activeTextEditor?.document.uri; if (!fileUri) { return; } const ctx = this.altimateCodeChatService.getContextForUri(fileUri); if (!ctx) { return; } await this.altimateCodeChatService.openChat({ initialMessage: `Analyze \`@${ctx.relativePath}\` for dbt best practices, performance, and documentation completeness.`, title: `Analyze: ${ctx.fileName}`, }); }, ), commands.registerCommand("dbtPowerUser.openAltimateChat", async () => { const context = this.altimateCodeChatService.getEditorContext(); if (context) { await this.altimateCodeChatService.openChat({ initialMessage: `Help me with \`@${context.relativePath}\`:\n\`\`\`\n${context.code}\n\`\`\``, title: `Chat: ${context.fileName}`, }); } else { await this.altimateCodeChatService.openChat({ initialMessage: "How can I help you with your dbt project?", title: "Altimate Code Chat", }); } }), ); } private async printProjectInfo(project: DBTProject) { this.diagnosticsOutputChannel.logLine( `Project Name=${project.getProjectName()}`, ); this.diagnosticsOutputChannel.logLine( `Adapter Type=${project.getAdapterType()}`, ); const dbtVersion = project.getDBTVersion(); if (!dbtVersion) { this.diagnosticsOutputChannel.logLine("DBT is not initialized properly"); } else { this.diagnosticsOutputChannel.logLine( `DBT version=${dbtVersion.join(".")}`, ); } if (!project.getPythonBridgeStatus()) { this.diagnosticsOutputChannel.logLine("Python bridge is not connected"); } else { this.diagnosticsOutputChannel.logLine("Python bridge is connected"); } this.diagnosticsOutputChannel.logNewLine(); const paths = [ { pathType: "DBT Project File", path: project.getDBTProjectFilePath(), }, { pathType: "Target", path: project.getTargetPath() }, { pathType: "PackageInstall", path: project.getPackageInstallPath(), }, { pathType: "Manifest", path: project.getManifestPath() }, { pathType: "Catalog", path: project.getCatalogPath() }, ...(project.getModelPaths() || []).map((path) => ({ pathType: "Model", path, })), ...(project.getSeedPaths() || []).map((path) => ({ pathType: "Seed", path, })), ...(project.getMacroPaths() || []).map((path) => ({ pathType: "Macro", path, })), ]; for (const p of paths) { if (!p.path) { this.diagnosticsOutputChannel.logLine(`${p.pathType} path not found`); continue; } let line = `${p.pathType} path=${p.path}\t\t`; if (!existsSync(p.path)) { line += "File doesn't exists at location"; } else { line += "File exists at location"; } this.diagnosticsOutputChannel.logLine(line); } const dbtProjectFilePath = project.getDBTProjectFilePath(); if (existsSync(dbtProjectFilePath)) { this.diagnosticsOutputChannel.logNewLine(); this.diagnosticsOutputChannel.logNewLine(); this.diagnosticsOutputChannel.logLine("dbt_project.yml"); this.diagnosticsOutputChannel.logHorizontalRule(); const fileContent = readFileSync(dbtProjectFilePath, "utf8"); this.diagnosticsOutputChannel.logLine(fileContent.replace(/\n/g, "\r\n")); this.diagnosticsOutputChannel.logHorizontalRule(); } this.diagnosticsOutputChannel.logNewLine(); const diagnostics = project.getAllDiagnostic(); this.diagnosticsOutputChannel.logLine( `Number of diagnostics issues=${diagnostics.length}`, ); for (const d of diagnostics) { this.diagnosticsOutputChannel.logLine(d.message); } await project.debug(false); } private runSelectedQuery(uri: Uri, range: Range): void { // Get the document and extract the selected text const document = workspace.textDocuments.find( (doc) => doc.uri.toString() === uri.toString(), ); if (!document) { window.showErrorMessage("Document not found"); return; } const selectedQuery = document.getText(range); if (!selectedQuery.trim()) { window.showErrorMessage("No query selected"); return; } // Create a model name based on the selection - use "cte_query" as default const modelName = "cte_query"; // Execute the selected query using the existing infrastructure this.dbtProjectContainer.executeSQL(uri, selectedQuery, modelName); } private async runCteWithDependencies( uri: Uri, cteIndex: number, ctes: CteInfo[], ): Promise { this.dbtTerminal.debug( "CteExecution", `Starting CTE execution for index ${cteIndex} with ${ctes.length} total CTEs`, ); try { // Get the document asynchronously let document = workspace.textDocuments.find( (doc) => doc.uri.toString() === uri.toString(), ); if (!document) { // Try to open the document if not found in workspace try { document = await workspace.openTextDocument(uri); } catch (error) { this.dbtTerminal.error( "CteExecution", `Failed to open document: ${uri.toString()}`, error, ); window.showErrorMessage("Document not found and could not be opened"); return; } } const text = document.getText(); // Find the target CTE and all its dependencies const targetCte = ctes[cteIndex]; if (!targetCte) { this.dbtTerminal.warn( "CteExecution", `CTE not found at index ${cteIndex}, available CTEs: ${ctes.length}`, ); window.showErrorMessage("CTE not found"); return; } this.dbtTerminal.debug( "CteExecution", `Target CTE: ${targetCte.name} (index: ${targetCte.index})`, ); // Get all CTEs from the same WITH clause that come before or at the target index const sameScopeCtesUpToTarget = ctes.filter( (cte) => cte.withClauseStart === targetCte.withClauseStart && cte.index <= targetCte.index, ); this.dbtTerminal.debug( "CteExecution", `Found ${sameScopeCtesUpToTarget.length} CTEs in dependency chain: ${sameScopeCtesUpToTarget.map((c) => c.name).join(", ")}`, ); // Build the complete query with dependencies const cteDefinitions: string[] = []; for (const cte of sameScopeCtesUpToTarget) { // Extract the full CTE definition (name + AS + query) const cteStart = cte.range.start; // Get from CTE name to end of its query const cteStartPos = document.offsetAt(cteStart); // Improved regex to handle quoted identifiers, dotted names, and complex column lists // Supports: identifier, "quoted identifier", schema.table, `backtick quoted`, [bracket quoted] const cteNameMatch = text .substring(cteStartPos) .match( /^((?:[a-zA-Z_][a-zA-Z0-9_]*|"[^"]+"|`[^`]+`|\[[^\]]+\])(?:\.(?:[a-zA-Z_][a-zA-Z0-9_]*|"[^"]+"|`[^`]+`|\[[^\]]+\]))*(?:\s*\([^)]*\))?)\s+as\s*\(/i, ); if (cteNameMatch) { const cteQuery = document.getText(cte.queryRange); const fullCteDefinition = `${cteNameMatch[1]} AS (\n${cteQuery}\n)`; cteDefinitions.push(fullCteDefinition); this.dbtTerminal.debug( "CteExecution", `Added CTE to query: ${cteNameMatch[1]} (${cteQuery.length} chars)`, ); } else { this.dbtTerminal.warn( "CteExecution", `Could not parse CTE definition for: ${cte.name}`, ); } } // Check if we have any valid CTE definitions if (cteDefinitions.length === 0) { this.dbtTerminal.warn( "CteExecution", "No valid CTE definitions found, cannot build query", ); window.showErrorMessage("Failed to extract CTE definitions"); return; } // Build the complete query including preamble before WITH clause // Extract everything before the WITH clause (dbt configs, variables, etc.) const preamble = text.substring(0, targetCte.withClauseStart).trim(); let query = ""; if (preamble) { query += preamble + "\n\n"; this.dbtTerminal.debug( "CteExecution", `Including preamble (${preamble.length} chars) before WITH clause`, ); } query += "WITH "; query += cteDefinitions.join(",\n"); // Add a simple SELECT to execute the target CTE with proper quoting const quotedTargetName = this.quoteSqlIdentifier(targetCte.name); query += `\nSELECT * FROM ${quotedTargetName}`; this.dbtTerminal.debug( "CteExecution", `Generated query length: ${query.length} characters`, ); // Create a unique model name with timestamp to prevent collisions const timestamp = Date.now(); const hash = this.generateShortHash(targetCte.name + timestamp); const modelName = `cte_${targetCte.name}_${hash}`; this.dbtTerminal.debug( "CteExecution", `Executing CTE query with model name: ${modelName}`, ); // Execute the complete query with dependencies this.dbtProjectContainer.executeSQL(uri, query, modelName); } catch (error) { this.dbtTerminal.error( "CteExecution", "Unexpected error in runCteWithDependencies", error, ); window.showErrorMessage( `Failed to execute CTE: ${error instanceof Error ? error.message : "Unknown error"}`, ); } } private quoteSqlIdentifier(identifier: string): string { // If identifier is already quoted or contains dots, return as-is if (identifier.match(/^["'`\[]/) || identifier.includes(".")) { return identifier; } // If identifier contains special characters or spaces, quote it if (!identifier.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) { return `"${identifier}"`; } return identifier; } private generateShortHash(input: string): string { // Simple hash function to generate a short unique suffix let hash = 0; for (let i = 0; i < input.length; i++) { const char = input.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; // Convert to 32-bit integer } return Math.abs(hash).toString(36).substring(0, 6); } dispose() { while (this.disposables.length) { const x = this.disposables.pop(); if (x) { x.dispose(); } } } } ================================================ FILE: src/commands/runModel.ts ================================================ import path = require("path"); import { RunModelType } from "@altimateai/dbt-integration"; import { Uri, window } from "vscode"; import { GenerateModelFromSourceParams } from "../code_lens_provider/sourceModelCreationCodeLensProvider"; import { DBTProjectContainer } from "../dbt_client/dbtProjectContainer"; import { NodeTreeItem } from "../treeview_provider/modelTreeviewProvider"; import { extendErrorWithSupportLinks } from "../utils"; export class RunModel { constructor(private dbtProjectContainer: DBTProjectContainer) {} runModelOnActiveWindow(type?: RunModelType) { if (!window.activeTextEditor) { return; } const fullPath = window.activeTextEditor.document.uri; this.runDBTModel(fullPath, type); } buildModelOnActiveWindow(type?: RunModelType) { if (!window.activeTextEditor) { return; } const fullPath = window.activeTextEditor.document.uri; this.buildDBTModel(fullPath, type); } runTestsOnActiveWindow() { if (!window.activeTextEditor) { return; } const fullPath = window.activeTextEditor.document.uri; this.runDBTModelTest(fullPath); } compileModelOnActiveWindow() { if (!window.activeTextEditor) { return; } const fullPath = window.activeTextEditor.document.uri; this.compileDBTModel(fullPath); } compileQueryOnActiveWindow() { if (!window.activeTextEditor) { return; } const fullPath = window.activeTextEditor.document.uri; const query = window.activeTextEditor.document.getText(); if (query !== undefined) { this.compileDBTQuery(fullPath, query); } } private getQuery() { if (!window.activeTextEditor) { return; } const cursor = window.activeTextEditor.selection; return window.activeTextEditor.document.getText( cursor.isEmpty ? undefined : cursor, ); } executeQueryOnActiveWindow() { const query = this.getQuery(); if (query === undefined) { return; } const modelPath = window.activeTextEditor?.document.uri; if (modelPath) { const modelName = path.basename(modelPath.fsPath, ".sql"); this.executeSQL(window.activeTextEditor!.document.uri, query, modelName); } } runModelOnNodeTreeItem(type: RunModelType) { return (model?: NodeTreeItem) => { if (model === undefined) { this.runModelOnActiveWindow(type); return; } if (!model.url) { return; } switch (type) { case RunModelType.TEST: { if (model.label) { this.runDBTTest( Uri.file(model.url), model.label.toString().split(".")[0], ); } break; } case RunModelType.BUILD_CHILDREN: case RunModelType.BUILD_CHILDREN_PARENTS: case RunModelType.BUILD_PARENTS: { // Catch Parents || Children RunTypes this.buildDBTModel(Uri.file(model.url), type); break; } case RunModelType.RUN_CHILDREN: case RunModelType.RUN_PARENTS: { // Catch Parents || Children RunTypes this.runDBTModel(Uri.file(model.url), type); break; } } }; } showCompiledSQLOnActiveWindow() { const fullPath = window.activeTextEditor?.document.uri; if (fullPath !== undefined) { this.showCompiledSQL(fullPath); } } generateSchemaYMLOnActiveWindow() { const fullPath = window.activeTextEditor?.document.uri; if (fullPath !== undefined) { this.generateSchemaYML(fullPath); } } showRunSQLOnActiveWindow() { const fullPath = window.activeTextEditor?.document.uri; if (fullPath !== undefined) { this.showRunSQL(fullPath); } } generateDBTDocsOnActiveWindow() { const fullPath = window.activeTextEditor?.document.uri; if (fullPath !== undefined) { this.generateDBTDocs(fullPath); } } runDBTModel(modelPath: Uri, type?: RunModelType) { this.dbtProjectContainer.runModel(modelPath, type); } buildDBTModel(modelPath: Uri, type?: RunModelType) { this.dbtProjectContainer.buildModel(modelPath, type); } compileDBTModel(modelPath: Uri, type?: RunModelType) { this.dbtProjectContainer.compileModel(modelPath, type); } generateDBTDocs(modelPath: Uri, type?: RunModelType) { this.dbtProjectContainer.generateDocs(modelPath); } compileDBTQuery(modelPath: Uri, query: string) { this.dbtProjectContainer.compileQuery(modelPath, query); } runDBTTest(modelPath: Uri, testName: string) { this.dbtProjectContainer.runTest(modelPath, testName); } runDBTModelTest(modelPath: Uri) { const modelName = path.basename(modelPath.fsPath, ".sql"); this.dbtProjectContainer.runModelTest(modelPath, modelName); } async executeSQL(uri: Uri, query: string, modelName: string) { this.dbtProjectContainer.executeSQL(uri, query, modelName); } showCompiledSQL(modelPath: Uri) { this.dbtProjectContainer.showCompiledSQL(modelPath); } generateSchemaYML(modelPath: Uri) { const modelName = path.basename(modelPath.fsPath, ".sql"); this.dbtProjectContainer.generateSchemaYML(modelPath, modelName); } showRunSQL(modelPath: Uri) { this.dbtProjectContainer.showRunSQL(modelPath); } createModelBasedonSourceConfig(params: GenerateModelFromSourceParams) { const project = this.dbtProjectContainer.findDBTProject(params.currentDoc); const sourcePath = path.dirname(params.currentDoc.fsPath); if (project) { project.generateModel(params.sourceName, params.tableName, sourcePath); } else { window.showErrorMessage( extendErrorWithSupportLinks( "Could not generate model! No project found for " + params.currentDoc.fsPath + ".", ), ); } } } ================================================ FILE: src/commands/runTest.ts ================================================ import { Uri, window } from "vscode"; import { DBTProjectContainer } from "../dbt_client/dbtProjectContainer"; import { QueryManifestService } from "../services/queryManifestService"; /** * Handles singular data tests — standalone `.sql` files that live under the * project's configured `test-paths` and are executed with `dbt test --select * `. * * These are not models and must not be dispatched through `RunModel`: running * them as a model spawns `dbt run`, which is never meaningful for a test file. * The original command entry points (`dbtPowerUser.runCurrentModel`, * `dbtPowerUser.testCurrentModel`, `dbtPowerUser.runTest`) consult this class * first and delegate to `RunModel` only when the active file is not a singular * test. */ export class RunTest { constructor( private dbtProjectContainer: DBTProjectContainer, private queryManifestService: QueryManifestService, ) {} /** * Returns the singular test name for the given file URI, or `undefined` if * the file is not a singular data test in the current project's manifest. * * Singular tests are SQL files that appear in the manifest as test resources * without `test_metadata` (generic tests like `not_null` / `unique` have * `test_metadata` populated and live in `schema.yml`, not in `.sql` files). * Detection uses the parsed manifest rather than a hardcoded `tests/` * prefix, so projects with custom `test-paths` in `dbt_project.yml` are * handled correctly. */ getSingularTestName(uri: Uri): string | undefined { const event = this.queryManifestService.getEventByDocument(uri); if (!event) { return undefined; } const filePath = uri.fsPath; for (const [name, testData] of event.testMetaMap) { if (testData.test_metadata === undefined && testData.path === filePath) { return name; } } return undefined; } /** * Runs the singular test at the given URI via * `dbt test --select `. */ runSingularTest(uri: Uri, testName: string): void { this.dbtProjectContainer.runTest(uri, testName); } /** * Attempts to run the active editor's file as a singular test. Returns * `true` if the file was a singular test and the run was dispatched, * `false` otherwise — in which case the caller should fall back to the * regular model/test behavior. */ runSingularTestOnActiveWindowIfApplicable(): boolean { if (!window.activeTextEditor) { return false; } const uri = window.activeTextEditor.document.uri; const testName = this.getSingularTestName(uri); if (testName === undefined) { return false; } this.runSingularTest(uri, testName); return true; } } ================================================ FILE: src/commands/sqlToModel.ts ================================================ import { DBTTerminal } from "@altimateai/dbt-integration"; import { inject } from "inversify"; import * as path from "path"; import { Position, ProgressLocation, Range, window } from "vscode"; import { AltimateRequest } from "../altimate"; import { DBTProjectContainer } from "../dbt_client/dbtProjectContainer"; import { ManifestCacheChangedEvent, ManifestCacheProjectAddedEvent, } from "../dbt_client/event/manifestCacheChangedEvent"; import { AltimateAuthService } from "../services/altimateAuthService"; import { TelemetryService } from "../telemetry"; import { extendErrorWithSupportLinks } from "../utils"; export class SqlToModel { private eventMap: Map = new Map(); constructor( private dbtProjectContainer: DBTProjectContainer, private telemetry: TelemetryService, private altimate: AltimateRequest, @inject("DBTTerminal") private dbtTerminal: DBTTerminal, private altimateAuthService: AltimateAuthService, ) { dbtProjectContainer.onManifestChanged((event) => this.onManifestCacheChanged(event), ); } private async onManifestCacheChanged(event: ManifestCacheChangedEvent) { event.added?.forEach((added) => { this.eventMap.set(added.project.projectRoot.fsPath, added); }); event.removed?.forEach((removed) => { this.eventMap.delete(removed.projectRoot.fsPath); }); } async getModelFromSql() { if (!this.altimateAuthService.handlePreviewFeatures()) { return; } this.telemetry.sendTelemetryEvent("sqlToModel"); if (!window.activeTextEditor) { return; } const activedoc = window.activeTextEditor; const currentFilePath = activedoc.document.uri; const model = path.basename( window.activeTextEditor!.document.fileName, ".sql", ); const project = this.dbtProjectContainer.findDBTProject(currentFilePath); if (!project) { window.showErrorMessage( extendErrorWithSupportLinks( "Could not find a dbt project. Please put the new model in a dbt project before converting to a model.", ), ); this.telemetry.sendTelemetryError("sqlToModelNoProjectError"); return; } const event = this.eventMap.get(project.projectRoot.fsPath); if (!event) { project.throwDiagnosticsErrorIfAvailable(); // If we get here, we don't know what exactly is the error. // Probably the extension is still initializing window.showErrorMessage( extendErrorWithSupportLinks( "The extension is still initializing, please retry again.", ), ); this.telemetry.sendTelemetryError("sqlToModelNoManifestError"); return; } const { nodeMetaMap, sourceMetaMap } = event; const allmodels = Array.from(nodeMetaMap.nodes()); const allsources = Array.from(sourceMetaMap.values()); const fileText = activedoc.document.getText(); try { const sqlToModelResponse = await window.withProgress( { location: ProgressLocation.Notification, title: "Convert SQL to Model...", cancellable: false, }, async () => { let compiledSql: string | undefined; try { compiledSql = await project.unsafeCompileQuery(fileText); } catch (error) { window.showErrorMessage( extendErrorWithSupportLinks( "Could not compile the SQL: " + (error as Error).message, ), ); return; } return await this.altimate.runModeller({ // if we can run this through compile sql, we can also do // conversions that were half done. if it fails, just send the text as is. sql: compiledSql || fileText, adapter: project.getAdapterType(), models: allmodels, sources: allsources, }); }, ); // if somehow the response isnt there or got an error response if ( sqlToModelResponse === undefined || sqlToModelResponse.sql === undefined ) { window.showErrorMessage( extendErrorWithSupportLinks( "Could not convert sql to model. Encountered unknown error when converting sql to model.", ), ); this.dbtTerminal.error( "sqlToModelEmptyBackendResponseError", `Could not convert sql to model for query: ${fileText}`, new Error("Empty response from backend"), ); return; } const startpos = new Position(0, 0); const endpos = new Position( activedoc.document.lineCount, activedoc.document.lineAt(activedoc.document.lineCount - 1).text.length, ); activedoc.edit((editBuilder) => { editBuilder.replace( new Range(startpos, endpos), sqlToModelResponse.sql, ); }); window.showInformationMessage( `SQL successfully converted to model ${model}`, ); } catch (err) { window.showErrorMessage( extendErrorWithSupportLinks( "Could not convert SQL to model: " + (err as Error).message, ), ); this.dbtTerminal.error( "sqlToModelError", `Could not convert sql to model for query: ${fileText}`, err, ); } } } ================================================ FILE: src/commands/tests/initCatalog.ts ================================================ import { Catalog } from "@altimateai/dbt-integration"; import { ScanContext } from "./scanContext"; import { AltimateScanStep } from "./step"; export class InitCatalog implements AltimateScanStep { public async run(scanContext: ScanContext) { const project = scanContext.project; const scanResults = scanContext.scanResults; const cata = await project.getCatalog(); if (cata.length === 0) { let catalogResults = scanResults["missingCatalog"]; if (catalogResults === undefined) { catalogResults = scanResults["missingCatalog"] = {}; } scanResults["missingCatalog"][ project.getProjectName() + project.projectRoot ] = true; } const modelDict: Record = Object.create(null); cata.forEach((model) => { const modelKey = JSON.stringify({ projectroot: project.projectRoot.fsPath, project: project.getProjectName(), database: model.table_database.toLowerCase(), schema: model.table_schema.toLowerCase(), name: model.table_name.toLowerCase(), }); modelDict[modelKey] = modelDict[modelKey] || []; modelDict[modelKey].push(model); }); return modelDict; } } ================================================ FILE: src/commands/tests/missingSchemaTest.ts ================================================ import { RESOURCE_TYPE_MODEL } from "@altimateai/dbt-integration"; import { Diagnostic, DiagnosticSeverity, Range } from "vscode"; import { ScanContext } from "./scanContext"; import { AltimateScanStep } from "./step"; export class MissingSchemaTest implements AltimateScanStep { public async run(scanContext: ScanContext) { const { eventMap: projectEventMap, diagnostics: projectDiagnostics, scanResults, } = scanContext; if (projectEventMap === undefined) { return; } const { nodeMetaMap } = projectEventMap; for (const value of nodeMetaMap.nodes()) { // blacklisting node types.. should we instead whitelist just models and sources? if ( // TODO - need to filter out only models here but the resource type isnt available !value.unique_id.startsWith(RESOURCE_TYPE_MODEL) || value.config.materialized === "seed" || value.config.materialized === "ephemeral" ) { continue; } if (!value.patch_path && value.path) { const errMessage = `Documentation missing for model: ${value.name}`; let projDiagnostic = projectDiagnostics[value.path]; if (projDiagnostic === undefined) { projectDiagnostics[value.path] = projDiagnostic = []; } projDiagnostic.push( new Diagnostic( new Range(0, 0, 0, 0), errMessage, DiagnosticSeverity.Information, ), ); // TODO - set a note that this model is missing documentation // so that we dont keep telling users that x column doc is missing let missingDocsDict = scanResults["missingDoc"]; if (missingDocsDict === undefined) { scanResults["missingDoc"] = missingDocsDict = new Set(); } missingDocsDict.add(value.unique_id); } } } } ================================================ FILE: src/commands/tests/scanContext.ts ================================================ import { Catalog } from "@altimateai/dbt-integration"; import { Diagnostic } from "vscode"; import { DBTProject } from "../../dbt_client/dbtProject"; import { ManifestCacheProjectAddedEvent } from "../../dbt_client/event/manifestCacheChangedEvent"; export interface AltimateCatalog { [projectName: string]: { [key: string]: Catalog }; } export class ScanContext { project: DBTProject; catalog: AltimateCatalog = {}; eventMap: ManifestCacheProjectAddedEvent | undefined; diagnostics: { [filepath: string]: Diagnostic[] }; scanResults: { [key: string]: any } = {}; constructor( project: DBTProject, eventMap: ManifestCacheProjectAddedEvent | undefined, ) { this.project = project; this.catalog = {}; this.eventMap = eventMap; this.diagnostics = {}; } } ================================================ FILE: src/commands/tests/staleModelColumnTest.ts ================================================ import { createFullPathForNode } from "@altimateai/dbt-integration"; import { readFileSync } from "fs"; import { Diagnostic, DiagnosticSeverity, Range, Uri } from "vscode"; import { getColumnNameByCase, removeProtocol } from "../../utils"; import { ScanContext } from "./scanContext"; import { AltimateScanStep } from "./step"; export class StaleModelColumnTest implements AltimateScanStep { private getTextLocation( modelname: string, colname: string, schemaPath: string, ): Range | undefined { // 1) Read the file at filepath const docContent: string = readFileSync(schemaPath, "utf-8"); // Use regex to find whole word matches for model const modelRegex = new RegExp(`\\bname\\:\\s*?${modelname}\\b`); const modelMatch = docContent.match(modelRegex); if (!modelMatch) { return undefined; } // 2) Search for exact matches of 'colname' that occur after 'modelname' const colRegex = new RegExp( `\\bname\\:\\s*?${colname}\\b|\\balias\\:\\s*?${colname}\\b`, "g", ); let colMatch; while ((colMatch = colRegex.exec(docContent)) !== null) { if (colMatch.index > (modelMatch.index || 0)) { // Calculate line and character number for the match const beforeMatch = docContent.substring(0, colMatch.index); const lines = beforeMatch.split("\n"); const line = lines.length - 1; const char = lines[line].length; // +1 to make it 1-based // doing this because regex contains variable number of chars. const matchLength = colMatch[0].length; // 3) Return the line number and character number return new Range( line, char + matchLength - colname.length, line, char + matchLength, ); } } return undefined; } public async run(scanContext: ScanContext) { const { project, catalog: altimateCatalog, eventMap: projectEventMap, diagnostics: projectDiagnostics, } = scanContext; const projectName = project.getProjectName(); const projectRootUri = project.projectRoot; if (projectEventMap === undefined) { return; } const { nodeMetaMap } = projectEventMap; for (const value of nodeMetaMap.nodes()) { if (value.config.materialized === "ephemeral") { // ephemeral models by nature wont be materialized so we cant verify if they are stale. continue; } const modelKey = JSON.stringify({ projectroot: projectRootUri.fsPath, project: projectName, database: value.database.toLowerCase(), schema: value.schema.toLowerCase(), name: value.alias.toLowerCase(), }); if ( Object.keys(altimateCatalog[projectName + projectRootUri]).includes( modelKey, ) ) { // do model-level checks here. const modelDict = altimateCatalog[projectName + projectRootUri][modelKey]; const allDBColumns = modelDict.map(({ column_name }) => getColumnNameByCase(column_name, project.getAdapterType()), ); const packagePath = project.getPackageInstallPath(); if (packagePath === undefined) { throw new Error( "packagePath is not defined in " + project.projectRoot.fsPath, ); } for (const existingCol of Object.keys(value.columns)) { if ( !allDBColumns.includes( getColumnNameByCase(existingCol, project.getAdapterType()), ) ) { const errMessage = `Column ${existingCol} listed in model ${value.name} is not found in the database. It may be outdated or misspelled.`; // If we are here, the patch_path is guaranteed to be defined since // we catch missing doc errors before we enter this function. const schemaPath = createFullPathForNode( projectName, projectRootUri.fsPath, value.package_name, packagePath, removeProtocol(value.patch_path), ) || Uri.joinPath( project.projectRoot, removeProtocol(value.patch_path), ).fsPath; const colInDocRange = this.getTextLocation( value.name, existingCol, schemaPath, ); let schemaDiagnostics = projectDiagnostics[schemaPath]; if (schemaDiagnostics === undefined) { projectDiagnostics[schemaPath] = schemaDiagnostics = []; } schemaDiagnostics.push( new Diagnostic( colInDocRange || new Range(0, 0, 0, 0), errMessage, DiagnosticSeverity.Warning, ), ); } } } } } } ================================================ FILE: src/commands/tests/step.ts ================================================ import { ScanContext } from "./scanContext"; export interface AltimateScanStep { run(scanContext: ScanContext): Promise; } ================================================ FILE: src/commands/tests/undocumentedModelColumnTest.ts ================================================ import { Diagnostic, DiagnosticSeverity, Range } from "vscode"; import { getColumnNameByCase } from "../../utils"; import { ScanContext } from "./scanContext"; import { AltimateScanStep } from "./step"; export class UndocumentedModelColumnTest implements AltimateScanStep { public async run(scanContext: ScanContext) { const { project, catalog: altimateCatalog, eventMap: projectEventMap, diagnostics: projectDiagnostics, scanResults, } = scanContext; const projectName = project.getProjectName(); const projectRootUri = project.projectRoot; if (projectEventMap === undefined) { return; } const { nodeMetaMap } = projectEventMap; for (const value of nodeMetaMap.nodes()) { if ( (scanResults["missingDoc"] !== undefined && scanResults["missingDoc"].has(value.unique_id)) || value.config.materialized === "seed" || value.config.materialized === "ephemeral" ) { // schema is missing, no point in looking for undocumented columns // or the model is not materialized / seed type continue; } const modelKey = JSON.stringify({ projectroot: projectRootUri.fsPath, project: projectName, database: value.database.toLowerCase(), schema: value.schema.toLowerCase(), name: value.alias.toLowerCase(), }); if ( Object.keys(altimateCatalog[projectName + projectRootUri]).includes( modelKey, ) ) { // do model-level checks here. const modelDict = altimateCatalog[projectName + projectRootUri][modelKey]; const existingColumnsLowered = Object.keys(value.columns).map((key) => getColumnNameByCase(key, project.getAdapterType()), ); for (const column of modelDict) { if ( !existingColumnsLowered.includes( getColumnNameByCase(column.column_name, project.getAdapterType()), ) ) { if (!value.path) { continue; } const errMessage = `Column ${column.column_name} is undocumented in model: ${value.name}`; let modelDiagnostics = projectDiagnostics[value.path]; if (modelDiagnostics === undefined) { projectDiagnostics[value.path] = modelDiagnostics = []; } modelDiagnostics.push( new Diagnostic( new Range(0, 0, 0, 0), errMessage, DiagnosticSeverity.Information, ), ); } } } } } } ================================================ FILE: src/commands/tests/unmaterializedModelTest.ts ================================================ import { Diagnostic, DiagnosticSeverity, Range } from "vscode"; import { ScanContext } from "./scanContext"; import { AltimateScanStep } from "./step"; export class UnmaterializedModelTest implements AltimateScanStep { public async run(scanContext: ScanContext) { const { project, catalog: altimateCatalog, eventMap: projectEventMap, diagnostics: projectDiagnostics, } = scanContext; const projectName = project.getProjectName(); const projectRootUri = project.projectRoot; if (projectEventMap === undefined) { return; } const { nodeMetaMap } = projectEventMap; for (const value of nodeMetaMap.nodes()) { if (value.config.materialized === "ephemeral") { // ephemeral models by nature wont be materialized. // seeds should be materialized so that other features // work well so we'll raise errors for those continue; } const modelKey = JSON.stringify({ projectroot: projectRootUri.fsPath, project: projectName, database: value.database.toLowerCase(), schema: value.schema.toLowerCase(), name: value.alias.toLowerCase(), }); if ( !Object.keys(altimateCatalog[projectName + projectRootUri]).includes( modelKey, ) && value.path ) { // When the model is not in model dict, we could not find the table or view in // information schema. meaning it was not materialized. const errMessage = `Model ${value.name} does not exist in the database`; let modelDiagnostics = projectDiagnostics[value.path]; if (modelDiagnostics === undefined) { projectDiagnostics[value.path] = modelDiagnostics = []; } modelDiagnostics.push( new Diagnostic( new Range(0, 0, 0, 0), errMessage, DiagnosticSeverity.Information, ), ); } } } } ================================================ FILE: src/commands/validateSql.ts ================================================ import { DBTTerminal } from "@altimateai/dbt-integration"; import { inject } from "inversify"; import { basename } from "path"; import { PythonException } from "python-bridge"; import { CancellationToken, commands, Diagnostic, DiagnosticCollection, DiagnosticSeverity, languages, Position, ProgressLocation, Range, Uri, ViewColumn, window, workspace, } from "vscode"; import { AltimateRequest, ModelNode } from "../altimate"; import { SqlPreviewContentProvider } from "../content_provider/sqlPreviewContentProvider"; import { DBTProjectContainer } from "../dbt_client/dbtProjectContainer"; import { ManifestCacheChangedEvent, ManifestCacheProjectAddedEvent, } from "../dbt_client/event/manifestCacheChangedEvent"; import { TelemetryService } from "../telemetry"; import { extendErrorWithSupportLinks } from "../utils"; export class ValidateSql { private eventMap: Map = new Map(); private diagnosticsCollection: DiagnosticCollection; constructor( private dbtProjectContainer: DBTProjectContainer, private telemetry: TelemetryService, private altimate: AltimateRequest, @inject("DBTTerminal") private dbtTerminal: DBTTerminal, ) { dbtProjectContainer.onManifestChanged((event) => this.onManifestCacheChanged(event), ); this.diagnosticsCollection = languages.createDiagnosticCollection(); } private async onManifestCacheChanged(event: ManifestCacheChangedEvent) { event.added?.forEach((added) => { this.eventMap.set(added.project.projectRoot.fsPath, added); }); event.removed?.forEach((removed) => { this.eventMap.delete(removed.projectRoot.fsPath); }); } private showError(exc: unknown) { if (exc instanceof PythonException) { window.showErrorMessage( extendErrorWithSupportLinks( `An error occured while trying to compile your model: ` + exc.exception.message + ".", ), ); this.telemetry.sendTelemetryError( "validateSQLCompileNodePythonError", exc, ); this.dbtTerminal.error( "validateSQLError", "Error encountered while compiling/retrieving schema for model", exc, ); return; } this.telemetry.sendTelemetryError( "validateSQLCompileNodeUnknownError", exc, ); // Unknown error window.showErrorMessage( extendErrorWithSupportLinks( "Could not validate SQL: " + (exc as Error).message, ), ); } async validateSql() { this.telemetry.sendTelemetryEvent("validateSql"); if (!window.activeTextEditor) { return; } const activedoc = window.activeTextEditor; const currentFilePath = activedoc.document.uri; const project = this.dbtProjectContainer.findDBTProject(currentFilePath); if (!project) { await window.showErrorMessage("Unable to build project"); return; } const modelName = basename(currentFilePath.fsPath, ".sql"); const event = this.getEvent(); if (!event) { return; } const { graphMetaMap, nodeMetaMap } = event; const node = nodeMetaMap.lookupByBaseName(modelName); if (!node) { return; } const parentNodes = graphMetaMap.parents.get(node.unique_id)?.nodes; if (!parentNodes) { return; } const parentModels: ModelNode[] = []; let relationsWithoutColumns: string[] = []; let compiledQuery: string | undefined; let cancellationToken: CancellationToken | undefined; let abortController: AbortController | undefined; await window.withProgress( { location: ProgressLocation.Notification, title: "Validating SQL", cancellable: true, }, async (_, token) => { try { cancellationToken = token; abortController = new AbortController(); token.onCancellationRequested(() => abortController!.abort()); const fileContentBytes = await workspace.fs.readFile(currentFilePath); if (cancellationToken.isCancellationRequested) { return; } try { compiledQuery = await project.unsafeCompileQuery( fileContentBytes.toString(), modelName, ); } catch (error) { window.showErrorMessage( extendErrorWithSupportLinks( "Unable to compile query for model " + node.name + " : " + error, ), ); return; } if (cancellationToken.isCancellationRequested) { return; } const modelsToFetch = project.getNonEphemeralParents([ node.unique_id, ]); const { mappedNode, relationsWithoutColumns: _relationsWithoutColumns, } = await project.getNodesWithDBColumns( modelsToFetch, abortController!.signal, ); parentModels.push(...modelsToFetch.map((n) => mappedNode[n])); relationsWithoutColumns = _relationsWithoutColumns; } catch (exc) { this.showError(exc); } }, ); if (cancellationToken?.isCancellationRequested) { return; } if (!compiledQuery) { return; } if (relationsWithoutColumns.length !== 0) { window.showErrorMessage( extendErrorWithSupportLinks( "Failed to fetch columns for " + relationsWithoutColumns.join(", ") + ". Probably the dbt models are not yet materialized.", ), ); } const request = { sql: compiledQuery, dialect: project.getAdapterType(), models: parentModels, }; const response = await this.getProject()?.validateSql(request); const activeUri = window.activeTextEditor?.document.uri; if (activeUri.scheme === SqlPreviewContentProvider.SCHEME) { // current focus on compiled sql document return; } const compileSQLUri = activeUri.with({ scheme: SqlPreviewContentProvider.SCHEME, }); const isOpen = !!window.visibleTextEditors.find( (item) => item.document.uri === compileSQLUri, ); if (!response || !response?.error_type) { const tabGroup = window.tabGroups.all.find( (tabGroup) => (tabGroup.activeTab?.input as { uri: Uri })?.uri.toString() === compileSQLUri.toString(), ); if (tabGroup) { await window.tabGroups.close(tabGroup); } window.showInformationMessage("SQL is valid."); this.diagnosticsCollection.set(compileSQLUri, []); return; } if (response.error_type === "sql_unknown_error") { window.showErrorMessage("Unable to validate SQL."); this.telemetry.sendTelemetryError( "validateSQLError", response.errors[0].description, ); this.diagnosticsCollection.set(compileSQLUri, []); return; } if ( response.error_type === "sql_parse_error" || (response.errors.length > 0 && response.errors[0].start_position) ) { if (!isOpen) { const doc = await workspace.openTextDocument(compileSQLUri); await window.showTextDocument(doc, ViewColumn.Beside, true); await languages.setTextDocumentLanguage(doc, "sql"); } } commands.executeCommand("workbench.action.problems.focus"); const diagnostics = response?.errors?.map( ({ description, start_position, end_position }) => { let startPos = new Position(0, 1); let endPos = new Position(0, 1); if (start_position) { startPos = new Position(start_position[0], start_position[1]); } if (end_position) { endPos = new Position(end_position[0], end_position[1]); } return new Diagnostic( new Range(startPos, endPos), description, DiagnosticSeverity.Error, ); }, ); this.diagnosticsCollection.set(compileSQLUri, diagnostics); } private getProject() { const currentFilePath = window.activeTextEditor?.document.uri; if (!currentFilePath) { return; } return this.dbtProjectContainer.findDBTProject(currentFilePath); } private getEvent(): ManifestCacheProjectAddedEvent | undefined { if (window.activeTextEditor === undefined || this.eventMap === undefined) { return; } const currentFilePath = window.activeTextEditor.document.uri; const projectRootpath = this.dbtProjectContainer.getProjectRootpath(currentFilePath); if (projectRootpath === undefined) { return; } const event = this.eventMap.get(projectRootpath.fsPath); if (event === undefined) { return; } return event; } } ================================================ FILE: src/commands/walkthroughCommands.ts ================================================ import { CommandProcessExecutionFactory, DBTTerminal, } from "@altimateai/dbt-integration"; import { inject } from "inversify"; import { gte } from "semver"; import { commands, ProgressLocation, QuickPickItem, window, workspace, } from "vscode"; import { DBTProjectContainer } from "../dbt_client/dbtProjectContainer"; import { PythonEnvironment } from "../dbt_client/pythonEnvironment"; import { ProjectQuickPickItem } from "../quickpick/projectQuickPick"; import { TelemetryService } from "../telemetry"; import { getFirstWorkspacePath } from "../utils"; enum PromptAnswer { YES = "Yes", NO = "No", } enum DbtInstallationPromptAnswer { INSTALL = "Install dbt core", INSTALL_CLOUD = "Install dbt cloud", INSTALL_FUSION = "Install dbt fusion", } export class WalkthroughCommands { constructor( private dbtProjectContainer: DBTProjectContainer, private telemetry: TelemetryService, private commandProcessExecutionFactory: CommandProcessExecutionFactory, @inject(PythonEnvironment) private pythonEnvironment: PythonEnvironment, @inject("DBTTerminal") private dbtTerminal: DBTTerminal, ) {} async validateProjects( projectContext: ProjectQuickPickItem | undefined, skipConfirmation = false, ) { if (projectContext === undefined) { // This shouldnt happen really window.showErrorMessage( "No project was selected, please select a project in the step 'Pick a dbt project' above.", ); return; } let debugCommand = "dbt debug"; if ( workspace .getConfiguration("dbt") .get("dbtIntegration", "core") === "cloud" ) { debugCommand = "dbt environment show"; } if (!skipConfirmation) { const answer = await window.showInformationMessage( `Do you want to validate the project: ${projectContext.label}? This will run the command '${debugCommand}' inside this project. Do you want to continue?`, PromptAnswer.YES, PromptAnswer.NO, ); if (answer !== PromptAnswer.YES) { return; } } try { this.telemetry.sendTelemetryEvent("validateProject"); const project = this.dbtProjectContainer.findDBTProject( projectContext.uri, ); if (project === undefined) { throw new Error(`Project ${projectContext.label} was not found`); } const runModelOutput = await project.debug(); if (runModelOutput.fullOutput.includes("ERROR")) { throw new Error(runModelOutput.fullOutput); } } catch (err) { this.dbtTerminal.error( "validateProjectError", `Error when validating ${projectContext.label}`, err, ); window.showErrorMessage( "Error running dbt debug for project " + projectContext.label + ". Please check the output tab for more details.", ); throw err; } } async installDeps( projectContext: ProjectQuickPickItem | undefined, skipConfirmation = false, ) { if (projectContext === undefined) { // This shouldnt happen really window.showErrorMessage( "No project was selected, please select a project in the step 'Pick a dbt project' above.", ); return; } if (!skipConfirmation) { const answer = await window.showInformationMessage( `Do you want to install packages for the project: ${projectContext.label}? This will run the command 'dbt deps' inside this project. Do you want to continue?`, PromptAnswer.YES, PromptAnswer.NO, ); if (answer !== PromptAnswer.YES) { return; } } try { this.telemetry.sendTelemetryEvent("installDeps"); const project = this.dbtProjectContainer.findDBTProject( projectContext.uri, ); if (project === undefined) { throw new Error(`Project ${projectContext.label} was not found`); } await project.installDeps(); } catch (err) { this.dbtTerminal.debug( "WalkthroughCommands.installDeps", "Could not install deps", err, ); this.telemetry.sendTelemetryError("installDepsError", err); window.showErrorMessage( "Error installing dbt dependencies for project " + projectContext.label + ". Please check the output tab for more details.", ); throw err; } } async installDbt(): Promise { const dbtIntegration = workspace .getConfiguration("dbt") .get("dbtIntegration", "core"); switch (dbtIntegration) { case "core": return this.installDbtCore(); case "fusion": return this.installDbtFusion(); case "cloud": return this.installDbtCloud(); default: throw new Error( `Unsupported dbt integration: ${dbtIntegration}. Supported values are 'core', 'cloud', 'fusion'.`, ); } } private async installDbtFusion(): Promise { let error = undefined; await window.withProgress( { title: `Installing dbt fusion...`, location: ProgressLocation.Notification, cancellable: false, }, async () => { try { const platform = process.platform; let command: string; let args: string[]; if (platform === "darwin" || platform === "linux") { command = "sh"; args = [ "-c", "curl -fsSL https://public.cdn.getdbt.com/fs/install/install.sh | sh -s -- --update", ]; } else if (platform === "win32") { command = "powershell"; args = [ "-Command", "irm https://public.cdn.getdbt.com/fs/install/install.ps1 | iex", ]; } else { throw new Error( `Unsupported platform: ${platform}, only MacOS, Linux and Windows are supported for dbt fusion installation`, ); } await this.commandProcessExecutionFactory .createCommandProcessExecution({ command, args, cwd: getFirstWorkspacePath(), envVars: this.pythonEnvironment.getEnvironmentVariables(), }) .completeWithTerminalOutput(); // Initialize after installation await this.dbtProjectContainer.detectDBT(); this.dbtProjectContainer.initialize(); } catch (err) { error = err; } }, ); if (error) { const answer = await window.showErrorMessage( "Could not install dbt fusion: " + (error as Error).message, DbtInstallationPromptAnswer.INSTALL_FUSION, ); if (answer === DbtInstallationPromptAnswer.INSTALL_FUSION) { commands.executeCommand("dbtPowerUser.installDbt"); } } } private async installDbtCloud(): Promise { let error = undefined; await window.withProgress( { title: `Installing dbt cloud...`, location: ProgressLocation.Notification, cancellable: false, }, async () => { try { const { stdout, stderr } = await this.commandProcessExecutionFactory .createCommandProcessExecution({ command: this.pythonEnvironment.pythonPath, args: [ "-m", "pip", "install", "dbt", "--no-cache-dir", "--force-reinstall", ], cwd: getFirstWorkspacePath(), envVars: this.pythonEnvironment.getEnvironmentVariables(), }) .completeWithTerminalOutput(); if ( !stdout.includes("Successfully installed") && !stdout.includes("Requirement already satisfied") && stderr ) { throw new Error(stderr); } await this.dbtProjectContainer.detectDBT(); this.dbtProjectContainer.initialize(); } catch (err) { error = err; } }, ); if (error) { const answer = await window.showErrorMessage( "Could not install dbt cloud: " + (error as Error).message, DbtInstallationPromptAnswer.INSTALL_CLOUD, ); if (answer === DbtInstallationPromptAnswer.INSTALL_CLOUD) { commands.executeCommand("dbtPowerUser.installDbt"); } } } private async installDbtCore(): Promise { const dbtVersion: QuickPickItem | undefined = await window.showQuickPick( ["1.8", "1.9", "1.10", "1.11"].map((value) => ({ label: value, })), { title: "Select your dbt version", canPickMany: false, }, ); if (!dbtVersion) { return; } const adapter: QuickPickItem | undefined = await window.showQuickPick( [ "snowflake", "bigquery", "redshift", "postgres", "databricks", "sqlserver", "duckdb", "athena", "spark", "clickhouse", "trino", "synapse", "fabric", "risingwave", ].map((value) => ({ label: value })), { title: "Select your adapter", canPickMany: false, }, ); if (!adapter || !adapter.label) { return; } const packageVersion = dbtVersion.label; const packageName = this.mapToAdapterPackage(adapter.label); let error = undefined; await window.withProgress( { title: `Installing ${packageName} ${packageVersion}...`, location: ProgressLocation.Notification, cancellable: false, }, async () => { try { const args = [ "-m", "pip", "install", "--no-cache-dir", "--force-reinstall", ]; const isIndependentAdapterPackage = gte( packageVersion + ".0", "1.8.0", ); if (isIndependentAdapterPackage) { args.push(`dbt-core~=${packageVersion}.0`); args.push(`${packageName}`); } else { args.push(`${packageName}==${packageVersion}`); } const { stdout, stderr } = await this.commandProcessExecutionFactory .createCommandProcessExecution({ command: this.pythonEnvironment.pythonPath, args, cwd: getFirstWorkspacePath(), envVars: this.pythonEnvironment.getEnvironmentVariables(), }) .completeWithTerminalOutput(); if ( !stdout.includes("Successfully installed") && !stdout.includes("Requirement already satisfied") && stderr ) { throw new Error(stderr); } await this.dbtProjectContainer.detectDBT(); this.dbtProjectContainer.initialize(); } catch (err) { error = err; } }, ); if (error) { const answer = await window.showErrorMessage( "Could not install dbt: " + (error as Error).message, DbtInstallationPromptAnswer.INSTALL, ); if (answer === DbtInstallationPromptAnswer.INSTALL) { commands.executeCommand("dbtPowerUser.installDbt"); } } } private mapToAdapterPackage(adapter: string): string { switch (adapter) { case "snowflake": return "dbt-snowflake"; case "bigquery": return "dbt-bigquery"; case "redshift": return "dbt-redshift"; case "postgres": return "dbt-postgres"; case "databricks": return "dbt-databricks"; case "sqlserver": return "dbt-sqlserver"; case "duckdb": return "dbt-duckdb"; case "athena": return "dbt-athena-community"; case "spark": return "dbt-spark"; case "clickhouse": return "dbt-clickhouse"; case "trino": return "dbt-trino"; case "synapse": return "dbt-synapse"; case "fabric": return "dbt-fabric"; case "risingwave": return "dbt-risingwave"; } throw new Error("Adapter is not supported" + adapter); } } ================================================ FILE: src/comment_provider/conversationProvider.ts ================================================ import { DBTTerminal, RESOURCE_TYPE_MACRO, RESOURCE_TYPE_TEST, } from "@altimateai/dbt-integration"; import { inject } from "inversify"; import { CancellationToken, commands, Comment, CommentAuthorInformation, CommentMode, CommentReply, comments, CommentThread, CommentThreadState, Disposable, env, MarkdownString, Range, TextDocument, Uri, window, workspace, } from "vscode"; import { Conversation, ConversationGroup, SharedDoc } from "../altimate"; import { ConversationService } from "../services/conversationService"; import { QueryManifestService } from "../services/queryManifestService"; import { SharedStateService } from "../services/sharedStateService"; import { UsersService } from "../services/usersService"; import { TelemetryService } from "../telemetry"; import { extendErrorWithSupportLinks } from "../utils"; import path = require("path"); // Extends vscode commentthread and add extra fields for reference export interface ConversationCommentThread extends CommentThread { share_id: SharedDoc["share_id"]; conversation_group_id: ConversationGroup["conversation_group_id"]; meta: ConversationGroup["meta"]; } export class ConversationComment implements Comment { label: string | undefined; timestamp: Date; savedBody: string | MarkdownString; // for the Cancel button constructor( public conversation_id: Conversation["conversation_id"], public body: string | MarkdownString, public mode: CommentMode, public author: CommentAuthorInformation, public time: string, public parent?: ConversationCommentThread, public contextValue?: string, ) { this.savedBody = this.body; this.timestamp = new Date(this.time); } } const ALLOWED_FILE_EXTENSIONS = [".sql"]; export class ConversationProvider implements Disposable { private disposables: Disposable[] = []; private commentController; private timer: NodeJS.Timeout | undefined; private isPolling: boolean = false; // record of share id with conv group // used to identify deleted records during polling // can be removed in future if we get right events like delete, add etc., private _threads: Record> = {}; constructor( private conversationService: ConversationService, private usersService: UsersService, @inject("DBTTerminal") private dbtTerminal: DBTTerminal, private emitterService: SharedStateService, private queryManifestService: QueryManifestService, private telemetry: TelemetryService, ) { if (!this.isCollaborationEnabled()) { return; } // Create comment controller this.commentController = comments.createCommentController( "altimate-conversations", // this title will be used to show in selection options when more than one extension in vscode has commenting feature "Altimate dbt conversations", ); this.commentController.commentingRangeProvider = { provideCommentingRanges: ( document: TextDocument, token: CancellationToken, ) => { // enable only for allowed files if ( !ALLOWED_FILE_EXTENSIONS.find((ext) => document.uri.fsPath.endsWith(ext), ) ) { return null; } const lineCount = document.lineCount; return [new Range(0, 0, lineCount - 1, 0)]; }, }; this.disposables.push( emitterService.eventEmitter.event((d) => { if ( d.command === "dbtProjectsInitialized" || d.command === "refetchConversations" ) { this.loadThreads(); } }), ); this.disposables.push(this.commentController); } private isCollaborationEnabled() { const enableCollaboration = workspace .getConfiguration("dbt") .get("enableCollaboration", false); this.dbtTerminal.debug( "ConversationProvider:isCollaborationEnabled", "collaboration status", enableCollaboration, ); return enableCollaboration; } // simple polling for getting latest conversations private setupPolling() { clearTimeout(this.timer); const pollingInterval = workspace .getConfiguration("dbt") .get("conversationsPollingInterval", 30); this.dbtTerminal.debug( "ConversationProvider:setupRefetch", "refresh conversations after", pollingInterval, ); this.timer = setTimeout(() => { this.loadThreads(); }, pollingInterval * 1000); this.isPolling = true; } private async loadThreads() { this.dbtTerminal.debug( "ConversationProvider:loadThreads", "loading threads", ); const shares = await this.conversationService.loadSharedDocs(); if (shares && shares.length && !this.isPolling) { this.setupPolling(); } if (!shares?.length) { this.dbtTerminal.debug( "ConversationProvider:loadThreads", "No conversations yet", ); return; } shares.map(async (dbtDocsShare) => { this.dbtTerminal.debug( "ConversationProvider:loadThreads", "loading conversations", dbtDocsShare.share_id, ); const conversations = dbtDocsShare.conversation_group; if (!conversations?.length) { this.dbtTerminal.debug( "ConversationProvider:loadThreads", "No conversations in share", dbtDocsShare.share_id, ); return; } const pendingConversations = conversations.filter( (c) => c.status === "Pending", ); this.emitterService.fire({ command: "conversations:updates", payload: { shareId: dbtDocsShare.share_id, conversationGroups: pendingConversations, }, }); // If local cache has this share already, find if any conversation is missing from db (which as resolved) // can be removed in future if we get right events like delete, add etc., if (this._threads[dbtDocsShare.share_id]) { const currentConversationGroupIds = Object.keys( this._threads[dbtDocsShare.share_id], ); const conversationGroupIdsFromDB = pendingConversations.map((c) => c.conversation_group_id.toString(), ); const missingConversationGroupIds = currentConversationGroupIds.filter( (id) => !conversationGroupIdsFromDB.includes(id), ); if (missingConversationGroupIds.length) { this.dbtTerminal.debug( "ConversationProvider:loadThreads", "resolving threads", missingConversationGroupIds, ); // delete resolved conversations missingConversationGroupIds.forEach((id) => this._threads[dbtDocsShare.share_id][id].dispose(), ); } } const project = this.queryManifestService.getProjectByName( dbtDocsShare.project_name, ); if (!project) { this.dbtTerminal.debug( "ConversationProvider:loadThreads", "not able to get project", dbtDocsShare, ); return; } pendingConversations.map((conversationGroup) => { const uri = Uri.file( path.join( project.projectRoot.fsPath, conversationGroup.meta.filePath, ), ); const thread = this._threads[dbtDocsShare.share_id]?.[ conversationGroup.conversation_group_id ] ?? (this.commentController!.createCommentThread( uri, new Range( conversationGroup.meta.range?.start.line || 0, conversationGroup.meta.range?.start.character || 0, conversationGroup.meta.range?.end.line || 0, conversationGroup.meta.range?.end.character || 0, ), [], ) as ConversationCommentThread); // whenever new comment thread is added, active editor loses focus if (window.activeTextEditor?.document.uri.fsPath === uri.fsPath) { window.showTextDocument(window.activeTextEditor.document); } thread.state = CommentThreadState.Unresolved; thread.comments = conversationGroup.conversations.map( (conversation) => new ConversationComment( conversation.conversation_id, this.convertTextFromDbToCommentFormat(conversation.message), CommentMode.Preview, { name: this.usersService.getUserById(conversation.user_id) ?.first_name || "Unknown", }, conversation.timestamp, undefined, "", ), ); const isInDocsEditor = !!conversationGroup.meta.field; if (isInDocsEditor) { thread.comments = [ new ConversationComment( -1, [ "This comment is added from documentation editor.", 'Please click "View in documentation editor" button to view in documentation editor.\n', conversationGroup.meta.column ? `Column: ${conversationGroup.meta.column}\n` : "", "Description:", conversationGroup.meta.highlight, ].join(" "), CommentMode.Preview, { name: "Altimate", }, new Date().toISOString(), undefined, "", ), ...thread.comments, ]; } thread.conversation_group_id = conversationGroup.conversation_group_id; thread.meta = conversationGroup.meta; thread.share_id = dbtDocsShare.share_id; thread.label = "Discussion"; this.addContextValue(thread); // update the local cache this._threads[dbtDocsShare.share_id] = { ...this._threads[dbtDocsShare.share_id], [conversationGroup.conversation_group_id]: thread, }; }); }); } private addContextValue(thread: ConversationCommentThread) { let contextValue = "saved"; if (thread.meta.field === "description") { contextValue += "|description"; } thread.contextValue = contextValue; } // convert "@[john](john)" to "@john" private convertTextFromDbToCommentFormat(text: string) { return new MarkdownString(text.replace(/@\[(.*?)\]\((.*?)\)/g, "@$2")); } // convert "@john" to "@[john](john)" private convertTextToDbFormat(text: string) { return new MarkdownString(text).value.replace(/@(\S+)\s/g, "@[$1]($1) "); } private addComment(reply: CommentReply) { const thread = reply.thread; thread.contextValue = "saving"; const newComment = new ConversationComment( -1, new MarkdownString(reply.text), CommentMode.Preview, { name: this.usersService.user?.first_name || "Unknown" }, new Date().toISOString(), thread as ConversationCommentThread, "", ); thread.comments = [...thread.comments, newComment]; return newComment; } async copyThreadLink(thread: ConversationCommentThread) { this.telemetry.sendTelemetryEvent("dbtCollaboration:copyLink", { source: "vscode", }); if (!thread.share_id) { window.showErrorMessage( extendErrorWithSupportLinks("Unable to find conversation."), ); return; } const result = await this.conversationService.getAppUrlByShareId( thread.share_id, ); if (result?.app_url) { env.clipboard.writeText( `${result.app_url}/${thread.conversation_group_id}`, ); window.showInformationMessage("Url copied", "Ok"); } } async viewInDocEditor(thread: ConversationCommentThread) { this.telemetry.sendTelemetryEvent("dbtCollaboration:viewInDocEditor", { source: "vscode", }); this.dbtTerminal.debug( "ConversationProvider:viewInDocEditor", "viewing conversation", thread.share_id, thread.conversation_group_id, ); commands.executeCommand("dbtPowerUser.DocsEdit.focus"); this.emitterService.fire({ command: "viewConversation", payload: { shareId: thread.share_id, conversation_group_id: thread.conversation_group_id, }, }); // When clicking button in vscode comment, active text editor changes to comment // refocussing the model to make sure documentation shows up const editor = window.visibleTextEditors.find( (editor) => editor.document.uri.fsPath === thread.uri.fsPath, ); if (editor?.document) { window.showTextDocument(editor.document); } } async viewInDbtDocs(thread: ConversationCommentThread) { this.telemetry.sendTelemetryEvent("dbtCollaboration:viewInDbtDocs", { source: "vscode", }); if (!thread.share_id) { window.showErrorMessage( extendErrorWithSupportLinks("Unable to find conversation."), ); return; } this.dbtTerminal.debug( "ConversationProvider:viewInDbtDocs", `firing render dbtdocs event`, thread.share_id, ); this.emitterService.fire({ command: "dbtdocsview:render", payload: { shareId: thread.share_id, conversationGroupId: thread.conversation_group_id, userId: this.usersService.user?.id, }, }); } private getNodeMeta(uri: Uri, resourceName: string) { const event = this.queryManifestService.getEventByDocument(uri); if (!event) { this.dbtTerminal.debug("getNodeMeta", "event not available"); return; } const currentNode = event.nodeMetaMap.lookupByBaseName(resourceName); // For model if (currentNode) { return { resource_type: currentNode.resource_type, uniqueId: currentNode.unique_id, }; } const macroNode = event.macroMetaMap.get(resourceName); // For macro if (macroNode) { return { resource_type: RESOURCE_TYPE_MACRO, uniqueId: macroNode.unique_id, }; } const testNode = event.testMetaMap.get(resourceName); // For tests if (testNode) { return { resource_type: RESOURCE_TYPE_TEST, uniqueId: testNode.unique_id, }; } } createCommentThread(uri: Uri, range: Range) { return this.commentController?.createCommentThread(uri, range, []); } async saveConversation( message: string, uri: Uri, extraMeta: Record = {}, range: Range | undefined, source: "vscode" | "documentation-editor" = "vscode", ) { this.telemetry.sendTelemetryEvent("dbtCollaboration:create", { source, }); const model = path.basename(uri.fsPath, ".sql"); const convertedMessage = this.convertTextToDbFormat(message); const nodeMeta = this.getNodeMeta(uri, model); // Find selected text const editor = window.visibleTextEditors.find( (editor) => editor.document.uri.fsPath === uri.fsPath, ); const project = this.queryManifestService.getProjectByUri(uri); const { value, ...rest } = extraMeta; // update highlighted text as desc if conversation is created from desc field in doc editor const highlight = rest.field === "description" ? (value as string) : (range?.isSingleLine ? editor?.document.lineAt(range?.start?.line ?? 0).text : editor?.document.getText(range)) || ""; const meta = { ...rest, highlight, source: "extension", uniqueId: nodeMeta?.uniqueId, filePath: path.relative(project?.projectRoot.fsPath || "", uri.fsPath), resource_type: nodeMeta?.resource_type, range: range ? { end: range.end, start: range.start, } : undefined, }; let shareName = "Discussion on "; if (nodeMeta?.uniqueId) { shareName += nodeMeta.uniqueId; } else if (model) { shareName += model; } else { shareName = "Untitled Discussion"; } // create share const result = await this.conversationService.shareDbtDocs({ name: shareName, // `dbt docs discussion on ${nodeMeta?.uniqueId}`, description: "", uri, model, }); // Failing silently, because this case will happen if key is not added // message for adding key will be already shown if (!result) { return; } const { shareId, shareUrl } = result; this.dbtTerminal.debug( "ConversationProvider:createConversation", "created conversation, adding conversation to group", shareId, shareUrl, ); // create conversation group const addReplyResult = await this.conversationService.createConversationGroup(shareId, { message: convertedMessage, meta, }); if (!addReplyResult) { throw new Error("Unable to create group"); } this.dbtTerminal.debug( "ConversationProvider", "added conversation to created conversation group", addReplyResult, ); return { conversation_id: addReplyResult.conversation_id, shareId, conversation_group_id: addReplyResult.conversation_group_id, meta, }; } async createConversation( reply: CommentReply, extraMeta: Record = {}, source: "vscode" | "documentation-editor" = "vscode", ) { try { this.dbtTerminal.debug( "ConversationProvider:createConversation", "creating conversation", reply, ); const thread = reply.thread as ConversationCommentThread; thread.state = CommentThreadState.Unresolved; this.addComment(reply); thread.label = "Pending"; const result = await this.saveConversation( reply.text, thread.uri, extraMeta, thread.range, source, ); if (!result) { return; } const { conversation_id, shareId, conversation_group_id, meta } = result; (thread.comments[0] as ConversationComment).conversation_id = conversation_id; thread.share_id = shareId; thread.conversation_group_id = conversation_group_id; thread.meta = meta; this._threads[shareId] = { ...this._threads[shareId], [thread.conversation_group_id]: thread, }; thread.label = "Discussion"; this.addContextValue(thread); } catch (error) { this.dbtTerminal.error( "ConversationProvider:createConversation", "unable to create conversation", error, ); // If share cannot be created, delete the thread reply.thread.dispose(); window.showErrorMessage( extendErrorWithSupportLinks( `Unable to save your comment. ${(error as Error).message}`, ), ); } finally { if (!this.isPolling) { this.setupPolling(); } } } async replyToConversation(reply: CommentReply) { this.telemetry.sendTelemetryEvent("dbtCollaboration:reply", { source: "vscode", }); const thread = reply.thread as ConversationCommentThread; try { if (!thread.share_id) { throw new Error("Unable to find conversation. Missing share id"); } this.dbtTerminal.debug( "ConversationProvider:replyToConversation", "replying to conversation", reply, ); this.addComment(reply); if (!thread.conversation_group_id) { if (!thread.share_id) { throw new Error( "Unable to find conversation. Missing conversation group id", ); } return; } await this.conversationService.addConversationToGroup( thread.share_id, thread.conversation_group_id, this.convertTextToDbFormat(reply.text), ); } catch (error) { this.dbtTerminal.error( "ConversationProvider:replyToConversation", "unable to reply conversation", error, ); window.showErrorMessage( extendErrorWithSupportLinks( `Unable to save your reply. ${(error as Error).message}`, ), ); } this.addContextValue(thread); } async resolveConversation(commentThread: ConversationCommentThread) { try { this.telemetry.sendTelemetryEvent("dbtCollaboration:resolve", { source: "vscode", }); if (!commentThread.share_id) { throw new Error("Unable to find conversation. Missing share id"); } this.dbtTerminal.debug( "ConversationProvider:resolveConversation", `resolving conversation: ${commentThread.conversation_group_id} in share: ${commentThread.share_id}`, ); const result = await this.conversationService.resolveConversation( commentThread.share_id, commentThread.conversation_group_id, ); this.dbtTerminal.debug( "ConversationProvider:resolveConversation", `resolved conversation: ${commentThread.conversation_group_id} in share: ${commentThread.share_id}`, result, ); commentThread.dispose(); } catch (error) { this.dbtTerminal.error( "ConversationProvider:resolveConversation", "unable to resolve conversation", error, ); window.showErrorMessage( extendErrorWithSupportLinks( `Unable to resolve comment. ${(error as Error).message}`, ), ); } } dispose() { if (this.timer) { clearTimeout(this.timer); this.timer = undefined; } while (this.disposables.length) { const x = this.disposables.pop(); if (x) { x.dispose(); } } } } ================================================ FILE: src/comment_provider/index.ts ================================================ import { Disposable } from "vscode"; import { ConversationProvider } from "./conversationProvider"; export class CommentProviders implements Disposable { private disposables: Disposable[] = []; constructor(private conversationProvider: ConversationProvider) { this.disposables.push(this.conversationProvider); } dispose() { while (this.disposables.length) { const x = this.disposables.pop(); if (x) { x.dispose(); } } } } ================================================ FILE: src/content_provider/index.ts ================================================ import { Disposable, workspace } from "vscode"; import { SqlPreviewContentProvider } from "./sqlPreviewContentProvider"; export class ContentProviders implements Disposable { private disposables: Disposable[] = []; constructor(private sqlPreviewContentProvider: SqlPreviewContentProvider) { this.disposables.push( workspace.registerTextDocumentContentProvider( SqlPreviewContentProvider.SCHEME, this.sqlPreviewContentProvider, ), ); } dispose() { while (this.disposables.length) { const x = this.disposables.pop(); if (x) { x.dispose(); } } } } ================================================ FILE: src/content_provider/sqlPreviewContentProvider.ts ================================================ import { readFileSync } from "fs"; import { Disposable, Event, EventEmitter, ProgressLocation, TextDocumentChangeEvent, TextDocumentContentProvider, Uri, window, workspace, } from "vscode"; import { DBTProjectContainer } from "../dbt_client/dbtProjectContainer"; import { TelemetryService } from "../telemetry"; import path = require("path"); export class SqlPreviewContentProvider implements TextDocumentContentProvider, Disposable { static readonly SCHEME = "query-preview"; private _onDidChange = new EventEmitter(); private compilationDocs = new Map(); private subscriptions: Disposable[] = []; private debounceTimers: Map = new Map(); constructor( private dbtProjectContainer: DBTProjectContainer, private telemetry: TelemetryService, ) { // Register a single global listener for all document changes this.subscriptions.push( workspace.onDidChangeTextDocument((e: TextDocumentChangeEvent) => { // Check if this document has an associated preview const fileUriString = e.document.uri.toString(); for (const [ previewUriString, previewUri, ] of this.compilationDocs.entries()) { const actualFileUri = previewUri.with({ scheme: "file" }); if (actualFileUri.toString() === fileUriString) { // Debounce the update const existingTimer = this.debounceTimers.get(previewUriString); if (existingTimer) { clearTimeout(existingTimer); } const timer = setTimeout(() => { this._onDidChange.fire(previewUri); this.debounceTimers.delete(previewUriString); }, 500); this.debounceTimers.set(previewUriString, timer); break; } } }), ); // Clean up when editors are closed, not when text documents are closed // This prevents premature cleanup during document lifecycle events this.subscriptions.push( window.onDidChangeVisibleTextEditors(() => { // Get all visible preview document URIs const visiblePreviewUris = new Set( window.visibleTextEditors .filter( (editor) => editor.document.uri.scheme === SqlPreviewContentProvider.SCHEME, ) .map((editor) => editor.document.uri.toString()), ); // Remove documents that are no longer visible for (const [uriString] of this.compilationDocs.entries()) { if (!visiblePreviewUris.has(uriString)) { this.compilationDocs.delete(uriString); const timer = this.debounceTimers.get(uriString); if (timer) { clearTimeout(timer); this.debounceTimers.delete(uriString); } } } }), ); } dispose(): void { this._onDidChange.dispose(); for (const subscription of this.subscriptions) { subscription.dispose(); } for (const timer of this.debounceTimers.values()) { clearTimeout(timer); } this.debounceTimers.clear(); } get onDidChange(): Event { return this._onDidChange.event; } provideTextDocumentContent(uri: Uri): string | Thenable { const uriString = uri.toString(); // Track this preview document for change detection this.compilationDocs.set(uriString, uri); return window.withProgress( { location: ProgressLocation.Notification, title: "Compiling dbt model...", cancellable: false, }, async () => await this.requestCompilation(uri), ); } private async requestCompilation(uri: Uri) { try { const fsPath = decodeURI(uri.fsPath); const modelName = path.basename(fsPath, ".sql"); // Read from the active document if available, otherwise fall back to file const actualFileUri = uri.with({ scheme: "file" }); const document = workspace.textDocuments.find( (doc) => doc.uri.toString() === actualFileUri.toString(), ); const query = document ? document.getText() : readFileSync(fsPath, "utf8"); const project = this.dbtProjectContainer.findDBTProject(Uri.file(fsPath)); if (project === undefined) { this.telemetry.sendTelemetryError("sqlPreviewNotLoadingError"); return "Still loading dbt project, please try again later..."; } this.telemetry.sendTelemetryEvent("requestCompilation"); await project.refreshProjectConfig(); return await project.unsafeCompileQuery(query, modelName); } catch (error: any) { const errorMessage = (error as Error).message; window.showErrorMessage(`Error while compiling: ${errorMessage}`); return errorMessage; } } } ================================================ FILE: src/cte_profiler/cteProfilerDecorationProvider.ts ================================================ import { DBTTerminal } from "@altimateai/dbt-integration"; import { inject } from "inversify"; import { DecorationOptions, Disposable, OverviewRulerLane, Range, TextEditor, TextEditorDecorationType, ThemeColor, Uri, window, } from "vscode"; import { CteProfilerService } from "./cteProfilerService"; import { CteProfileEntry } from "./cteProfilerTypes"; export class CteProfilerDecorationProvider implements Disposable { private disposables: Disposable[] = []; private visible = true; private readonly hotDecorationType: TextEditorDecorationType; private readonly warmDecorationType: TextEditorDecorationType; private readonly coolDecorationType: TextEditorDecorationType; constructor( private cteProfilerService: CteProfilerService, @inject("DBTTerminal") private dbtTerminal: DBTTerminal, ) { this.hotDecorationType = window.createTextEditorDecorationType({ gutterIconPath: Uri.file( __dirname + "/../../media/images/profiler-hot.svg", ), gutterIconSize: "contain", after: { margin: "0 0 0 8px", textDecoration: "none", }, overviewRulerColor: new ThemeColor("editorError.foreground"), overviewRulerLane: OverviewRulerLane.Right, }); this.warmDecorationType = window.createTextEditorDecorationType({ gutterIconPath: Uri.file( __dirname + "/../../media/images/profiler-warm.svg", ), gutterIconSize: "contain", after: { margin: "0 0 0 8px", textDecoration: "none", }, overviewRulerColor: new ThemeColor("editorWarning.foreground"), overviewRulerLane: OverviewRulerLane.Right, }); this.coolDecorationType = window.createTextEditorDecorationType({ after: { margin: "0 0 0 8px", textDecoration: "none", }, }); this.disposables.push( this.cteProfilerService.onResultChanged(() => { window.visibleTextEditors.forEach((editor) => { this.updateDecorations(editor); }); }), window.onDidChangeActiveTextEditor((editor) => { if (editor) { this.updateDecorations(editor); } }), ); } dispose() { this.hotDecorationType.dispose(); this.warmDecorationType.dispose(); this.coolDecorationType.dispose(); while (this.disposables.length) { const x = this.disposables.pop(); if (x) { x.dispose(); } } } toggle(): void { this.visible = !this.visible; window.visibleTextEditors.forEach((editor) => { this.updateDecorations(editor); }); } private updateDecorations(editor: TextEditor): void { const result = this.cteProfilerService.getResult( editor.document.uri.toString(), ); if (!result || !this.visible) { editor.setDecorations(this.hotDecorationType, []); editor.setDecorations(this.warmDecorationType, []); editor.setDecorations(this.coolDecorationType, []); return; } const hot: DecorationOptions[] = []; const warm: DecorationOptions[] = []; const cool: DecorationOptions[] = []; for (const cte of result.ctes) { const line = cte.line; // Line numbers are captured at profile time; the document may have been // edited since, so skip entries that no longer point at a valid line. if (line < 0 || line >= editor.document.lineCount) { continue; } const lineLength = editor.document.lineAt(line).text.length; const range = new Range(line, lineLength, line, lineLength); const decoration: DecorationOptions = { range, renderOptions: { after: { contentText: formatProfileText(cte.marginalTimeMs, cte.rowCount), color: new ThemeColor(tierToThemeColor(cte.tier)), }, }, }; if (cte.tier === "hot") { hot.push(decoration); } else if (cte.tier === "warm") { warm.push(decoration); } else { cool.push(decoration); } } // Add EOF summary on the last content line of the document. // // Status determines what (if anything) we render: // • complete → "Total: