Skip to content

feat: documentation lineage propagation #1498

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 43 commits into from
Nov 22, 2024

Conversation

AdiGajbhiye
Copy link
Contributor

@AdiGajbhiye AdiGajbhiye commented Nov 7, 2024

Overview

https://www.loom.com/share/b289e2b70e674eb39ff34341520d9ee5

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

Important

Introduces documentation lineage propagation with DbtLineageService and UI updates for column-level lineage handling.

  • Behavior:
    • Adds event_type to DBTColumnLineageRequest in altimate.ts.
    • Introduces DbtLineageService to handle column-level lineage and documentation propagation.
    • Implements DocumentationPropagationButton in DocGeneratorInput.tsx for UI interaction.
  • Services:
    • New DbtLineageService class for managing lineage operations, including getConnectedColumns() and handleColumnLineage().
    • Updates DbtTestService to support fetching tests for models and columns.
  • UI Components:
    • Adds DocumentationPropagationButton in DocGeneratorInput.tsx for propagating documentation.
    • Introduces new styles in styles.module.scss for UI consistency.
  • Misc:
    • Adds PropagateIcon to icons/index.tsx for UI representation.
    • Refactors NewLineagePanel and DocsEditViewPanel to integrate with DbtLineageService.

This description was created by Ellipsis for cc9f90a. It will automatically update as commits are pushed.


Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced DbtLineageService for managing and retrieving column-level lineage information.
    • Added DocumentationPropagationButton for propagating documentation across database columns.
    • New methods in DbtTestService for retrieving tests based on the current model context.
    • Enhanced DocsEditViewPanel with new commands for managing column lineage.
    • Added a new property event_type to improve column lineage request handling.
    • New icon component PropagateIcon for improved visual representation.
  • Improvements

    • Updated error handling across various components for better user feedback.
    • Streamlined message handling in DocsEditViewPanel.
    • Enhanced progress tracking and cancellation management in DbtLineageService.
    • Improved the logic for managing connected tables and columns in DbtLineageService.
  • Styles

    • Added new styles for documentation editor components to improve UI presentation.
    • Introduced new CSS classes for better layout and design in documentation propagation.

Copy link
Contributor

coderabbitai bot commented Nov 7, 2024

Walkthrough

This pull request introduces several significant changes across multiple files, primarily focusing on enhancing data lineage functionality. Key updates include the addition of a new event_type property in the DBTColumnLineageRequest interface and the introduction of the DbtLineageService class, which centralizes lineage management tasks. Other modifications include new methods in the DbtTestService and updates to the DocsEditViewPanel and NewLineagePanel classes to integrate lineage services. Additionally, new components and styles for documentation propagation are added, enhancing the overall structure and user interaction capabilities.

Changes

File Change Summary
src/altimate.ts Added event_type: string; to DBTColumnLineageRequest interface.
src/services/dbtLineageService.ts Added DbtLineageService class, CllEvents enum, and multiple methods for managing column lineage.
src/services/dbtTestService.ts Added methods getTestsForCurrentModel and getTestsForModel to retrieve test configurations.
src/webview_provider/docsEditPanel.ts Updated constructor to include DbtLineageService, added new methods for handling lineage requests and improved error handling.
src/webview_provider/newLineagePanel.ts Refactored to utilize DbtLineageService, removed cancellation token logic, and streamlined command handling.
webview_panels/src/assets/icons/index.tsx Added export for PropagateIcon.
webview_panels/src/modules/documentationEditor/components/docGenerator/DocGeneratorInput.tsx Imported DocumentationPropagationButton and modified onChange function for action dispatching.
webview_panels/src/modules/documentationEditor/components/documentationPropagation/DocumentationPropagation.tsx Introduced DocumentationPropagationButton component with state management for documentation propagation.
webview_panels/src/modules/documentationEditor/components/documentationPropagation/styles.module.scss Added new CSS classes .itemCard and .itemRow for styling.

Possibly related PRs

Suggested reviewers

  • mdesmet

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

sweep-ai bot commented Nov 7, 2024

Hey @AdiGajbhiye, here is an example of how you can ask me to improve this pull request:

@Sweep Add unit tests for the `LineageService.getConnectedTables()` method to verify:
1. Correct table retrieval for different node types (source, model, metric, exposure)
2. Handling of edge cases like empty or undefined graph meta maps
3. Sorting of returned tables

📖 For more information on how to use Sweep, please read our documentation.

@@ -535,7 +543,7 @@ export class DocsEditViewPanel implements WebviewViewProvider {
return undefined;
}

const { command, syncRequestId, args } = message;
const { command, syncRequestId, ...params } = message;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might break other commands handled here. Make sure all the commands in this handler are tested from webview panel

}
}

private sendResponseToWebview({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is also available in altimateWebview provider

@@ -168,7 +124,7 @@ export class NewLineagePanel
}

if (command === "downstreamTables") {
const body = await this.getDownstreamTables(params);
const body = this.dbtLineageService.getDownstreamTables(params);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no await needed?

Copy link
Contributor

@ellipsis-dev ellipsis-dev bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Changes requested. Incremental review on 84cb9ef in 51 seconds

More details
  • Looked at 498 lines of code in 1 files
  • Skipped 0 files when reviewing.
  • Skipped posting 1 drafted comments based on config settings.
1. src/webview_provider/docsEditPanel.ts:744
  • Draft comment:
    The saveDocumentation function is duplicated. Consider removing the duplicate to avoid redundancy and potential maintenance issues.
  • Reason this comment was not posted:
    Decided after close inspection that this draft comment was likely wrong and/or not actionable:
    The comment seems to be incorrect as there is no evidence of duplication in the provided code. The function was refactored into a separate method, which is a common practice to improve code organization and readability.
    I might be missing some context from other parts of the codebase that are not included in the diff. However, based on the provided information, there is no duplication.
    The task is to review the diff provided, and based on that, there is no duplication of the saveDocumentation function.
    The comment about the saveDocumentation function being duplicated is incorrect based on the provided diff. It should be removed.

Workflow ID: wflow_E5k4DkzGROblD48L


Want Ellipsis to fix these issues? Tag @ellipsis-dev in a comment. You can customize Ellipsis with 👍 / 👎 feedback, review rules, user-specific overrides, quiet mode, and more.

@@ -913,6 +741,288 @@ export class DocsEditViewPanel implements WebviewViewProvider {
);
}

private async saveDocumentation(message: any, syncRequestId: string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please provide a return type for the saveDocumentation function to improve code readability and maintainability.

Suggested change
private async saveDocumentation(message: any, syncRequestId: string) {
private async saveDocumentation(message: any, syncRequestId: string): Promise<void> {

}
}

private async handleSyncRequestFromWebview(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please provide a return type for the handleSyncRequestFromWebview function to improve code readability and maintainability.

Suggested change
private async handleSyncRequestFromWebview(
private async handleSyncRequestFromWebview(syncRequestId: string | undefined, callback: () => any, command: string, showErrorNotification?: boolean): Promise<void> {

}
// check if file exists, if not create an empty file
if (!existsSync(patchPath)) {
writeFileSync(patchPath, "");

Check failure

Code scanning / CodeQL

Potential file system race condition High

The file may have changed since it
was checked
.
}
// Force reload from manifest after manifest refresh
this.loadedFromManifest = false;
writeFileSync(patchPath, stringify(parsedDocFile, { lineWidth: 0 }));

Check failure

Code scanning / CodeQL

Potential file system race condition High

The file may have changed since it
was checked
.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between bf4570b and 84cb9ef.

📒 Files selected for processing (1)
  • src/webview_provider/docsEditPanel.ts (7 hunks)
🧰 Additional context used
🪛 GitHub Check: CodeQL
src/webview_provider/docsEditPanel.ts

[failure] 792-792: Potential file system race condition
The file may have changed since it was checked.


[failure] 924-924: Potential file system race condition
The file may have changed since it was checked.

🪛 Biome
src/webview_provider/docsEditPanel.ts

[error] 761-764: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Unsafe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 771-773: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Unsafe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🔇 Additional comments (3)
src/webview_provider/docsEditPanel.ts (3)

23-23: LGTM: New imports and constructor changes

The new imports and constructor dependency injection are well-structured and align with the new lineage functionality being added.

Also applies to: 35-35, 53-58, 121-121


660-668: LGTM: Column lineage base handler

The handler is well-implemented with proper error handling through the callback.


972-1024: LGTM: Well-structured utility methods

The new utility methods handleSyncRequestFromWebview and sendResponseToWebview are well-implemented with:

  • Proper error handling
  • Type safety
  • Consistent response format

Copy link
Contributor

@ellipsis-dev ellipsis-dev bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Looks good to me! Incremental review on 42eb8bc in 49 seconds

More details
  • Looked at 75 lines of code in 2 files
  • Skipped 0 files when reviewing.
  • Skipped posting 5 drafted comments based on config settings.
1. webview_panels/src/modules/documentationEditor/components/documentationPropagation/DocumentationPropagation.tsx:11
  • Draft comment:
    Remove unused import panelLogger to clean up the code.
  • Reason this comment was not posted:
    Confidence changes required: 10%
    The code in DocumentationPropagation.tsx has a redundant import statement for panelLogger which is not used anywhere in the file. Removing unused imports is a good practice to keep the code clean and efficient.
2. src/webview_provider/docsEditPanel.ts:741
  • Draft comment:
    Consider adding error handling within window.withProgress to manage any errors during the bulk save operation.
  • Reason this comment was not posted:
    Confidence changes required: 50%
    The saveDocumentationBulk function in docsEditPanel.ts uses window.withProgress to show progress while saving documentation. However, it does not handle any errors that might occur during the save operation. Adding error handling would improve robustness.
3. webview_panels/src/modules/documentationEditor/components/documentationPropagation/DocumentationPropagation.tsx:157
  • Draft comment:
    Consider using ?? instead of || for nullish coalescing to handle null or undefined values more accurately.
  • Reason this comment was not posted:
    Confidence changes required: 30%
    The patchPath assignment in DocumentationPropagation.tsx uses a fallback mechanism with ||. In TypeScript, using ?? (nullish coalescing) is preferred for handling null or undefined values, as it avoids false positives with falsy values like 0 or ''.
4. src/webview_provider/docsEditPanel.ts:734
  • Draft comment:
    Consider using early returns in the saveDocumentationBulk function to reduce nesting and improve readability.
  • Reason this comment was not posted:
    Decided after close inspection that this draft comment was likely wrong and/or not actionable:
    The 'saveDocumentationBulk' function is a new addition, and the comment is about improving readability by using early returns. The function currently uses a loop to iterate over 'message.models' and calls 'saveDocumentation' for each item. There is no obvious nesting that could be reduced by early returns, as the function is straightforward.
    I might be missing some subtlety in how early returns could be applied here, but the function seems simple enough that early returns wouldn't significantly improve readability.
    Given the simplicity of the function, early returns might not provide a noticeable improvement in readability. The function's current structure is clear and direct.
    The comment about using early returns in the 'saveDocumentationBulk' function is not strongly justified given the function's simplicity. The comment should be deleted.
5. webview_panels/src/modules/documentationEditor/components/documentationPropagation/DocumentationPropagation.tsx:149
  • Draft comment:
    Use semantic versioning (semver) for version comparisons instead of direct array index comparisons for better accuracy and readability.
  • Reason this comment was not posted:
    Decided after close inspection that this draft comment was likely wrong and/or not actionable:
    The comment seems to be misplaced as there is no evidence of version comparison or array index usage for versioning in the provided code. The code is focused on propagating documentation and handling related data structures.
    I might be missing some context about where versioning could be relevant, but based on the provided code, there is no indication of version handling that would require semantic versioning.
    Given the code provided, there is no indication of version handling, so the comment about semantic versioning seems irrelevant.
    The comment about using semantic versioning is not relevant to the changes made in the diff and should be deleted.

Workflow ID: wflow_LZV4eGj5tmoQrvl0


You can customize Ellipsis with 👍 / 👎 feedback, review rules, user-specific overrides, quiet mode, and more.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (4)
webview_panels/src/modules/documentationEditor/components/documentationPropagation/DocumentationPropagation.tsx (4)

41-76: Consider consolidating state management

The component uses multiple separate useState hooks that could be consolidated into a single state object for better maintainability.

Consider refactoring the state management like this:

-  const [allColumns, setAllColumns] = useState<DocsItem[]>([]);
-  const [currColumns, setCurrColumns] = useState<DocsItem[]>([]);
-  const [isLoading, setIsLoading] = useState(false);
-  const [tableMetadata, setTableMetadata] = useState<TableMetadata[]>([]);
-  const [testsMetadata, setTestsMetadata] = useState<Record<string, unknown>>({});
-  const [selectedColumns, setSelectedColumns] = useState<Record<string, boolean>>({});
+  const [state, setState] = useState({
+    allColumns: [] as DocsItem[],
+    currColumns: [] as DocsItem[],
+    isLoading: false,
+    tableMetadata: [] as TableMetadata[],
+    testsMetadata: {} as Record<string, unknown>,
+    selectedColumns: {} as Record<string, boolean>
+  });

77-86: Extract magic number into a named constant

The iteration limit of 3 should be extracted into a named constant for better maintainability.

+const MAX_DOWNSTREAM_LEVELS = 3;
 const loadMoreDownstreamModels = async () => {
   executeRequestInAsync("columnLineageBase", { event: "start" });
   setIsLoading(true);
   let i = 0;
   const iAllColumns = [...allColumns];
   let iCurrColumns = currColumns;
-  while (i++ < 3) {
+  while (i++ < MAX_DOWNSTREAM_LEVELS) {

99-100: Remove commented code

Remove the commented line as it's not providing any value and could cause confusion.

   if (item.type === "indirect") continue;
-  // if (item.viewsType !== "Unchanged") continue;

156-158: Use nullish coalescing consistently

The eslint comment suggests nullish coalescing is preferred, but logical OR is used instead.

-        // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
-        patchPath: node?.patchPath || defaultPatchPath,
+        patchPath: node?.patchPath ?? defaultPatchPath,
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 84cb9ef and 42eb8bc.

📒 Files selected for processing (2)
  • src/webview_provider/docsEditPanel.ts (6 hunks)
  • webview_panels/src/modules/documentationEditor/components/documentationPropagation/DocumentationPropagation.tsx (1 hunks)
🧰 Additional context used
🪛 GitHub Check: CodeQL
src/webview_provider/docsEditPanel.ts

[failure] 810-810: Potential file system race condition
The file may have changed since it was checked.


[failure] 942-942: Potential file system race condition
The file may have changed since it was checked.

🪛 Biome
src/webview_provider/docsEditPanel.ts

[error] 779-782: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Unsafe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 789-791: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Unsafe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

webview_panels/src/modules/documentationEditor/components/documentationPropagation/DocumentationPropagation.tsx

[error] 170-170: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

🔇 Additional comments (6)
webview_panels/src/modules/documentationEditor/components/documentationPropagation/DocumentationPropagation.tsx (3)

13-39: LGTM: Well-structured interfaces

The interfaces are well-defined with appropriate types and clear structure.


166-176: ** Performance optimization needed in setAllColumnsValue**

This comment is skipped as it's already covered in past review comments and the static analysis hints correctly identified the performance issue with spread operator in reducers.

🧰 Tools
🪛 Biome

[error] 170-170: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


178-269: LGTM: Well-structured UI implementation

The UI implementation is well-organized with proper conditional rendering and state management for buttons and form elements.

src/webview_provider/docsEditPanel.ts (3)

23-23: LGTM: Clean dependency injection and import organization

The new imports and constructor injection of DbtLineageService are well-organized and follow good practices.

Also applies to: 35-35, 53-58, 121-121


660-668: LGTM: Clean implementation of columnLineageBase handler

The handler properly delegates to the lineage service and handles cancellation.


990-1042: LGTM: Well-implemented utility methods

The new utility methods handleSyncRequestFromWebview and sendResponseToWebview are well-structured with proper error handling and type safety.

Comment on lines +762 to +988
);

if (existingColumn) {
// ignore tests, data_tests from existing column, as it will be recreated in `getTestDataByColumn`
const { tests, data_tests, ...rest } = existingColumn.toJSON();
this.setOrDeleteInParsedDocument(
existingColumn,
"description",
column.description?.trim(),
);
this.setOrDeleteInParsedDocument(
existingColumn,
"data_type",
(rest.data_type || column.type)?.toLowerCase(),
);
const allTests = this.getTestDataByColumn(
message,
column.name,
project,
existingColumn.toJSON(),
);
this.setOrDeleteInParsedDocument(
existingColumn,
"tests",
allTests?.tests,
);
this.setOrDeleteInParsedDocument(
existingColumn,
"data_tests",
allTests?.data_tests,
);
} else {
const name = getColumnNameByCase(
column.name,
projectByFilePath.getAdapterType(),
);
model.addIn(["columns"], {
name,
description: column.description?.trim() || undefined,
data_type: column.type?.toLowerCase(),
...this.getTestDataByColumn(message, column.name, project),
...(isQuotedIdentifier(
column.name,
projectByFilePath.getAdapterType(),
)
? { quote: true }
: undefined),
});
}
});
}
// Force reload from manifest after manifest refresh
this.loadedFromManifest = false;
writeFileSync(patchPath, stringify(parsedDocFile, { lineWidth: 0 }));
this.documentation = (
await this.docGenService.getDocumentationForCurrentActiveFile()
).documentation;
const tests = await this.dbtTestService.getTestsForCurrentModel();
if (syncRequestId) {
this._panel!.webview.postMessage({
command: "response",
args: {
syncRequestId,
body: {
saved: true,
tests,
},
status: true,
},
});
}
} catch (error) {
this.transmitError();
this.telemetry.sendTelemetryError(
TelemetryEvents["DocumentationEditor/SaveError"],
error,
);
window.showErrorMessage(
`Could not save documentation to ${patchPath}: ${error}`,
);
this.terminal.error(
"saveDocumentationError",
`Could not save documentation to ${patchPath}`,
error,
false,
);
if (syncRequestId) {
this._panel!.webview.postMessage({
command: "response",
args: {
syncRequestId,
body: {
saved: false,
},
status: true,
},
});
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Address file system race conditions and improve code organization

The saveDocumentation method has several issues:

  1. Potential file system race conditions when checking existence and writing files

  2. Switch statement declarations that could leak

  3. Method is quite long and could be broken down

  4. Fix file system race conditions:

+ import { writeFileSync, readFileSync, renameSync } from 'fs';
+ import { tmpdir } from 'os';
+ import { randomBytes } from 'crypto';

  private async saveDocumentation(message: any, syncRequestId: string) {
    let patchPath = message.patchPath;
    try {
      // ... existing code ...

-     if (!existsSync(patchPath)) {
-       writeFileSync(patchPath, "");
+     // Create a temporary file
+     const tmpPath = path.join(tmpdir(), randomBytes(16).toString('hex'));
+     if (!existsSync(patchPath)) {
+       writeFileSync(tmpPath, "");
+       renameSync(tmpPath, patchPath);
      }

      // ... existing code ...

-     writeFileSync(patchPath, stringify(parsedDocFile, { lineWidth: 0 }));
+     writeFileSync(tmpPath, stringify(parsedDocFile, { lineWidth: 0 }));
+     renameSync(tmpPath, patchPath);
  1. Fix switch statement declarations:
  switch (message.dialogType) {
    case "Existing file": {
+     let openDialog;
      openDialog = await window.showOpenDialog({
        filters: { Yaml: ["yml"] },
        canSelectMany: false,
      });
      if (openDialog === undefined || openDialog.length === 0) {
        return;
      }
      patchPath = openDialog[0].fsPath;
      break;
    }
    case "New file": {
+     let saveDialog;
      saveDialog = await window.showSaveDialog({
        filters: { Yaml: ["yml"] },
      });
      if (!saveDialog) {
        return;
      }
      patchPath = saveDialog.fsPath;
      break;
    }
  }
  1. Consider extracting the following methods to improve readability:
  • handleFileSelection(dialogType: string): Promise<string>
  • updateModelDocumentation(model: YAMLMap, message: any)
  • updateColumnDocumentation(column: YAMLMap, message: any)

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 GitHub Check: CodeQL

[failure] 810-810: Potential file system race condition
The file may have changed since it was checked.


[failure] 942-942: Potential file system race condition
The file may have changed since it was checked.

🪛 Biome

[error] 779-782: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Unsafe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 789-791: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Unsafe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

Comment on lines 669 to 723
case "getDownstreamColumns": {
const targets = params.targets as [string, string][];
const _tables = targets
.map(
(t) =>
this.dbtLineageService.getDownstreamTables({ table: t[0] })
?.tables,
)
.filter((t) => Boolean(t))
.flat() as Table[];
const tables = _tables.map((t) => t?.table);
if (tables.length === 0) {
this.handleSyncRequestFromWebview(
syncRequestId,
() => ({}),
command,
);
return;
}
const selectedColumn = {
table: params.model as string,
name: params.column as string,
};
const currAnd1HopTables = [...tables, ...targets.map((t) => t[0])];
const columns = await this.dbtLineageService.getConnectedColumns({
targets,
currAnd1HopTables,
selectedColumn,
upstreamExpansion: false,
showIndirectEdges: false,
eventType: "documentation_propagation",
});
const testsResult = await Promise.all(
targets.map(async (t) => {
if (!t[0].startsWith("model")) {
return;
}
const splits = t[0].split(".");
const modelName = splits[splits.length - 1];
return await this.dbtTestService.getTestsForModel(modelName);
}),
);
const tests: Record<string, unknown> = {};
targets.forEach((t, i) => {
tests[t[0]] = testsResult[i];
});
this.handleSyncRequestFromWebview(
syncRequestId,
() => {
return { ...columns, tables: _tables, tests };
},
command,
);
break;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix async operations and add error handling in getDownstreamColumns

The current implementation has several issues that need to be addressed:

  1. Async operations aren't properly awaited
  2. Missing null check before using startsWith
  3. No error handling for the entire operation

Apply this diff to fix the issues:

  case "getDownstreamColumns": {
+   try {
      const targets = params.targets as [string, string][];
-     const _tables = targets
-       .map(
-         (t) =>
-           this.dbtLineageService.getDownstreamTables({ table: t[0] })
-             ?.tables,
-       )
-       .filter((t) => Boolean(t))
-       .flat() as Table[];
+     const _tablesNested = await Promise.all(
+       targets.map(async (t) => {
+         const result = await this.dbtLineageService.getDownstreamTables({ table: t[0] });
+         return result?.tables;
+       })
+     );
+     const _tables = _tablesNested.filter((t) => Boolean(t)).flat() as Table[];
      const tables = _tables.map((t) => t?.table);
      if (tables.length === 0) {
        this.handleSyncRequestFromWebview(
          syncRequestId,
          () => ({}),
          command,
        );
        return;
      }
      const selectedColumn = {
        table: params.model as string,
        name: params.column as string,
      };
      const currAnd1HopTables = [...tables, ...targets.map((t) => t[0])];
      const columns = await this.dbtLineageService.getConnectedColumns({
        targets,
        currAnd1HopTables,
        selectedColumn,
        upstreamExpansion: false,
        showIndirectEdges: false,
        eventType: "documentation_propagation",
      });
      const testsResult = await Promise.all(
        targets.map(async (t) => {
-         if (!t[0].startsWith("model")) {
+         if (!t[0] || !t[0].startsWith("model")) {
            return;
          }
          const splits = t[0].split(".");
          const modelName = splits[splits.length - 1];
          return await this.dbtTestService.getTestsForModel(modelName);
        }),
      );
      const tests: Record<string, unknown> = {};
      targets.forEach((t, i) => {
        tests[t[0]] = testsResult[i];
      });
      this.handleSyncRequestFromWebview(
        syncRequestId,
        () => {
          return { ...columns, tables: _tables, tests };
        },
        command,
      );
+   } catch (error) {
+     this.handleSyncRequestFromWebview(
+       syncRequestId,
+       () => {
+         throw error;
+       },
+       command,
+       true
+     );
+   }
    break;
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case "getDownstreamColumns": {
const targets = params.targets as [string, string][];
const _tables = targets
.map(
(t) =>
this.dbtLineageService.getDownstreamTables({ table: t[0] })
?.tables,
)
.filter((t) => Boolean(t))
.flat() as Table[];
const tables = _tables.map((t) => t?.table);
if (tables.length === 0) {
this.handleSyncRequestFromWebview(
syncRequestId,
() => ({}),
command,
);
return;
}
const selectedColumn = {
table: params.model as string,
name: params.column as string,
};
const currAnd1HopTables = [...tables, ...targets.map((t) => t[0])];
const columns = await this.dbtLineageService.getConnectedColumns({
targets,
currAnd1HopTables,
selectedColumn,
upstreamExpansion: false,
showIndirectEdges: false,
eventType: "documentation_propagation",
});
const testsResult = await Promise.all(
targets.map(async (t) => {
if (!t[0].startsWith("model")) {
return;
}
const splits = t[0].split(".");
const modelName = splits[splits.length - 1];
return await this.dbtTestService.getTestsForModel(modelName);
}),
);
const tests: Record<string, unknown> = {};
targets.forEach((t, i) => {
tests[t[0]] = testsResult[i];
});
this.handleSyncRequestFromWebview(
syncRequestId,
() => {
return { ...columns, tables: _tables, tests };
},
command,
);
break;
}
case "getDownstreamColumns": {
try {
const targets = params.targets as [string, string][];
const _tablesNested = await Promise.all(
targets.map(async (t) => {
const result = await this.dbtLineageService.getDownstreamTables({ table: t[0] });
return result?.tables;
})
);
const _tables = _tablesNested.filter((t) => Boolean(t)).flat() as Table[];
const tables = _tables.map((t) => t?.table);
if (tables.length === 0) {
this.handleSyncRequestFromWebview(
syncRequestId,
() => ({}),
command,
);
return;
}
const selectedColumn = {
table: params.model as string,
name: params.column as string,
};
const currAnd1HopTables = [...tables, ...targets.map((t) => t[0])];
const columns = await this.dbtLineageService.getConnectedColumns({
targets,
currAnd1HopTables,
selectedColumn,
upstreamExpansion: false,
showIndirectEdges: false,
eventType: "documentation_propagation",
});
const testsResult = await Promise.all(
targets.map(async (t) => {
if (!t[0] || !t[0].startsWith("model")) {
return;
}
const splits = t[0].split(".");
const modelName = splits[splits.length - 1];
return await this.dbtTestService.getTestsForModel(modelName);
}),
);
const tests: Record<string, unknown> = {};
targets.forEach((t, i) => {
tests[t[0]] = testsResult[i];
});
this.handleSyncRequestFromWebview(
syncRequestId,
() => {
return { ...columns, tables: _tables, tests };
},
command,
);
} catch (error) {
this.handleSyncRequestFromWebview(
syncRequestId,
() => {
throw error;
},
command,
true
);
}
break;
}

Copy link
Contributor

@ellipsis-dev ellipsis-dev bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Changes requested. Incremental review on 773d0b9 in 30 seconds

More details
  • Looked at 13 lines of code in 1 files
  • Skipped 0 files when reviewing.
  • Skipped posting 0 drafted comments based on config settings.

Workflow ID: wflow_yyFUkAfUp7X5fHtE


Want Ellipsis to fix these issues? Tag @ellipsis-dev in a comment. You can customize Ellipsis with 👍 / 👎 feedback, review rules, user-specific overrides, quiet mode, and more.

if (item.type === "indirect") continue;
if (item.viewsType !== "Unchanged") continue;
if (
iCurrColumns.find(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider optimizing the search for columns in iCurrColumns by using a Set or Map for faster lookup instead of using find in a loop. This will improve performance, especially with larger datasets.

iCurrColumns = [];
break;
}
const newColumns: DocsItem[] = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using early returns in loadMoreDownstreamModels to reduce nesting and improve readability. This applies to other parts of the function as well.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Outside diff range and nitpick comments (3)
webview_panels/src/modules/documentationEditor/components/documentationPropagation/DocumentationPropagation.tsx (2)

41-70: Consider using useReducer for better state management

The component currently manages multiple related state variables. Consider refactoring to use useReducer to centralize state management and make state updates more predictable.

Example implementation:

interface State {
  allColumns: DocsItem[];
  currColumns: DocsItem[];
  isLoading: boolean;
  tableMetadata: TableMetadata[];
  testsMetadata: Record<string, unknown>;
  selectedColumns: Record<string, boolean>;
}

type Action = 
  | { type: 'SET_ALL_COLUMNS', payload: DocsItem[] }
  | { type: 'SET_CURR_COLUMNS', payload: DocsItem[] }
  | { type: 'SET_LOADING', payload: boolean }
  // ... other actions

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case 'SET_ALL_COLUMNS':
      return { ...state, allColumns: action.payload };
    // ... other cases
  }
};

186-271: Enhance accessibility and loading state feedback

Consider the following improvements:

  1. Add aria-labels to buttons and inputs
  2. Show loading spinner during operations
  3. Add error messages for failed operations

Example improvements:

 <Button
   color="primary"
   outline
   onClick={loadMoreDownstreamModels}
   disabled={isLoading}
+  aria-label="Load more downstream models"
 >
-  Load 3 more downstream levels
+  {isLoading ? 'Loading...' : 'Load 3 more downstream levels'}
 </Button>
src/webview_provider/docsEditPanel.ts (1)

762-762: Add return type for better type safety

The function should explicitly declare its return type.

- private async saveDocumentation(message: any, syncRequestId: string) {
+ private async saveDocumentation(message: any, syncRequestId: string): Promise<void> {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 42eb8bc and 773d0b9.

📒 Files selected for processing (2)
  • src/webview_provider/docsEditPanel.ts (6 hunks)
  • webview_panels/src/modules/documentationEditor/components/documentationPropagation/DocumentationPropagation.tsx (1 hunks)
🧰 Additional context used
🪛 GitHub Check: CodeQL
src/webview_provider/docsEditPanel.ts

[failure] 810-810: Potential file system race condition
The file may have changed since it was checked.


[failure] 942-942: Potential file system race condition
The file may have changed since it was checked.

🪛 Biome
src/webview_provider/docsEditPanel.ts

[error] 779-782: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Unsafe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 789-791: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Unsafe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

webview_panels/src/modules/documentationEditor/components/documentationPropagation/DocumentationPropagation.tsx

[error] 174-174: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

🔇 Additional comments (6)
webview_panels/src/modules/documentationEditor/components/documentationPropagation/DocumentationPropagation.tsx (3)

13-39: LGTM! Well-structured interfaces

The interfaces are well-defined with appropriate types and clear structure.


140-168: Add validation and error handling to propagateDocumentation

The function should validate inputs and handle potential errors during the bulk save operation.

Consider adding:

  1. Input validation before processing
  2. Try-catch block around the save operation
  3. User feedback for success/failure cases

170-180: Skip: Performance issue already flagged

This issue was already identified in a previous review comment and the static analysis hints.

🧰 Tools
🪛 Biome

[error] 174-174: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

src/webview_provider/docsEditPanel.ts (3)

23-23: LGTM: Clean dependency injection and imports

The new imports and constructor parameter for DbtLineageService are well-organized and follow dependency injection patterns.

Also applies to: 35-35, 53-58, 121-121


660-668: LGTM: Clean implementation of column lineage base handler

The handler properly integrates with the lineage service and includes appropriate event handling.


990-1042: LGTM: Well-structured utility methods

The new utility methods are well-implemented with:

  • Proper error handling including PythonException
  • Consistent response formatting
  • Clear separation of concerns

Comment on lines 77 to 138
const loadMoreDownstreamModels = async () => {
executeRequestInAsync("columnLineageBase", { event: "start" });
setIsLoading(true);
let i = 0;
const iAllColumns = [...allColumns];
let iCurrColumns = currColumns;
while (i++ < 3) {
if (iCurrColumns.length === 0) {
break;
}
const result = (await executeRequestInSync("getDownstreamColumns", {
targets: iCurrColumns.map((c) => [c.model, c.column]),
model: currentDocsData?.uniqueId,
column: name,
})) as DownstreamColumns;
if (!result.column_lineage) {
break;
}
setTableMetadata((prev) => [...prev, ...result.tables]);
setTestsMetadata((prev) => ({ ...prev, ...result.tests }));
if (result.column_lineage.length === 0) {
iCurrColumns = [];
break;
}
const newColumns: DocsItem[] = [];
for (const item of result.column_lineage) {
if (item.type === "indirect") continue;
if (item.viewsType !== "Unchanged") continue;
if (
iCurrColumns.find(
(c) => c.model === item.target[0] && c.column === item.target[1],
)
) {
newColumns.push({
model: item.source[0],
column: item.source[1],
description:
result.tables.find((t) => t.table === item.source[0])?.columns[
item.source[1]
]?.description ?? "",
});
}
}
iCurrColumns = newColumns;
iAllColumns.push(...newColumns);
}
executeRequestInAsync("columnLineageBase", { event: "end" });
const finalAllColumns: DocsItem[] = [];
for (const c of iAllColumns) {
if (
finalAllColumns.find(
(_c) => _c.model === c.model && _c.column === c.column,
)
) {
continue;
}
finalAllColumns.push(c);
}
setIsLoading(false);
setAllColumns(finalAllColumns);
setCurrColumns(iCurrColumns);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling and simplify loadMoreDownstreamModels function

The function could benefit from:

  1. Proper error handling for failed requests
  2. Breaking down complex logic into smaller functions
  3. Adding loading state error handling

Consider refactoring like this:

const loadMoreDownstreamModels = async () => {
  try {
    executeRequestInAsync("columnLineageBase", { event: "start" });
    setIsLoading(true);
    
    const newColumns = await loadNextThreeLevels();
    const finalColumns = removeDuplicateColumns(newColumns);
    
    setAllColumns(finalColumns);
    setCurrColumns(newColumns.currColumns);
  } catch (error) {
    console.error('Failed to load downstream models:', error);
    // Handle error appropriately
  } finally {
    setIsLoading(false);
    executeRequestInAsync("columnLineageBase", { event: "end" });
  }
};

Comment on lines 669 to 723
case "getDownstreamColumns": {
const targets = params.targets as [string, string][];
const testsResult = await Promise.all(
targets.map(async (t) => {
if (!t[0].startsWith("model")) {
return;
}
const splits = t[0].split(".");
const modelName = splits[splits.length - 1];
return await this.dbtTestService.getTestsForModel(modelName);
}),
);
const tests: Record<string, unknown> = {};
targets.forEach((t, i) => {
tests[t[0]] = testsResult[i];
});
const _tables = targets
.map(
(t) =>
this.dbtLineageService.getDownstreamTables({ table: t[0] })
?.tables,
)
.filter((t) => Boolean(t))
.flat() as Table[];
const tables = _tables.map((t) => t?.table);
if (tables.length === 0) {
this.handleSyncRequestFromWebview(
syncRequestId,
() => ({ column_lineage: [], tables: [], tests }),
command,
);
return;
}
const selectedColumn = {
table: params.model as string,
name: params.column as string,
};
const currAnd1HopTables = [...tables, ...targets.map((t) => t[0])];
const columns = await this.dbtLineageService.getConnectedColumns({
targets,
currAnd1HopTables,
selectedColumn,
upstreamExpansion: false,
showIndirectEdges: false,
eventType: "documentation_propagation",
});
this.handleSyncRequestFromWebview(
syncRequestId,
() => {
return { ...columns, tables: _tables, tests };
},
command,
);
break;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix async operations and add safety checks

The getDownstreamColumns handler needs several improvements:

  1. Async operations aren't properly awaited
  2. Missing null check for array access
  3. The code could be more maintainable with intermediate variables

Apply this diff to fix the issues:

case "getDownstreamColumns": {
  const targets = params.targets as [string, string][];
  const testsResult = await Promise.all(
    targets.map(async (t) => {
-     if (!t[0].startsWith("model")) {
+     if (!t[0] || !t[0].startsWith("model")) {
        return;
      }
      const splits = t[0].split(".");
      const modelName = splits[splits.length - 1];
      return await this.dbtTestService.getTestsForModel(modelName);
    }),
  );
  const tests: Record<string, unknown> = {};
  targets.forEach((t, i) => {
    tests[t[0]] = testsResult[i];
  });
- const _tables = targets
-   .map(
-     (t) =>
-       this.dbtLineageService.getDownstreamTables({ table: t[0] })
-         ?.tables,
-   )
-   .filter((t) => Boolean(t))
-   .flat() as Table[];
+ const _tablesNested = await Promise.all(
+   targets.map(async (t) => {
+     const result = await this.dbtLineageService.getDownstreamTables({ table: t[0] });
+     return result?.tables;
+   })
+ );
+ const _tables = _tablesNested.filter((t) => Boolean(t)).flat() as Table[];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case "getDownstreamColumns": {
const targets = params.targets as [string, string][];
const testsResult = await Promise.all(
targets.map(async (t) => {
if (!t[0].startsWith("model")) {
return;
}
const splits = t[0].split(".");
const modelName = splits[splits.length - 1];
return await this.dbtTestService.getTestsForModel(modelName);
}),
);
const tests: Record<string, unknown> = {};
targets.forEach((t, i) => {
tests[t[0]] = testsResult[i];
});
const _tables = targets
.map(
(t) =>
this.dbtLineageService.getDownstreamTables({ table: t[0] })
?.tables,
)
.filter((t) => Boolean(t))
.flat() as Table[];
const tables = _tables.map((t) => t?.table);
if (tables.length === 0) {
this.handleSyncRequestFromWebview(
syncRequestId,
() => ({ column_lineage: [], tables: [], tests }),
command,
);
return;
}
const selectedColumn = {
table: params.model as string,
name: params.column as string,
};
const currAnd1HopTables = [...tables, ...targets.map((t) => t[0])];
const columns = await this.dbtLineageService.getConnectedColumns({
targets,
currAnd1HopTables,
selectedColumn,
upstreamExpansion: false,
showIndirectEdges: false,
eventType: "documentation_propagation",
});
this.handleSyncRequestFromWebview(
syncRequestId,
() => {
return { ...columns, tables: _tables, tests };
},
command,
);
break;
}
case "getDownstreamColumns": {
const targets = params.targets as [string, string][];
const testsResult = await Promise.all(
targets.map(async (t) => {
if (!t[0] || !t[0].startsWith("model")) {
return;
}
const splits = t[0].split(".");
const modelName = splits[splits.length - 1];
return await this.dbtTestService.getTestsForModel(modelName);
}),
);
const tests: Record<string, unknown> = {};
targets.forEach((t, i) => {
tests[t[0]] = testsResult[i];
});
const _tablesNested = await Promise.all(
targets.map(async (t) => {
const result = await this.dbtLineageService.getDownstreamTables({ table: t[0] });
return result?.tables;
})
);
const _tables = _tablesNested.filter((t) => Boolean(t)).flat() as Table[];
const tables = _tables.map((t) => t?.table);
if (tables.length === 0) {
this.handleSyncRequestFromWebview(
syncRequestId,
() => ({ column_lineage: [], tables: [], tests }),
command,
);
return;
}
const selectedColumn = {
table: params.model as string,
name: params.column as string,
};
const currAnd1HopTables = [...tables, ...targets.map((t) => t[0])];
const columns = await this.dbtLineageService.getConnectedColumns({
targets,
currAnd1HopTables,
selectedColumn,
upstreamExpansion: false,
showIndirectEdges: false,
eventType: "documentation_propagation",
});
this.handleSyncRequestFromWebview(
syncRequestId,
() => {
return { ...columns, tables: _tables, tests };
},
command,
);
break;
}

Copy link
Contributor

@ellipsis-dev ellipsis-dev bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Looks good to me! Incremental review on b944892 in 30 seconds

More details
  • Looked at 13 lines of code in 1 files
  • Skipped 0 files when reviewing.
  • Skipped posting 2 drafted comments based on config settings.
1. webview_panels/src/modules/documentationEditor/components/documentationPropagation/DocumentationPropagation.tsx:104
  • Draft comment:
    Ensure that filtering out 'Transformation' types aligns with the intended logic. Double-check if this change from 'Unchanged' to 'Transformation' is correct.
  • Reason this comment was not posted:
    Comment did not seem useful.
2. webview_panels/src/modules/documentationEditor/components/documentationPropagation/DocumentationPropagation.tsx:101
  • Draft comment:
    Use early returns in loadMoreDownstreamModels to reduce nesting and improve readability. This applies to the entire function.
  • Reason this comment was not posted:
    Comment was not on a valid diff hunk.

Workflow ID: wflow_sNJr33vS0qvawQeH


You can customize Ellipsis with 👍 / 👎 feedback, review rules, user-specific overrides, quiet mode, and more.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (2)
webview_panels/src/modules/documentationEditor/components/documentationPropagation/DocumentationPropagation.tsx (2)

125-134: Optimize duplicate checking in finalAllColumns construction

In the loop constructing finalAllColumns, using .find inside a loop can lead to O(n²) time complexity due to repeated linear searches. To improve performance, consider using a Set or a Map to track duplicates more efficiently.

Apply this diff to enhance performance:

-    const finalAllColumns: DocsItem[] = [];
-    for (const c of iAllColumns) {
-      if (
-        finalAllColumns.find(
-          (_c) => _c.model === c.model && _c.column === c.column,
-        )
-      ) {
-        continue;
-      }
-      finalAllColumns.push(c);
-    }
+    const seenKeys = new Set<string>();
+    const finalAllColumns: DocsItem[] = [];
+    for (const c of iAllColumns) {
+      const key = c.model + "/" + c.column;
+      if (seenKeys.has(key)) {
+        continue;
+      }
+      seenKeys.add(key);
+      finalAllColumns.push(c);
+    }

263-265: Optimize selection count calculation

Calculating the number of selected columns using Object.values(selectedColumns).filter((v) => Boolean(v)).length on every render can be inefficient for large datasets. Consider maintaining a separate count state or using a memoized selector to improve performance.

Here's how you might implement a selection count state:

+  const [selectedCount, setSelectedCount] = useState(0);

+  useEffect(() => {
+    setSelectedCount(
+      Object.values(selectedColumns).filter((v) => Boolean(v)).length
+    );
+  }, [selectedColumns]);

   <Button
     color="primary"
     disabled={
-      Object.values(selectedColumns).filter((v) => Boolean(v)).length === 0
+      selectedCount === 0
     }
     onClick={() => propagateDocumentation()}
   >
     Propagate documentation to selected models
   </Button>
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 773d0b9 and b944892.

📒 Files selected for processing (1)
  • webview_panels/src/modules/documentationEditor/components/documentationPropagation/DocumentationPropagation.tsx (1 hunks)
🧰 Additional context used
🪛 Biome
webview_panels/src/modules/documentationEditor/components/documentationPropagation/DocumentationPropagation.tsx

[error] 174-174: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

🔇 Additional comments (1)
webview_panels/src/modules/documentationEditor/components/documentationPropagation/DocumentationPropagation.tsx (1)

226-231: Avoid using spread operator when updating state in setSelectedColumns

Using the spread operator in state updates can be inefficient for large objects. Since selectedColumns is an object, consider using functional updates or alternative methods to update state without unnecessarily copying the entire object.

However, in this context, the impact might be minimal. If performance becomes a concern with large datasets, refactoring might be needed.

Comment on lines +160 to +161
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
patchPath: node?.patchPath || defaultPatchPath,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use nullish coalescing operator instead of logical OR

The use of || can lead to unintended behavior when node?.patchPath is a falsy value like an empty string. Replace node?.patchPath || defaultPatchPath with node?.patchPath ?? defaultPatchPath to ensure that defaultPatchPath is used only when node?.patchPath is null or undefined.

Apply this diff to follow best practices:

-        // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
-        patchPath: node?.patchPath || defaultPatchPath,
+        patchPath: node?.patchPath ?? defaultPatchPath,

Also, remove the ESLint disable comment since it is no longer necessary.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
patchPath: node?.patchPath || defaultPatchPath,
patchPath: node?.patchPath ?? defaultPatchPath,

Comment on lines +140 to +168
const propagateDocumentation = async () => {
const defaultPackageName = tableMetadata.filter((t) => t.packageName)[0]
?.packageName;
const defaultPatchPath = defaultPackageName
? defaultPackageName + "://models/schema.yml"
: "";

const req = [];

for (const item of allColumns) {
const key = item.model + "/" + item.column;
if (!selectedColumns[key]) continue;
const splits = item.model.split(".");
const modelName = splits[splits.length - 1];
const node = tableMetadata.find((t) => t.table === item.model);
req.push({
name: modelName,
description: node?.description,
columns: [{ name: item.column, description: currColumnDescription }],
dialogType: "Existing file",
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
patchPath: node?.patchPath || defaultPatchPath,
filePath: node?.url,
updatedTests: testsMetadata[item.model],
});
}

await executeRequestInSync("saveDocumentationBulk", { models: req });
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling in propagateDocumentation function

The function propagateDocumentation lacks error handling for the asynchronous operation. If executeRequestInSync("saveDocumentationBulk", { models: req }) fails, it could lead to unhandled promise rejections and impact user experience. Implement error handling to manage potential failures gracefully.

Apply this diff to include error handling:

-    await executeRequestInSync("saveDocumentationBulk", { models: req });
+    try {
+      await executeRequestInSync("saveDocumentationBulk", { models: req });
+    } catch (error) {
+      console.error('Failed to propagate documentation:', error);
+      // Optionally, provide user feedback or retry logic here
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const propagateDocumentation = async () => {
const defaultPackageName = tableMetadata.filter((t) => t.packageName)[0]
?.packageName;
const defaultPatchPath = defaultPackageName
? defaultPackageName + "://models/schema.yml"
: "";
const req = [];
for (const item of allColumns) {
const key = item.model + "/" + item.column;
if (!selectedColumns[key]) continue;
const splits = item.model.split(".");
const modelName = splits[splits.length - 1];
const node = tableMetadata.find((t) => t.table === item.model);
req.push({
name: modelName,
description: node?.description,
columns: [{ name: item.column, description: currColumnDescription }],
dialogType: "Existing file",
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
patchPath: node?.patchPath || defaultPatchPath,
filePath: node?.url,
updatedTests: testsMetadata[item.model],
});
}
await executeRequestInSync("saveDocumentationBulk", { models: req });
};
const propagateDocumentation = async () => {
const defaultPackageName = tableMetadata.filter((t) => t.packageName)[0]
?.packageName;
const defaultPatchPath = defaultPackageName
? defaultPackageName + "://models/schema.yml"
: "";
const req = [];
for (const item of allColumns) {
const key = item.model + "/" + item.column;
if (!selectedColumns[key]) continue;
const splits = item.model.split(".");
const modelName = splits[splits.length - 1];
const node = tableMetadata.find((t) => t.table === item.model);
req.push({
name: modelName,
description: node?.description,
columns: [{ name: item.column, description: currColumnDescription }],
dialogType: "Existing file",
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
patchPath: node?.patchPath || defaultPatchPath,
filePath: node?.url,
updatedTests: testsMetadata[item.model],
});
}
try {
await executeRequestInSync("saveDocumentationBulk", { models: req });
} catch (error) {
console.error('Failed to propagate documentation:', error);
// Optionally, provide user feedback or retry logic here
}
};

Copy link
Contributor

@ellipsis-dev ellipsis-dev bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Changes requested. Incremental review on cc9f90a in 1 minute and 1 seconds

More details
  • Looked at 48 lines of code in 2 files
  • Skipped 0 files when reviewing.
  • Skipped posting 2 drafted comments based on config settings.
1. src/webview_provider/docsEditPanel.ts:688
  • Draft comment:
    The method getUpstreamTables should be getDownstreamTables to correctly fetch downstream tables.
                  this.dbtLineageService.getDownstreamTables({ table: t[0] })
  • Reason this comment was not posted:
    Decided after close inspection that this draft comment was likely wrong and/or not actionable:
    The change from getDownstreamTables to getUpstreamTables seems intentional, especially since upstreamExpansion was also set to true. This suggests the code is now intended to handle upstream tables. The comment does not provide strong evidence that the change is incorrect, and the context supports the change.
    I might be missing some specific business logic that requires downstream tables instead of upstream. However, the change in the code suggests a clear intention to switch to upstream tables.
    The change in the code, including setting upstreamExpansion to true, strongly indicates that the intention is to work with upstream tables. Without additional context suggesting otherwise, the comment seems incorrect.
    The comment should be deleted as the change to getUpstreamTables appears intentional and aligns with other changes in the code.
2. src/webview_provider/docsEditPanel.ts:685
  • Draft comment:
    Use semantic versioning (semver) for version comparisons instead of direct array index comparisons. This ensures more accurate and reliable version checks. This issue is also present in other parts of the code.
  • Reason this comment was not posted:
    Comment was not on a valid diff hunk.

Workflow ID: wflow_89ZEWB7PBOdO5j0Q


Want Ellipsis to fix these issues? Tag @ellipsis-dev in a comment. You can customize Ellipsis with 👍 / 👎 feedback, review rules, user-specific overrides, quiet mode, and more.

if (item.viewsType === "Transformation") continue;
if (
iCurrColumns.find(
(c) => c.model === item.source[0] && c.column === item.source[1],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition should check against item.target instead of item.source to correctly identify current columns.

Suggested change
(c) => c.model === item.source[0] && c.column === item.source[1],
(c) => c.model === item.target[0] && c.column === item.target[1],

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between b944892 and cc9f90a.

📒 Files selected for processing (2)
  • src/webview_provider/docsEditPanel.ts (6 hunks)
  • webview_panels/src/modules/documentationEditor/components/documentationPropagation/DocumentationPropagation.tsx (1 hunks)
🧰 Additional context used
🪛 Biome
src/webview_provider/docsEditPanel.ts

[error] 779-782: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Unsafe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 789-791: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Unsafe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

webview_panels/src/modules/documentationEditor/components/documentationPropagation/DocumentationPropagation.tsx

[error] 174-174: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

🔇 Additional comments (3)
src/webview_provider/docsEditPanel.ts (3)

23-23: LGTM: Clean dependency injection and error handling setup.

The new imports and constructor changes follow good practices for dependency injection and error handling.

Also applies to: 35-35, 53-58, 121-121


660-668: LGTM: Clean implementation of column lineage base handler.

The handler properly delegates to the lineage service and handles cancellation.


990-1042: LGTM: Well-structured utility methods with proper error handling.

The new utility methods are well-implemented with:

  • Proper error handling for different error types
  • Clear separation of concerns
  • Type-safe message handling

Comment on lines +809 to +811
if (!existsSync(patchPath)) {
writeFileSync(patchPath, "");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Address potential file system race conditions.

The current implementation has potential race conditions when checking file existence and writing files.

Apply this diff to fix the issues:

+ import { writeFileSync, readFileSync, renameSync } from 'fs';
+ import { tmpdir } from 'os';
+ import { randomBytes } from 'crypto';

- if (!existsSync(patchPath)) {
-   writeFileSync(patchPath, "");
- }
+ const tmpPath = path.join(tmpdir(), randomBytes(16).toString('hex'));
+ writeFileSync(tmpPath, "");
+ renameSync(tmpPath, patchPath);

- writeFileSync(patchPath, stringify(parsedDocFile, { lineWidth: 0 }));
+ const tmpOutputPath = path.join(tmpdir(), randomBytes(16).toString('hex'));
+ writeFileSync(tmpOutputPath, stringify(parsedDocFile, { lineWidth: 0 }));
+ renameSync(tmpOutputPath, patchPath);

Also applies to: 941-942

Comment on lines +777 to +800
switch (message.dialogType) {
case "Existing file":
const openDialog = await window.showOpenDialog({
filters: { Yaml: ["yml"] },
canSelectMany: false,
});
if (openDialog === undefined || openDialog.length === 0) {
return;
}
patchPath = openDialog[0].fsPath;
break;
case "New file":
const saveDialog = await window.showSaveDialog({
filters: { Yaml: ["yml"] },
});
if (!saveDialog) {
return;
}
this.telemetry.sendTelemetryEvent(
TelemetryEvents["DocumentationEditor/SaveNewFilePathSelect"],
);
patchPath = saveDialog.fsPath;
break;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix switch statement variable declarations.

The switch statement has variable declarations that could leak between cases.

Apply this diff to fix the issue:

switch (message.dialogType) {
  case "Existing file": {
+   let openDialog;
    openDialog = await window.showOpenDialog({
      filters: { Yaml: ["yml"] },
      canSelectMany: false,
    });
    if (openDialog === undefined || openDialog.length === 0) {
      return;
    }
    patchPath = openDialog[0].fsPath;
    break;
  }
  case "New file": {
+   let saveDialog;
    saveDialog = await window.showSaveDialog({
      filters: { Yaml: ["yml"] },
    });
    if (!saveDialog) {
      return;
    }
    patchPath = saveDialog.fsPath;
    break;
  }
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
switch (message.dialogType) {
case "Existing file":
const openDialog = await window.showOpenDialog({
filters: { Yaml: ["yml"] },
canSelectMany: false,
});
if (openDialog === undefined || openDialog.length === 0) {
return;
}
patchPath = openDialog[0].fsPath;
break;
case "New file":
const saveDialog = await window.showSaveDialog({
filters: { Yaml: ["yml"] },
});
if (!saveDialog) {
return;
}
this.telemetry.sendTelemetryEvent(
TelemetryEvents["DocumentationEditor/SaveNewFilePathSelect"],
);
patchPath = saveDialog.fsPath;
break;
}
switch (message.dialogType) {
case "Existing file": {
let openDialog;
openDialog = await window.showOpenDialog({
filters: { Yaml: ["yml"] },
canSelectMany: false,
});
if (openDialog === undefined || openDialog.length === 0) {
return;
}
patchPath = openDialog[0].fsPath;
break;
}
case "New file": {
let saveDialog;
saveDialog = await window.showSaveDialog({
filters: { Yaml: ["yml"] },
});
if (!saveDialog) {
return;
}
this.telemetry.sendTelemetryEvent(
TelemetryEvents["DocumentationEditor/SaveNewFilePathSelect"],
);
patchPath = saveDialog.fsPath;
break;
}
}
🧰 Tools
🪛 Biome

[error] 779-782: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Unsafe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 789-791: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Unsafe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

Comment on lines +669 to +723
case "getDownstreamColumns": {
const targets = params.targets as [string, string][];
const testsResult = await Promise.all(
targets.map(async (t) => {
if (!t[0].startsWith("model")) {
return;
}
const splits = t[0].split(".");
const modelName = splits[splits.length - 1];
return await this.dbtTestService.getTestsForModel(modelName);
}),
);
const tests: Record<string, unknown> = {};
targets.forEach((t, i) => {
tests[t[0]] = testsResult[i];
});
const _tables = targets
.map(
(t) =>
this.dbtLineageService.getUpstreamTables({ table: t[0] })
?.tables,
)
.filter((t) => Boolean(t))
.flat() as Table[];
const tables = _tables.map((t) => t?.table);
if (tables.length === 0) {
this.handleSyncRequestFromWebview(
syncRequestId,
() => ({ column_lineage: [], tables: [], tests }),
command,
);
return;
}
const selectedColumn = {
table: params.model as string,
name: params.column as string,
};
const currAnd1HopTables = [...tables, ...targets.map((t) => t[0])];
const columns = await this.dbtLineageService.getConnectedColumns({
targets,
currAnd1HopTables,
selectedColumn,
upstreamExpansion: true,
showIndirectEdges: false,
eventType: "documentation_propagation",
});
this.handleSyncRequestFromWebview(
syncRequestId,
() => {
return { ...columns, tables: _tables, tests };
},
command,
);
break;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix async operations and add safety checks.

The getDownstreamColumns handler needs several improvements:

  1. Missing null check before using startsWith
  2. Async operations in getDownstreamTables aren't properly awaited
  3. No error handling for the entire operation

Apply this diff to fix the issues:

case "getDownstreamColumns": {
  try {
    const targets = params.targets as [string, string][];
    const testsResult = await Promise.all(
      targets.map(async (t) => {
-       if (!t[0].startsWith("model")) {
+       if (!t[0] || !t[0].startsWith("model")) {
          return;
        }
        const splits = t[0].split(".");
        const modelName = splits[splits.length - 1];
        return await this.dbtTestService.getTestsForModel(modelName);
      }),
    );
    const tests: Record<string, unknown> = {};
    targets.forEach((t, i) => {
      tests[t[0]] = testsResult[i];
    });
-   const _tables = targets
-     .map(
-       (t) =>
-         this.dbtLineageService.getUpstreamTables({ table: t[0] })
-           ?.tables,
-     )
-     .filter((t) => Boolean(t))
-     .flat() as Table[];
+   const _tablesNested = await Promise.all(
+     targets.map(async (t) => {
+       const result = await this.dbtLineageService.getUpstreamTables({ table: t[0] });
+       return result?.tables;
+     })
+   );
+   const _tables = _tablesNested.filter((t) => Boolean(t)).flat() as Table[];
    // ... rest of the code ...
  } catch (error) {
    this.handleSyncRequestFromWebview(
      syncRequestId,
      () => {
        throw error;
      },
      command,
      true
    );
  }
}

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +125 to +135
for (const c of iAllColumns) {
if (
finalAllColumns.find(
(_c) => _c.model === c.model && _c.column === c.column,
)
) {
continue;
}
finalAllColumns.push(c);
}
setIsLoading(false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor duplicate removal to improve performance

Using find within a loop to check for duplicates results in O(n²) time complexity. This can negatively impact performance with larger datasets.

Apply the following refactor to enhance performance:

- const finalAllColumns: DocsItem[] = [];
- for (const c of iAllColumns) {
-   if (
-     finalAllColumns.find(
-       (_c) => _c.model === c.model && _c.column === c.column,
-     )
-   ) {
-     continue;
-   }
-   finalAllColumns.push(c);
- }
+ const seenKeys = new Set<string>();
+ const finalAllColumns: DocsItem[] = [];
+ for (const c of iAllColumns) {
+   const key = `${c.model}/${c.column}`;
+   if (!seenKeys.has(key)) {
+     seenKeys.add(key);
+     finalAllColumns.push(c);
+   }
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const c of iAllColumns) {
if (
finalAllColumns.find(
(_c) => _c.model === c.model && _c.column === c.column,
)
) {
continue;
}
finalAllColumns.push(c);
}
setIsLoading(false);
const seenKeys = new Set<string>();
const finalAllColumns: DocsItem[] = [];
for (const c of iAllColumns) {
const key = `${c.model}/${c.column}`;
if (!seenKeys.has(key)) {
seenKeys.add(key);
finalAllColumns.push(c);
}
}
setIsLoading(false);

@mdesmet mdesmet merged commit a9d0046 into master Nov 22, 2024
9 checks passed
@mdesmet mdesmet deleted the feat-documentation-lineage-propagation branch November 22, 2024 04:12
@coderabbitai coderabbitai bot mentioned this pull request Nov 27, 2024
2 tasks
@coderabbitai coderabbitai bot mentioned this pull request Feb 4, 2025
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants