Skip to content

fix: special charcater decoding from datadiff username - #31134

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

fix: special charcater decoding from datadiff username#31134
TeddyCr wants to merge 3 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 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
@gitar-bot

gitar-bot Bot commented Aug 6, 2026

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

Fixes special character decoding in data-diff usernames by properly rendering the SQLAlchemy URL, adds pre-flight duplicate key validation to surface table and column errors, and fixes the Makefile target definition. 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 👍 / 👎 | Gitar | 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 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 on lines +91 to +92
if url.password is not None:
userinfo += f":{quote(str(url.password), safe=' +')}"
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.

Double encoding in data diff

2 participants