Skip to content

fix: special charcater decoding from datadiff username - #31134

Open
TeddyCr wants to merge 7 commits into
open-metadata:mainfrom
TeddyCr:ISSUE-31124
Open

fix: special charcater decoding from datadiff username#31134
TeddyCr wants to merge 7 commits into
open-metadata:mainfrom
TeddyCr:ISSUE-31124

Conversation

@TeddyCr

@TeddyCr TeddyCr commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Describe your changes:

Fixes #31124

The serviceUrl we hand to data-diff is a canonical SQLAlchemy URL, and SQLAlchemy
percent-encodes the username when it renders one. data-diff's URI parser only decodes the
password, host and query string — never the userinfo username — so an encoded username reaches
the driver still encoded and user@corp.com authenticates as user%40corp.com.

render_url_for_data_diff() renders the URL so every component survives exactly one
encode/decode round trip: the username is handed over decoded (except for :/?#, which must
stay encoded or the authority no longer parses, and which we log a warning about since
data-diff will not decode them back), while the password stays encoded because data-diff
does decode it. TableParameter.data_diff_service_url applies this at the single point where
the URL leaves us; connection dicts pass through untouched. The stored serviceUrl is
unchanged and remains a valid SQLAlchemy URL.

Two related items rode along:

  • Duplicate key detection. data-diff's result model assumes the key is unique — it folds
    rows into a {key: sign} map and asserts each key appears at most twice. A non-unique key
    therefore fails deep inside the library (ValueError: Duplicate primary keys on joindiff, a
    bare AssertionError on hashdiff), or silently inflates counts through the join fan-out.
    None of that tells the user which column is at fault. _validate_key_uniqueness() now runs a
    count/count-distinct on the key columns first and aborts with the table, the column(s), the
    row/distinct-key counts and up to 5 offending values. The check fails open: if it cannot run,
    we log and continue rather than failing an otherwise valid test. The test's WHERE clause is
    applied (it may itself make the key unique); sampling is not (a key unique only within one
    random sample is not a key).
  • Build hygiene. make build-ingestion-base-slim-local was defined under a duplicate
    build-ingestion-base-local target name and so was unreachable. scripts/datamodel_generation.py
    now silences the format of 'X' not understood warnings for the custom format vocabulary
    OpenMetadata owns (queryBuilder, utc-millisec, …) — only those, so a genuinely new or
    misspelled format still surfaces — and passes --formatters black isort explicitly, since
    external formatters are becoming opt-in upstream and the post-processing depends on black's
    output shape.

Type of change:

  • Bug fix

High-level design:

N/A — small change.

Tests:

Use cases covered

  • Table diff on a Snowflake service whose username is an email (user@corp.com) authenticates
    successfully, with both password and private-key auth
  • A user-overridden serviceUrl with special characters is decoded the same way
  • A table diff configured with a non-unique key column aborts with a message naming the table,
    the column and sample duplicated values instead of failing with an opaque AssertionError
  • A key-uniqueness check that cannot run (permissions, unsupported dialect) does not fail the
    test — the diff proceeds

Unit tests

  • I added unit tests for the new/changed logic.
  • Files added:
    • ingestion/tests/unit/observability/data_quality/validations/test_data_diff_url.py
      round-trips every URL component through data-diff's own parser, covers the double-encoding
      regression, reserved-character handling, the warning path, dict pass-through, and end-to-end
      Snowflake serviceUrl construction
    • ingestion/tests/unit/observability/data_quality/validations/table/sqlalchemy/test_table_diff.py
      asserts connect_to_table receives a decoded username on both diff paths without mutating the
      stored URL, plus DuplicateKeyError message shapes, _validate_key_uniqueness ordering/skip
      behaviour, and the fail-open paths
  • Run with make unit_ingestion / pytest ingestion/tests/unit/observability/data_quality

Backend integration tests

  • Not applicable (no backend API changes).

Ingestion integration tests

  • Not applicable — covered by unit tests against data-diff's real URI parser.

  • A user-overridden serviceUrl with special characters is decoded the same way

  • A table diff configured with a non-unique key column aborts with a message naming the table,
    the column and sample duplicated values instead of failing with an opaque AssertionError

  • A key-uniqueness check that cannot run (permissions, unsupported dialect) does not fail the
    test — the diff proceeds

Unit tests

  • I added unit tests for the new/changed logic.
  • Files added:
    • ingestion/tests/unit/observability/data_quality/validations/test_data_diff_url.py
      round-trips every URL component through data-diff's own parser, covers the double-encoding
      regression, reserved-character handling, the warning path, dict pass-through, and end-to-end
      Snowflake serviceUrl construction
    • ingestion/tests/unit/observability/data_quality/validations/table/sqlalchemy/test_table_diff.py
      asserts connect_to_table receives a decoded username on both diff paths without mutating the
      stored URL, plus DuplicateKeyError message shapes, _validate_key_uniqueness ordering/skip
      behaviour, and the fail-open paths
  • Run with make unit_ingestion / pytest ingestion/tests/unit/observability/data_quality

Backend integration tests

  • Not applicable (no backend API changes).

Ingestion integration tests

  • Not applicable — covered by unit tests against data-diff's real URI parser.

Playwright (UI) tests

  • Not applicable (no UI changes).

Manual testing performed

  1. Configured a Snowflake service with an email-style username and ran a tableDiff test —
    previously failed authentication, now connects.
  2. Ran a tableDiff on a table with a duplicated key column — test result is Aborted with the
    column name and sample duplicate values in the result message.
  3. Ran make generate — no format not understood warnings, generated models unchanged.
  4. Ran make build-ingestion-slim-local — builds the slim image.

UI screen recording / screenshots:

Not applicable.

Checklist:

  • I have read the CONTRIBUTING document.
  • My PR title is Fixes <issue-number>: <short explanation>
  • My PR is linked to a GitHub issue via Fixes #31124 above.
  • I have commented on my code, particularly in hard-to-understand areas.
  • For JSON Schema changes: not applicable.
  • For UI changes: not applicable.
  • I have added tests and listed them above.

Suggested PR title: Fixes #31124: stop double-encoding the username in the data-diff service URL

Two things to verify before posting — I inferred them from the diff rather than running anything: the manual test steps above (I did not run a live Snowflake
diff), and whether you want the duplicate-key detection in this PR at all, since it's a distinct behaviour change from the encoding fix and might read better as
its own issue/PR.

@TeddyCr
TeddyCr requested a review from a team as a code owner August 6, 2026 18:22
Copilot AI lite review requested due to automatic review settings August 6, 2026 18:22
Comment thread ingestion/src/metadata/data_quality/validations/table/sqlalchemy/tableDiff.py Outdated
Comment thread Makefile Outdated

Copilot AI left a comment

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI review requested due to automatic review settings August 6, 2026 19:17

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

ingestion/src/metadata/data_quality/validations/utils.py:69

  • This percent-encoding implementation uses ord(char) and emits a single %XX sequence per character. That is not valid for non-ASCII usernames (URI percent-encoding must be based on UTF-8 bytes), and it also leaves raw % characters untouched, which can produce invalid/incomplete percent-escape sequences in the rendered URI. A more robust approach is to use urllib.parse.quote over a UTF-8 string and explicitly control the safe set; if some characters cannot be safely unescaped by data-diff (username is not decoded), consider encoding them and emitting a warning similar to the reserved-character warning.
def _encode_username_for_data_diff(username: str) -> str:
    """Percent-encode only what data-diff's URI parser needs to locate the userinfo boundaries."""
    return "".join(f"%{ord(char):02X}" if char in USERNAME_RESERVED_CHARACTERS else char for char in username)

ingestion/src/metadata/data_quality/validations/table/sqlalchemy/tableDiff.py:676

  • _table_with_duplicate_keys assumes table_diff_iter.result_list is always present and iterable. If result_list is None/unset for a given DiffResultWrapper (or if a different failure path triggers the AssertionError), this will raise a secondary exception and mask the original error. Consider guarding with a falsy check (e.g., treat missing result_list as 'unknown table' and return None) before iterating.
        for sign, values in table_diff_iter.result_list:
            marker = (sign, tuple(values[:key_length]))
            if marker in seen:
                return self.runtime_params.table1 if sign == "-" else self.runtime_params.table2
            seen.add(marker)
        return None

Comment thread ingestion/src/metadata/data_quality/validations/utils.py
Copilot AI review requested due to automatic review settings August 11, 2026 13:57
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

✅ PR checks passed

The linked issue has a description and all required Shipping project fields set. Thanks!

@github-actions github-actions Bot added Ingestion safe to test Add this label to run secure Github workflows on PRs labels Aug 11, 2026
@TeddyCr TeddyCr added the To release Will cherry-pick this PR into the release branch label Aug 11, 2026

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (1)

ingestion/src/metadata/data_quality/validations/table/sqlalchemy/tableDiff.py:637

  • The PR description says _validate_key_uniqueness() now runs a COUNT/COUNT DISTINCT (and reports duplicated values) before diffing, but the code here explicitly states the opposite (no up-front uniqueness check) and there is no _validate_key_uniqueness implementation/call in this module. Please either (a) update the PR description/tests expectations to match the implemented behavior (re-wrapping data-diff’s duplicate-key errors), or (b) implement and invoke the up-front uniqueness check as described.
    @contextmanager
    def _duplicate_keys_named(self, table_diff_iter: DiffResultWrapper) -> Iterator[None]:
        """Re-raise data-diff's duplicate-key failures with the key columns named.

        A key that is not unique makes a row-level diff undefined, and data-diff reports it in two
        equally opaque ways: joindiff validates the key itself and raises
        `ValueError("Duplicate primary keys")`, while hashdiff only trips over it in `_get_stats`,
        which folds rows into a `{key: sign}` map and asserts a key never repeats with the same
        sign - a bare `AssertionError`. Neither names the key, so we do.

        We deliberately do not check uniqueness up front: that is a COUNT/COUNT DISTINCT over both
        tables on every run, far too expensive on a large table to pay for an error that only
        happens when the key is misconfigured. Both paths here are reached only once the diff has
        already failed, and neither queries anything.

IceS2
IceS2 previously approved these changes Aug 11, 2026
Re-indenting _run under the duplicate-key context manager shifted the
baselined diagnostics off their columns, so five of them resurfaced.
Narrow threshold once, guard the row-count division, and pass column_diff
by keyword - it was landing in the changed row count slot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 11, 2026 20:59
@github-actions github-actions Bot added safe to test Add this label to run secure Github workflows on PRs and removed safe to test Add this label to run secure Github workflows on PRs labels Aug 11, 2026

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (2)

ingestion/src/metadata/data_quality/validations/table/sqlalchemy/tableDiff.py:175

  • DuplicateKeyError's docstring says the message "names the key columns only", but the exception message also includes the table name when available (location = f"in {table}"). This makes the docstring misleading for readers and for anyone grepping for what gets surfaced to users.
class DuplicateKeyError(Exception):
    """A diff key column is not unique, which makes a row-level diff undefined.

    The message names the key columns only. The duplicated values themselves are row data, which
    this test result is not the place to publish, and finding them costs a scan of the table.
    """

ingestion/src/metadata/data_quality/validations/table/sqlalchemy/tableDiff.py:641

  • The PR description says a new up-front key-uniqueness check (_validate_key_uniqueness()) runs COUNT/COUNT DISTINCT and aborts with row/distinct counts and sample offending values. In the current implementation, duplicate keys are only detected/reported after data-diff fails, and the raised DuplicateKeyError does not include counts or offending values.

Either implement the described up-front validation (and tests), or adjust the PR description/test plan to match the current behavior so users know what to expect.

    @contextmanager
    def _duplicate_keys_named(self, table_diff_iter: DiffResultWrapper) -> Iterator[None]:
        """Re-raise data-diff's duplicate-key failures with the key columns named.

        A key that is not unique makes a row-level diff undefined, and data-diff reports it in two
        equally opaque ways: joindiff validates the key itself and raises
        `ValueError("Duplicate primary keys")`, while hashdiff only trips over it in `_get_stats`,
        which folds rows into a `{key: sign}` map and asserts a key never repeats with the same
        sign - a bare `AssertionError`. Neither names the key, so we do.

        We deliberately do not check uniqueness up front: that is a COUNT/COUNT DISTINCT over both
        tables on every run, far too expensive on a large table to pay for an error that only
        happens when the key is misconfigured. Both paths here are reached only once the diff has
        already failed, and neither queries anything.
        """

Copilot AI review requested due to automatic review settings August 12, 2026 00:32
@github-actions github-actions Bot added safe to test Add this label to run secure Github workflows on PRs and removed safe to test Add this label to run secure Github workflows on PRs labels Aug 12, 2026
@gitar-bot

gitar-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 2 resolved / 2 findings

Decodes usernames in data-diff service URLs and adds pre-flight key uniqueness validation, addressing the separator-less Concat and .PHONY target findings. No issues found.

✅ 2 resolved
Edge Case: Separator-less Concat can flag valid composite keys as duplicates

📄 ingestion/src/metadata/data_quality/validations/table/sqlalchemy/tableDiff.py:679-683 📄 ingestion/src/metadata/data_quality/validations/table/sqlalchemy/tableDiff.py:696-708
In _count_keys, distinct keys are counted via Count(Concat(this[list(segment.key_columns)]), distinct=True) with no separator. For a composite key, concatenating without a delimiter conflates distinct tuples (e.g. ('ab','c') and ('a','bc') both become 'abc'), and any NULL component makes the whole Concat NULL so COUNT(DISTINCT ...) drops those rows. Either case makes distinct_keys < total_rows for a genuinely unique key, raising DuplicateKeyError and aborting an otherwise valid diff — contradicting the intended fail-open behaviour. Pass a separator that cannot appear in the data (e.g. Concat([...], sep='|')) and/or coalesce NULLs before concatenation.

Quality: .PHONY name doesn't match renamed slim build target

📄 Makefile:266-267
The .PHONY declaration still names build-ingestion-base-slim-local, but the recipe below it was renamed to build-ingestion-slim-local. As a result the new target is not declared phony (and the phony name refers to a target that no longer exists). If a file named build-ingestion-slim-local ever exists, the target would be skipped. Update the .PHONY line to build-ingestion-slim-local.

Options

Display: compact → Showing less information.

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

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (1)

ingestion/src/metadata/data_quality/validations/table/sqlalchemy/tableDiff.py:640

  • The PR description says duplicate-key detection was added via an upfront _validate_key_uniqueness() COUNT/COUNT DISTINCT that (a) aborts with table/column counts + sample values and (b) “fails open” if it can’t run. The current implementation explicitly does not do an upfront uniqueness check (docstring here) and instead only re-labels data-diff’s downstream ValueError("Duplicate primary keys") / AssertionError, then aborts the test via DuplicateKeyError handling.

Please align the PR description with the shipped behavior, or implement the described upfront uniqueness check + fail-open semantics if that’s the intended user-facing change (especially for cases that don’t raise but may silently inflate counts due to join fan-out).

    @contextmanager
    def _duplicate_keys_named(self, table_diff_iter: DiffResultWrapper) -> Iterator[None]:
        """Re-raise data-diff's duplicate-key failures with the key columns named.

        A key that is not unique makes a row-level diff undefined, and data-diff reports it in two
        equally opaque ways: joindiff validates the key itself and raises
        `ValueError("Duplicate primary keys")`, while hashdiff only trips over it in `_get_stats`,
        which folds rows into a `{key: sign}` map and asserts a key never repeats with the same
        sign - a bare `AssertionError`. Neither names the key, so we do.

        We deliberately do not check uniqueness up front: that is a COUNT/COUNT DISTINCT over both
        tables on every run, far too expensive on a large table to pay for an error that only
        happens when the key is misconfigured. Both paths here are reached only once the diff has
        already failed, and neither queries anything.

@sonarqubecloud

Copy link
Copy Markdown

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

Labels

Ingestion safe to test Add this label to run secure Github workflows on PRs To release Will cherry-pick this PR into the release branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Double encoding in data diff

3 participants