Fixes 29542: make the testDefinitions entityType filter case-insensitive on both the REST and MCP doors - #31141
Fixes 29542: make the testDefinitions entityType filter case-insensitive on both the REST and MCP doors#31141TeddyCr wants to merge 7 commits into
Conversation
…n-metadata#29542) GET /v1/dataQuality/testDefinitions forwarded the raw entityType query param into the ListFilter, and TestDefinitionDAO compares it against the test_definition.entityType generated column with `=`. PostgreSQL evaluates that byte-exactly, so entityType=Column silently returned an empty page, while MySQL's utf8mb4_0900_ai_ci collation happened to match it. The behaviour was therefore DB-flavour dependent, not privilege dependent -- contrary to the issue's non-admin/authorizer theory, listInternal runs the same ListFilter for every caller and DefaultAuthorizer.authorize only gates, it never rewrites the query. Canonicalize the value at the REST boundary through the schema enum TestDefinitionEntityType so both engines behave identically without relying on collation, without an UPPER() wrapper on the generated column, and without a schema change or migration. An unrecognized value now returns 400 naming the valid values instead of a silently empty page, matching the parseIncidentStatus convention in TestCaseResolutionStatusResource. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ity (open-metadata#29542) Adds three integration tests to TestDefinitionResourceIT, each verified to fail against the pre-fix code on PostgreSQL: - COLUMN/Column/column all return a COLUMN-only page containing a COLUMN definition the test created and excluding a TABLE one it created. - An unknown entityType returns 400 naming the rejected value instead of 200 with an empty page. - An admin and a freshly created role-less user get the same results for the same query, retiring the issue's non-admin-bypass theory. Assertions are by membership rather than set equality because test_definition is a global collection and this class runs with ExecutionMode.CONCURRENT. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n-metadata#29542) Review follow-up. Two problems with the first cut: - `entityType != null` was true for the empty string, so `?entityType=` started returning 400 where it used to return 200 with an empty page. No in-repo caller emits it, but an external client that serializes an unset filter would have broken. Guard with `CommonUtil.nullOrEmpty` so a blank value is an absent filter; `Banana` still 400s. - `TestDefinitionsTool` is a second door into the same DAO and still bound the raw value, so after the first cut the two doors disagreed on both success and failure semantics for the same input: an LLM calling `list_test_definitions(entityType="Column")` got 0 rows silently while the REST door returned 400. Move the canonicalization to `TestDefinitionRepository.addEntityTypeFilter` so both callers share one implementation and cannot drift. Also document the 400 on the list operation so the generated OpenAPI spec describes it, and derive the valid-value list and the lookup from one constant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tadata#29542) - `list_emptyEntityTypeFilterIsIgnored_200_OK` pins that `?entityType=` returns 200 with both TABLE and COLUMN definitions, so the blank-value regression cannot come back. - `McpToolsValidationIT.testGetTestDefinitionsForMixedCaseEntityType` pins that the MCP tool called with `"Column"` returns a non-empty page of COLUMN definitions. The pre-existing `testGetTestDefinitionsForColumn` could not catch this: it asserts only that `data` is an array. - Rename `...ForAdminAndNonAdmin` to `...ForAdminAndRoleLessUser` and add a JavaDoc: it proves parity for a role-less caller only, not for the `DomainOnlyAccessRole`-with-no-domains config that genuinely diverges via `EntityUtil.addDomainQueryParam`. Both new tests were verified to fail without their respective fixes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n-metadata#29542) Review round 2. - `addEntityTypeFilter` now trims before deciding, matching the in-repo precedent `TestCaseResolutionStatusResource.parseIncidentStatus`. This makes the blank boundary consistent: `?entityType=%20` is an absent filter rather than a 400, and `?entityType=%20Column%20` resolves. - `tools.json` advertised the MCP `entityType` argument as free text. Now that a bad value throws instead of returning an empty page, declare `"enum": ["TABLE", "COLUMN"]` so a model does not emit one at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…pen-metadata#29542) - `assertColumnOnlyListing` now also asserts `paging.total > 0`. `paging.total` comes from `TestDefinitionDAO.listCount`, which carries its own copy of the `AND entityType=:entityType` condition, so the count path is now covered for real rather than by construction. - Casings gain `" Column "` and blanks gain `" "`, pinning the trim. - `testGetTestDefinitionsForMixedCaseEntityType` moves from the duplicated `@Order(11)` to `@Order(22)`. `MethodOrderer.OrderAnnotation` leaves ties unspecified, and that class is SAME_THREAD and ordered because later tests consume earlier state, so a tie is a latent flake. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a case-sensitivity bug in the testDefinitions listing flow by canonicalizing the entityType filter at the boundary (shared by both the REST resource and the MCP tool), ensuring consistent behavior across PostgreSQL/MySQL and preventing silent empty results for mixed-case inputs.
Changes:
- Centralized
entityTypecanonicalization inTestDefinitionRepository.addEntityTypeFilter(...)and reused it from both REST and MCP entry points. - Added REST OpenAPI documentation for a
400response whenentityTypeis invalid. - Added integration tests covering case-insensitive filtering, blank handling, invalid values (400), admin vs role-less parity, and MCP mixed-case behavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestDefinitionResource.java | Uses shared helper to canonicalize entityType and documents 400 response. |
| openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestDefinitionRepository.java | Adds shared entityType parsing/canonicalization helper with 400-on-invalid behavior. |
| openmetadata-mcp/src/main/java/org/openmetadata/mcp/tools/TestDefinitionsTool.java | Reuses the shared helper so MCP and REST cannot drift. |
| openmetadata-mcp/src/main/resources/json/data/mcp/tools.json | Adds an enum constraint for entityType in the MCP tool descriptor. |
| openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/TestDefinitionResourceIT.java | Adds IT coverage for REST casing/blank/invalid/admin-vs-nonadmin behaviors. |
| openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/mcp/McpToolsValidationIT.java | Adds IT coverage for MCP mixed-case entityType returning COLUMN definitions. |
❌ PR checklist incompleteThis 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 |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Code Review ✅ Approved 2 resolved / 2 findingsMakes the testDefinitions entityType filter case-insensitive across both REST and MCP endpoints by normalizing values against the enum. However, the changes to tools.json introduced a syntax error due to a missing comma. ✅ 2 resolved✅ Quality: enum constraint dropped, weakening model guidance
✅ Bug: Missing comma makes tools.json invalid JSON
OptionsDisplay: compact → Showing less information. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Gitar | Powered by Gitar — free for open source |
Describe your changes:
Fixes #29542
I worked on the
testDefinitionslist endpoint because itsentityTypequery parameter was compared with a raw, unnormalized SQL string bind, so?entityType=Columnreturned 0 results while?entityType=COLUMNreturned the full set.Root cause.
TestDefinitionResourceforwarded the raw parameter into aListFilter, andCollectionDAO.TestDefinitionDAOcompares it with=against thetest_definition.entityTypegenerated column in three places —listBefore(:9000-9003),listAfter(:9069-9072) andlistCount(:9138-9141).It is DB-flavour dependent, which is worth knowing when reproducing: PostgreSQL string equality is byte-exact under the deterministic collations it ships with, so it always reproduces; MySQL ships
COLLATE=utf8mb4_0900_ai_ci(case-insensitive), so it typically does not. The fix deliberately does not rely on DB collation, so behaviour is now identical on both engines. Both are covered by tests, run on both dialects.Approach — normalize at the REST boundary, not in SQL. A new
TestDefinitionRepository.addEntityTypeFilter(ListFilter, String)trims the value, matches it case-insensitively against the generatedTestDefinitionEntityTypeenum, and stores the canonical.value(); an unrecognised value throwsIllegalArgumentException→ HTTP 400 viaCatalogGenericExceptionMapper:51. This mirrors the existing convention one file over —TestCaseResolutionStatusResource.parseIncidentStatus/parseIncidentGroupBy(:846-885) coerce query params through generated enums with a 400.Rejected:
UPPER(entityType) = UPPER(:entityType)in SQL. It would be six duplicated edits, it would still return a silent empty page for a typo like?entityType=Banana, and it would preclude an index on the PostgreSQL STORED generated column. Note also thatequalsIgnoreCaseis locale-independent by specification, so this avoids the Turkish-İ hazard that atoUpperCase-keyed map only mitigates by convention — there is notoUpperCase/toLowerCaseanywhere in this change.The MCP door is fixed by the same code.
openmetadata-mcp/.../tools/TestDefinitionsTool.java:95wrote the same raw parameter into aListFilterfor the same DAO, so an LLM callinglist_test_definitions(entityType="Column")got 0 rows silently. Both doors now call the one shared helper, so they cannot drift.tools.jsonalso gains"enum": ["TABLE", "COLUMN"]on that parameter so a model is less likely to emit a bad value at all.Two claims in the issue report are not supported by the code, and no change was made on their account:
EntityResource.listInternalauthorizes once as a pass/fail gate and then runs the identicalListFilterfor every caller;DefaultAuthorizer.authorizeonly throws or returns and never mutates the query. Confirmed empirically — in the pre-fix run, admin and a role-less non-admin behaved identically, and the failure appeared on the admin client. A regression test now pins that parity.Column."EntityType.Columnis the TypeScript enum member name; its value is"COLUMN"(generated/tests/testDefinition.ts:254-257), unchanged since1.12.6-release, andplaywright/.../TestDefinitionFilters.spec.tspins the wire values. No UI change is needed.Type of change:
High-level design:
N/A — small change (2 production files, ~+40 lines).
Tests:
Use cases covered
entityType=COLUMN,Column,columnand" Column "all return the same non-empty set, and every entry is a COLUMN definition.entityType=Bananareturns 400 with a message naming the rejected value, instead of a silent empty page.entityType=(blank) andentityType=%20are treated as an absent filter → 200 with both TABLE and COLUMN definitions.get_test_definitionstool returns COLUMN definitions forentityType="Column".Unit tests
Backend integration tests
openmetadata-integration-tests/..../it/tests/TestDefinitionResourceIT.java(4 tests),.../it/tests/mcp/McpToolsValidationIT.java(1 test)TestDefinitionResourceIT+McpToolsValidationIT→ 221 run, 0 failures, 0 errors-DdatabaseType=mysql): 221 run, 0 failures, 0 errorsBanana400 test both fail. Reverting only thenullOrEmptyguard: only the blank test fails. Reverting only the trim:Invalid entityType ' 'andInvalid entityType ' Column '. Reverting only the MCP one-liner:McpToolsValidationIT.testGetTestDefinitionsForMixedCaseEntityTypefails withExpecting actual not to be empty.test_definitionis a global collection and the class is@Execution(CONCURRENT), the tests assert membership of self-created FQNs plusallMatch(entityType == COLUMN)plus non-containment of a self-created TABLE definition — never an exact collection size, which would be flaky.assertColumnOnlyListingalso assertspaging.total > 0, which exercisesTestDefinitionDAO.listCount's own copy of the predicate.Note: the pre-existing
McpToolsValidationIT.testGetTestDefinitionsForColumnasserts only thatdatais an array, never that it is non-empty — so it would have passed against the broken MCP door. The new test asserts non-emptiness and that every element isCOLUMN.Ingestion integration tests
Playwright (UI) tests
TestDefinitionFilters.spec.tsalready pins the wire values the UI sends.Manual testing performed
./docker/run_local_docker.sh -m no-ui -d postgresql(PostgreSQL matters — see the collation note above).export TOKEN=<admin jwt>andexport BASE=http://localhost:8585/api.curl -s -H "Authorization: Bearer $TOKEN" "$BASE/v1/dataQuality/testDefinitions?entityType=COLUMN&limit=1000" | jq '.paging.total'→ non-zero.entityType=Column→ after the fix, the same non-zero count. Onmain:0.entityType=column→ same result.... "?entityType=Column&limit=1000" | jq '[.data[].entityType] | unique'→["COLUMN"].curl -s -o /dev/stderr -w '%{http_code}\n' ... "?entityType=Banana"→ 400, bodyInvalid entityType 'Banana'. Must be one of [TABLE, COLUMN]. Onmain:200with an emptydataarray.curl -s -o /dev/null -w '%{http_code}\n' ... "?entityType=&limit=1000"→ 200, with both TABLE and COLUMN definitions present.get_test_definitionswith{"entityType": "Column"}→ non-emptydata, every element"entityType": "COLUMN".UI screen recording / screenshots:
Not applicable — no UI changes.
Checklist:
Fixes <issue-number>: <short explanation>Fixes #<issue-number>above.tools.jsonis an MCP tool-descriptor resource, not an entity schema.)This PR fixes a real, reproducible defect on this endpoint, but it may not be the whole of what the reporter hit.
The reporter's specific symptom is still unexplained. They reported that granting
testDefinition: ViewAllshows "No data" while grantingAllworks. That is a permission-level difference, and it is explained by neither this casing fix (which is privilege-independent — the pre-fix failure reproduces on the admin client) nor the domain defect below (which requiresDomainOnlyAccessRolewith no domains, not aViewAll-vs-Alldistinction). A third mechanism, most likely inPolicyEvaluator/ResourceContexthandling ofVIEW_ALLversusALL, is not investigated here. Please confirm with the reporter before treating testDefinitions API: entityType query param is case-sensitive, frontend sends mixed case → 0 results for non-admin users #29542 as fully resolved.A separate, unfixed admin/non-admin asymmetry exists on this exact endpoint and should be filed on its own.
EntityUtil.addDomainQueryParam(EntityUtil.java:1093-1103) callsfilter.addQueryParam("entityType", entityType)with the resource type, andFilter.addQueryParamis aput(Filter.java:14-17) — so it overwrites the caller's value. For a non-admin, non-bot user holdingDomainOnlyAccessRolewith no domains, a bareGET /v1/dataQuality/testDefinitions— with noentityTypeparameter at all — is flipped out ofTestDefinitionDAO's all-null fast path into the custom branch, appendingAND entityType='testDefinition'→ 0 rows, always.DocStoreDAO(CollectionDAO.java:11607/11645/11683) andKnowledgePageDAO(:15997/16049/16105) read the same key and are clobbered identically. Fixing it means renaming the domain filter's parameter key acrossListFilterand every caller, which is well outside this bug fix.Behaviour change:
?entityType=<unrecognised>now returns 400 where it previously returned 200 with an empty page. Blank and whitespace-only values are exempt and still behave as an absent filter. One in-repo caller can reach the new 400 —useTestDefinitionData.ts:66casts a URL query token unchecked, so a hand-edited or bookmarked/test-library?entityType=Tablesnow shows an error toast instead of an empty table. That loud failure is deliberate; the same path also gains correct behaviour, since?entityType=columnnow works where it silently returned nothing.before-cursor (reverse) paging under theentityTypefilter is not directly asserted; it shares the identical canonical bind value, so it is covered by construction.🤖 Generated with Claude Code