Skip to content

fix(tableau): skip populate_views for filtered workbooks; O(1) project lookup - #33584

Open
zerafachris wants to merge 1 commit into
open-metadata:mainfrom
zerafachris:fix/tableau-early-filter-dict-projects-33441
Open

zerafachris wants to merge 1 commit into
open-metadata:mainfrom
zerafachris:fix/tableau-early-filter-dict-projects-33441

Conversation

@zerafachris

@zerafachris zerafachris commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #33441 — on large Tableau instances the connector calls populate_views(workbook, usage=True) for every workbook before any dashboard/project filtering is applied. This makes full syncs take days.

Changes

ingestion/src/metadata/ingestion/source/dashboard/tableau/client.py

  • get_all_projects() now builds all_projects as a dict[str, ProjectItem] keyed by str(project.id). The previous implementation stored a flat list, so get_project_parents_by_id had to do an O(n) next(… for … in self.all_projects …) scan on every recursive call. The dict makes each lookup O(1).

  • get_workbooks() gains an optional filter_fn(name: str, project_path: str | None) -> bool parameter. When provided, the closure is evaluated before the expensive populate_views API call. Returning True skips the workbook entirely, so filtered-out workbooks pay no cost.

ingestion/src/metadata/ingestion/source/dashboard/tableau/metadata.py

  • get_dashboards_list() builds an _early_filter closure from the existing dashboardFilterPattern / projectFilterPattern source-config knobs (using the existing filter_by_dashboard / filter_by_project helpers) and passes it as filter_fn to the client.

Impact

No functional change for instances without filter patterns. For instances that do filter (the common case for large deployments), workbooks that would previously be discarded after populate_views are now discarded before it, making the sync proportionally faster.

Tests

  • All 13 existing test_tableau_client.py tests pass.
  • All 47 existing test_tableau.py tests pass.
  • ruff check is clean on both changed files.

Prepared with AI assistance (Claude Code, Anthropic), reviewed for correctness before submission.

🤖 Generated with Claude Code

RetriggerConfidence Score: 4/5

The PR is not safe to merge until missing project paths preserve the existing direct-project fallback and the explicit bounded-cache requirement is satisfied.

Findings

  1. P1 Missing Projects Drop Workbooks
  2. P2 Filtered Status Is Lost
  3. P2 Project Cache Is Unbounded
Summary

This PR adds pre-fetch Tableau workbook filtering and replaces linear project lookup with an indexed lookup.

  • Applies dashboard and project patterns before expensive view and datasource retrieval.
  • Builds a project-ID dictionary for constant-time hierarchy traversal.
  • Introduces a missing-project fallback regression, removes filtered-record status accounting, and conflicts with the repository's bounded-cache requirement.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  C[Dashboard and project patterns] --> E[Early workbook filter]
  P[Project ID index] --> E
  E -->|Keep| V[Populate views and datasources]
  V --> D[Downstream dashboard stage]
  D --> O[Emit dashboard metadata]
  E -->|Skip| X[No API enrichment]
  X --> N[No downstream filtered-status record]
Loading

Reviews (1) · Last reviewed commit: "fix(tableau): skip populate_views for fi..."

…t lookup

Large Tableau instances call `populate_views(workbook, usage=True)` for every
workbook before any dashboard/project filtering, making full syncs take days.

Changes:
- `get_all_projects()`: build `all_projects` as a `dict[str, ProjectItem]`
  keyed by project id for O(1) lookup (was O(n) linear scan via `next()`).
- `get_workbooks()`: accept an optional `filter_fn(name, project_path) -> bool`
  callback evaluated *before* the expensive `populate_views` API call.
  Workbooks that match the filter are skipped entirely.
- `get_dashboards_list()`: build an `_early_filter` closure from
  `dashboardFilterPattern` / `projectFilterPattern` and pass it to the
  client, so discarded workbooks never pay the `populate_views` cost.

Fixes open-metadata#33441

Prepared with AI assistance (Claude Code, Anthropic), reviewed for correctness before submission.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@zerafachris
zerafachris requested a review from a team as a code owner September 18, 2026 14:23
@github-actions

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

This PR cannot be merged until the following are addressed on its linked issue:

The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically.

Maintainers can bypass this check by adding the skip-pr-checks label.

@github-actions

Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

Comment on lines +168 to +177
def _early_filter(name: str, project_path: str | None) -> bool:
"""Return True if the workbook should be skipped before populate_views."""
return filter_by_dashboard(dashboard_pattern, name) or filter_by_project(
project_pattern, project_path
)

yield from self.client.get_workbooks(
include_owners=self.source_config.includeOwners,
filter_fn=_early_filter,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Bug: Early project filter lacks single-name fallback, drops workbooks

The downstream project filter in dashboard_service.get_dashboard() computes project_name from get_project_name() (the workbook's immediate project.name) and only overrides it with the dotted parent path when get_project_names() returns a non-empty value. The new _early_filter instead passes only project_path (the result of get_project_parents_by_id) to filter_by_project, with no fallback. When path resolution returns None — e.g. get_all_projects() raised and left all_projects empty, or the workbook's project is missing from the fetched dict — filter_by_project(pattern, None) returns True for any configured pattern (see filters.py line 58-60), so the workbook is skipped early. The original pipeline would have kept it by falling back to the immediate project name. This silently drops dashboards precisely in the filtered large-deployment case this PR targets.

Only apply the project filter early when the parent path actually resolved, matching the downstream fallback so no workbook is wrongly skipped.:

def _early_filter(name: str, project_path: str | None) -> bool:
    """Return True if the workbook should be skipped before populate_views."""
    if filter_by_dashboard(dashboard_pattern, name):
        return True
    # Mirror dashboard_service: only skip on project when the path resolved.
    # A None path downstream falls back to the immediate project name, so
    # skipping here would drop workbooks that would otherwise be kept.
    if project_path is not None and filter_by_project(project_pattern, project_path):
        return True
    return False
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Sep 18, 2026

Copy link
Copy Markdown
Code Review ⚠️ Changes requested 0 closed / 1 findings

🟡 Medium risk

Optimizes Tableau ingestion by building projects as a dict for O(1) lookups and filtering workbooks before the expensive populate_views call, but the early project filter lacks a fallback to the workbook's immediate project name, causing dashboards to be silently dropped when path resolution fails — a critical issue for the filtered large-deployment case this targets.

⚠️ Bug: Early project filter lacks single-name fallback, drops workbooks

📄 ingestion/src/metadata/ingestion/source/dashboard/tableau/metadata.py:168-177 📄 ingestion/src/metadata/ingestion/source/dashboard/tableau/client.py:239-242

The downstream project filter in dashboard_service.get_dashboard() computes project_name from get_project_name() (the workbook's immediate project.name) and only overrides it with the dotted parent path when get_project_names() returns a non-empty value. The new _early_filter instead passes only project_path (the result of get_project_parents_by_id) to filter_by_project, with no fallback. When path resolution returns None — e.g. get_all_projects() raised and left all_projects empty, or the workbook's project is missing from the fetched dict — filter_by_project(pattern, None) returns True for any configured pattern (see filters.py line 58-60), so the workbook is skipped early. The original pipeline would have kept it by falling back to the immediate project name. This silently drops dashboards precisely in the filtered large-deployment case this PR targets.

Only apply the project filter early when the parent path actually resolved, matching the downstream fallback so no workbook is wrongly skipped.
def _early_filter(name: str, project_path: str | None) -> bool:
    """Return True if the workbook should be skipped before populate_views."""
    if filter_by_dashboard(dashboard_pattern, name):
        return True
    # Mirror dashboard_service: only skip on project when the path resolved.
    # A None path downstream falls back to the immediate project name, so
    # skipping here would drop workbooks that would otherwise be kept.
    if project_path is not None and filter_by_project(project_pattern, project_path):
        return True
    return False
🤖 Prompt for agents
Code Review: Optimizes Tableau ingestion by building projects as a dict for O(1) lookups and filtering workbooks before the expensive `populate_views` call, but the early project filter lacks a fallback to the workbook's immediate project name, causing dashboards to be silently dropped when path resolution fails — a critical issue for the filtered large-deployment case this targets.

1. ⚠️ Bug: Early project filter lacks single-name fallback, drops workbooks
   Files: ingestion/src/metadata/ingestion/source/dashboard/tableau/metadata.py:168-177, ingestion/src/metadata/ingestion/source/dashboard/tableau/client.py:239-242

   The downstream project filter in `dashboard_service.get_dashboard()` computes `project_name` from `get_project_name()` (the workbook's immediate `project.name`) and only overrides it with the dotted parent path when `get_project_names()` returns a non-empty value. The new `_early_filter` instead passes only `project_path` (the result of `get_project_parents_by_id`) to `filter_by_project`, with no fallback. When path resolution returns `None` — e.g. `get_all_projects()` raised and left `all_projects` empty, or the workbook's project is missing from the fetched dict — `filter_by_project(pattern, None)` returns `True` for any configured pattern (see filters.py line 58-60), so the workbook is skipped early. The original pipeline would have kept it by falling back to the immediate project name. This silently drops dashboards precisely in the filtered large-deployment case this PR targets.

   Fix (Only apply the project filter early when the parent path actually resolved, matching the downstream fallback so no workbook is wrongly skipped.):
   def _early_filter(name: str, project_path: str | None) -> bool:
       """Return True if the workbook should be skipped before populate_views."""
       if filter_by_dashboard(dashboard_pattern, name):
           return True
       # Mirror dashboard_service: only skip on project when the path resolved.
       # A None path downstream falls back to the immediate project name, so
       # skipping here would drop workbooks that would otherwise be kept.
       if project_path is not None and filter_by_project(project_pattern, project_path):
           return True
       return False

Review coverage

Rules No rules evaluated

Functional validation Not enabled · Set up

Auto-approval Not enabled · Set up

Options

Display: compact → Counting what did not apply, without listing it.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source

Comment on lines +240 to +242
project_path = self.get_project_parents_by_id(str(workbook.project_id))
if filter_fn(workbook.name, project_path):
continue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Missing Projects Drop Workbooks

If project discovery fails or a workbook references a project missing from all_projects, get_project_parents_by_id returns None. With an enabled project filter, that missing value is treated as filtered and the workbook is skipped. The previous downstream filter instead fell back to workbook.project_name, so a transient or incomplete project listing can now silently exclude workbooks that should be ingested.

Knowledge Base Used:

Comment on lines +168 to +177
def _early_filter(name: str, project_path: str | None) -> bool:
"""Return True if the workbook should be skipped before populate_views."""
return filter_by_dashboard(dashboard_pattern, name) or filter_by_project(
project_pattern, project_path
)

yield from self.client.get_workbooks(
include_owners=self.source_config.includeOwners,
filter_fn=_early_filter,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Filtered Status Is Lost

Workbooks rejected by this early filter never reach the existing status.filter(...) calls in the downstream dashboard stage. Ingestion summaries therefore no longer count or identify dashboards rejected by dashboard or project patterns, making it harder for operators to understand why records were omitted.

Knowledge Base Used:

Comment on lines +182 to +184
self.all_projects = {
str(project.id): project for project in Pager(self.tableau_server.projects)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Project Cache Is Unbounded

This dictionary eagerly retains every Tableau project without an explicit size limit. That violates the repository directive that all caches must be bounded and that bare dictionaries must not be used as unbounded caches. This requirement must be satisfied before merging.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant