Skip to content

fix(mssql,azuresql): keep OpenMetadata's own queries out of usage and lineage - #31163

Open
IceS2 wants to merge 3 commits into
mainfrom
fix/mssql-azuresql-query-header-inline
Open

fix(mssql,azuresql): keep OpenMetadata's own queries out of usage and lineage#31163
IceS2 wants to merge 3 commits into
mainfrom
fix/mssql-azuresql-query-header-inline

Conversation

@IceS2

@IceS2 IceS2 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

SQL Server reports the connector's own reflection SQL back as user queries. There are two
independent defects behind it, one per code path, and fixing either alone changes nothing.

Query Store never sees the header

sys.query_store_query_text stores one row per statement, and a leading comment belongs to
the batch rather than to any statement, so SQL Server discards it:

sent     : /* {"app": "OpenMetadata", ...} */ SELECT 1 AS m FROM sales.orders
recorded : SELECT 1 AS m FROM sales.orders

No pattern can match a string that was never stored. Sending the header after the first token
keeps it inside the statement. This is why Vertica already has an override — though there the
history table drops the comment outright, which is a different failure with the same remedy.

Unlike Vertica's statement.split(" "), the SQL Server version splits on the first
non-whitespace token: 17 of the MSSQL query constants are textwrap.dedent strings that open
with a newline, and for those the naive split lands the comment between two whitespace runs —
still ahead of the first keyword, still discarded, and indistinguishable from success.

The plan cache keeps the header, but not at position 0

sp_executesql puts the parameter declarations in front of the statement:

(@P1 NVARCHAR(MAX),@P2 NVARCHAR(MAX))/* {"app": "OpenMetadata", ...} */ WITH fk_info AS (...

The four usage, lineage and stored-procedure filters anchored the header at position 0, so they
missed every parameterised statement while working correctly for unparameterised ones. The
patterns now match the header anywhere in the text.

Azure SQL was never tagging anything

AzuresqlUsageSource extends MssqlUsageSource and shares these queries, but
azureSQLConnection.json never declared supportsQueryComment, so the
hasattr(connection, "supportsQueryComment") gate in create_generic_db_connection skipped
header injection entirely. It advertised supportsUsageExtraction while being unable to
identify its own queries. Declaring the field turns tagging on.

Verification

Against SQL Server 2022 on both pytds and pyodbc, the header now survives into Query Store and
the plan cache for bare, dedented, CTE and parameterised statements, and none of them reach the
parser. The integration test asserts this through the server rather than by inspecting strings —
a header at "\n /* ... */ SELECT" looks correct, parses fine, and is still discarded.

Metadata, usage, profiler and auto-classification pipelines were run end to end against a local
SQL Server for both connectors.

Cost

The unanchored predicate roughly doubles the cost of that one query — 338 ms to 662 ms measured
over 16 089 distinct query texts with a window covering all of them. It runs once per usage run
per database. No index is affected: query_sql_text and dm_exec_sql_text.text are
nvarchar(max), which SQL Server cannot use as an index key.

Not covered

The dialect's own bootstrap queries (fn_listextendedproperty, sys.system_views, the
isolation-level probe) run on the raw DBAPI connection, outside before_cursor_execute, so no
injection strategy can reach them. Excluding those needs a system-object deny-list, which is a
separate change. Query Store rows recorded before this fix stay untagged and age out with
retention.

… lineage

SQL Server reported the connector's own reflection SQL back as user queries.
Two independent defects had to be fixed together; either one alone leaves the
behaviour unchanged.

sys.query_store_query_text stores one row per statement, and a leading comment
belongs to the batch rather than to any statement, so the header was discarded
before it could ever be matched. Sending it after the first token keeps it
inside the statement. This is the same reason Vertica already has an override,
though there the history table drops the comment outright.

The plan-cache path did keep the header, but sp_executesql puts the parameter
declarations in front of it, so the position-0 anchor in the four usage,
lineage and stored-procedure filters missed every parameterised statement. The
patterns now match the header anywhere in the text.

Azure SQL reuses MssqlUsageSource and so shares those queries, but its schema
never declared supportsQueryComment, which left create_generic_db_connection
skipping header injection for it entirely. Declaring the field turns tagging on.

Verified against SQL Server 2022 on both pytds and pyodbc: the header now
survives into Query Store and the plan cache for bare, dedented, CTE and
parameterised statements, and none of them reach the parser.
Copilot AI review requested due to automatic review settings August 7, 2026 07:26
@IceS2
IceS2 requested a review from a team as a code owner August 7, 2026 07:26

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 was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

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

  • No GitHub issue is linked. Link an issue in the Development section of the PR (or add Fixes #12345 to the description). For a same-org cross-repo issue, add Fixes open-metadata/<repo>#123 to the description.

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 github-actions Bot added Ingestion safe to test Add this label to run secure Github workflows on PRs labels Aug 7, 2026
Comment on lines +73 to +80
stripped = statement.lstrip()
first_token = FIRST_TOKEN.match(stripped)
if not first_token or stripped.startswith("/*"):
return statement
leading_whitespace = statement[: len(statement) - len(stripped)]
token = first_token.group(0)
header = render_query_header(_pkg_version("openmetadata-ingestion"))
return f"{leading_whitespace}{token} {header}{stripped[len(token) :]}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Edge Case: Inline header injected inside a leading line comment (--)

inject_inline_query_header only skips statements whose first non-whitespace is a block comment (stripped.startswith("/*")). If a statement begins with a -- line comment (e.g. -- note SELECT ...), the first token is -- and the header is placed right after it: -- /* {...} */ note SELECT ..., so the header sits inside the line comment and is dropped — the exact failure this PR fixes. This is unlikely for OpenMetadata's own reflection queries today, so it is a latent gap rather than a live defect; if it matters, also guard stripped.startswith("--") (returning the statement unchanged) or inject after the first real keyword.

Leave statements starting with a line comment untouched, matching the existing block-comment guard.:

stripped = statement.lstrip()
first_token = FIRST_TOKEN.match(stripped)
if not first_token or stripped.startswith(("/*", "--")):
    return statement
  • Apply fix

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

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

✅ TypeScript Types Auto-Updated

The generated TypeScript types have been automatically updated based on JSON schema changes in this PR.

Copilot AI review requested due to automatic review settings August 7, 2026 07:31
@github-actions
github-actions Bot requested a review from a team as a code owner August 7, 2026 07:31

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 was unable to review this pull request because the user who requested the review has reached their quota limit.

@sonarqubecloud

sonarqubecloud Bot commented Aug 7, 2026

Copy link
Copy Markdown

Copilot AI review requested due to automatic review settings August 7, 2026 12:22

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 was unable to review this pull request because the user who requested the review has reached their quota limit.

@gitar-bot

gitar-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown
Code Review 👍 Approved with suggestions 0 resolved / 1 findings

Updates MSSQL and Azure SQL connectors to correctly inject and match query header comments, preventing internal reflection queries from polluting usage and lineage data. Consider ensuring that inline header injection safely handles statements starting with leading line comments.

💡 Edge Case: Inline header injected inside a leading line comment (--)

📄 ingestion/src/metadata/ingestion/connections/headers.py:73-80

inject_inline_query_header only skips statements whose first non-whitespace is a block comment (stripped.startswith("/*")). If a statement begins with a -- line comment (e.g. -- note SELECT ...), the first token is -- and the header is placed right after it: -- /* {...} */ note SELECT ..., so the header sits inside the line comment and is dropped — the exact failure this PR fixes. This is unlikely for OpenMetadata's own reflection queries today, so it is a latent gap rather than a live defect; if it matters, also guard stripped.startswith("--") (returning the statement unchanged) or inject after the first real keyword.

Leave statements starting with a line comment untouched, matching the existing block-comment guard.
stripped = statement.lstrip()
first_token = FIRST_TOKEN.match(stripped)
if not first_token or stripped.startswith(("/*", "--")):
    return statement
🤖 Prompt for agents
Code Review: Updates MSSQL and Azure SQL connectors to correctly inject and match query header comments, preventing internal reflection queries from polluting usage and lineage data. Consider ensuring that inline header injection safely handles statements starting with leading line comments.

1. 💡 Edge Case: Inline header injected inside a leading line comment (--)
   Files: ingestion/src/metadata/ingestion/connections/headers.py:73-80

   inject_inline_query_header only skips statements whose first non-whitespace is a block comment (`stripped.startswith("/*")`). If a statement begins with a `--` line comment (e.g. `-- note
   SELECT ...`), the first token is `--` and the header is placed right after it: `-- /* {...} */ note
   SELECT ...`, so the header sits inside the line comment and is dropped — the exact failure this PR fixes. This is unlikely for OpenMetadata's own reflection queries today, so it is a latent gap rather than a live defect; if it matters, also guard `stripped.startswith("--")` (returning the statement unchanged) or inject after the first real keyword.

   Fix (Leave statements starting with a line comment untouched, matching the existing block-comment guard.):
   stripped = statement.lstrip()
   first_token = FIRST_TOKEN.match(stripped)
   if not first_token or stripped.startswith(("/*", "--")):
       return statement

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

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Jest test Coverage

UI tests summary

Lines Statements Branches Functions
Coverage: 66%
66.22% (78162/118024) 50.22% (47203/93985) 51.44% (14226/27652)

@sonarqubecloud

sonarqubecloud Bot commented Aug 7, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

🔴 Playwright Results — workflow failed

Validated commit 3942b3d34ea881964d9641b9044d0b063dc8b426 in Playwright run 31177890068, attempt 1.

✅ 670 passed · ❌ 0 failed · 🟡 0 flaky · ⏭️ 0 skipped · 🧰 0 lifecycle flaky

Pipeline and setup failures (1)

  • Playwright performance gate Maximum shard-job elapsed before upload failed (target ≤ 1800 s) — exceeded on 1 shard(s): chromium-03 1972 s.

Performance

Blocking targets: ❌ unmet · Optimization targets: 🟡 in progress

Shard-job maxima below are not the full workflow wall time; the linked run includes build, fixture, planning, and reporting.

🕒 Full workflow signal wall (to summary) 1h 5m 56s

⏱️ Max setup 2m 58s · max shard execution 18m 59s · max shard-job elapsed before upload 32m 52s · reporting 7s

🌐 211.76 requests/attempt · 2.66 app boots/UI scenario · 5.88% common-shard skew

Optimization targets still in progress:

  • Browser traffic was 211.76 requests per attempt (convergence target: fewer than 200).
  • Application boot ratio was 2.66 per UI scenario (1864 boots / 700 scenarios; convergence target: at most 1).
Shard Passed Failed Flaky Skipped Lifecycle failed Lifecycle flaky
✅ Shard chromium-01 148 0 0 0 0 0
✅ Shard chromium-02 147 0 0 0 0 0
✅ Shard chromium-03 161 0 0 0 0 0
✅ Shard chromium-04 152 0 0 0 0 0
✅ Shard ingestion-01 22 0 0 0 0 0
✅ Shard ingestion-02 40 0 0 0 0 0

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants