Repository navigation
Test case PUT-create and CSV import skip TestCaseRepository.prepare's create-time checks #35667
Description
Affected module
Backend
Describe the bug
TestCaseRepository.prepare holds every create-time check for test cases: the disabled-definition gate (if (!update && …getEnabled())), validateTestParameters, validateColumnTestCase, validateDimensionColumns and setDataQualityDimension. Two create paths don't reach prepare correctly, so these checks are skipped:
Gap 1: PUT that creates a test case runs prepare with update = true
TestCaseResource.createOrUpdate calls repository.prepareInternal(test, true) unconditionally (TestCaseResource.java:893), before it knows whether the row exists. It then calls repository.createOrUpdate, which creates through createNewEntity and never prepares again (EntityRepository.java:4305-4313). As a result, every if (!update) gate in TestCaseRepository.prepare is skipped when a test case is created by PUT, which is how ingestion (dbt, Great Expectations, the SDKs) creates test cases. Today that skips the disabled-definition check (TestCaseRepository.java:888).
EntityResource.createOrUpdate doesn't have this problem: it routes creates through repository.create, which prepares with false. MetricResource.preauthorizeHierarchyUpdate and EntityCsv.createEntity both look the row up first.
Gap 2: the test case CSV import never calls prepare
TestCaseCsv.createEntity (TestCaseRepository.java:2503) creates and updates through createOrUpdateForImport, which calls neither prepare nor prepareInternal. The code says so itself at TestCaseRepository.java:2596-2597. The generic EntityCsv.createEntity does call it, with prepareInternal(entity, isUpdate). As a result:
- A real (non-dry-run) import runs none of the checks above, for new rows or for updates.
- A dry run calls
prepareInternal(testCase, true)for every row (TestCaseRepository.java:2622). It checks parameters but skips the create-only gates. Dry run and real import can therefore disagree on the same file: a row that failsvalidateTestParametersfails the dry run but is stored by the real import. repository.getOrCreateTestSuite(testCase)(TestCaseRepository.java:2583) runs before any check and also during a dry run, so dry runs and rejected rows can create basic test suites.
To Reproduce
Gap 1:
- Disable a test definition: PATCH
/v1/dataQuality/testDefinitions/{id}, settingenabledtofalse. - POST
/v1/dataQuality/testCaseswith that definition. Result: 400, "Test definition '…' is disabled and cannot be used to create test cases". - PUT
/v1/dataQuality/testCaseswith the same body and a new name. Result: 201, created.
Gap 2:
- Import a test case CSV (
PUT /v1/dataQuality/testCases/name/{name}/import?dryRun=false) with a new row that uses the disabled definition, or that omits a required parameter. Result: the row is created. - Run the same file with
dryRun=true. Result: the missing-parameter row fails and the disabled-definition row passes.
Expected behavior
Every create path runs TestCaseRepository.prepare with update = false, and every update path runs it with update = true. A test case is then held to the same checks whether it's created by POST, PUT, createMany or CSV import, and a dry run reports what the real import would do.
What needs to change
-
TestCaseResource.createOrUpdate(PUT). Replacerepository.prepareInternal(test, true)with a lookup, then pass the real value:repository.setFullyQualifiedName(test); boolean exists = repository.findByNameOrNull(test.getFullyQualifiedName(), Include.ALL) != null; repository.prepareInternal(test, exists);
Use
Include.ALLso the result matches the lookupEntityRepository.createOrUpdatedoes next. -
TestCaseCsv.createEntity(CSV import). FollowEntityCsv.createEntity:- Set the FQN.
- Look up the existing row and derive
isUpdate. - Call
repository.prepareInternal(testCase, isUpdate)for both the dry run and the real import, beforecreateOrUpdateForImport.
prepareassigns the table's basic suite (TestCaseRepository.java:906-907), which would overwrite the suite the row resolved from itstestSuitecolumn or the target bundle suite. Resolve that by applying the row's suite afterprepare, or by havingpreparekeep a suite the caller already set. -
CSV dry run. Replace
prepareInternal(testCase, true)withprepareInternal(testCase, isUpdate). Stop callinggetOrCreateTestSuitebefore validation, so a dry run or a rejected row doesn't create a basic suite. -
Remove the workarounds in Fixes 35274: Check test parameters against their definitions and fix open-window and boolean-set runs #35629. Fixes 35274: Check test parameters against their definitions and fix open-window and boolean-set runs #35629 adds definition-conformance checks for test cases and works around these gaps. Once items 1–3 land, remove whatever remains of those workarounds, such as a new-row conformance check in
TestCaseCsv, and update the Consequences section ofADR:2026-10-12-test-cases-are-checked-against-their-definition-at-create, which records the CSV import as not telling new rows from existing ones.
Tests (TestCaseResourceIT)
- PUT-create with a disabled definition returns 4xx, and no basic test suite is created for a table that had none.
- PUT on an existing test case whose definition was disabled afterwards returns 200, so the row stays editable.
- CSV import, real and dry run: a new row with a disabled definition fails.
- CSV import, real and dry run: a new row missing a required parameter fails.
- CSV import: updating an existing row still succeeds.
- The same CSV gives the same per-row result with
dryRun=trueanddryRun=false. - A dry run creates no test suite.
OpenMetadata version
main (upstream/main at the time of filing)
Additional context
Found while reviewing #35629 (#35629 (review)).
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsRelease Backlog 🚧
Activity