Skip to content

Add valid string case check (#1335)#1347

Open
SyedIshmumAhnaf wants to merge 2 commits into
databrickslabs:mainfrom
SyedIshmumAhnaf:feat/valid-string-case-check
Open

Add valid string case check (#1335)#1347
SyedIshmumAhnaf wants to merge 2 commits into
databrickslabs:mainfrom
SyedIshmumAhnaf:feat/valid-string-case-check

Conversation

@SyedIshmumAhnaf

Copy link
Copy Markdown

Changes

  • Add a has_valid_string_case row-level check for validating consistent string casing.
  • Support upper, lower, title, and sentence case modes.
  • Cast input values to strings before normalization and comparison so the check can handle non-string columns consistently.
  • Validate the case argument and reject unsupported values.
  • Register the check for declarative rule construction.
  • Add unit and integration test coverage for the new behavior.
  • Document the check and update the DQX check-definition agent skill.

Linked issues

Resolves #1335

Tests

  • manually tested
  • added unit tests
  • added integration tests
  • added end-to-end tests
  • added performance tests

Documentation and Demos

  • added/updated demos
  • added/updated docs
  • added/updated agent skills

@SyedIshmumAhnaf
SyedIshmumAhnaf requested a review from a team as a code owner July 19, 2026 18:41
@SyedIshmumAhnaf
SyedIshmumAhnaf requested review from nehamilak-db and removed request for a team July 19, 2026 18:41
@CLAassistant

CLAassistant commented Jul 19, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@ghanse ghanse left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Overall looks good. Left a few small comments. Can you add some performance tests for this check?

Here's an example:

@pytest.mark.parametrize(
"column",
[
"col1_email_standard",
"col2_email_with_quoted_local_part",
"col3_email_with_ip_domain",
"col4_email_with_multi_part_domain",
],
)
@pytest.mark.benchmark(group="test_benchmark_is_valid_email")
def test_benchmark_is_valid_email(benchmark, ws, generated_email_df, column):
dq_engine = DQEngine(workspace_client=ws, extra_params=EXTRA_PARAMS)
checks = [
DQRowRule(
name=f"{column}_is_valid_email_address",
criticality="warn",
check_func=check_funcs.is_valid_email,
column=column,
),
]
benchmark.group += f" {column}"
checked = dq_engine.apply_checks(generated_email_df, checks)
actual_count = benchmark(lambda: checked.count())
assert actual_count == EXPECTED_ROWS

| `is_older_than_col2_for_n_days` | Checks whether the values in one input column are at least N days older than the values in another column. | `column1`: first column to check (can be a string column name or a column expression); `column2`: second column to check (can be a string column name or a column expression); `days`: number of days; `negate`: if the condition should be negated |
| `regex_match` | Checks whether the values in the input column match a given regex. | `column`: column to check (can be a string column name or a column expression); regex: regex to check; `negate`: if the condition should be negated (true) or not |
| `is_valid_email` | Checks whether the values in the input column have valid email address format. | `column`: column to check (can be a string column name or a column expression) |
| `has_valid_string_case` | Checks whether the values match the requested string case. `upper` and `lower` use the corresponding Spark case functions; `title` uses Spark `initcap`; `sentence` splits on literal `.` characters, cases each segment independently, and preserves delimiters and surrounding whitespace. | `column`: column to check (can be a string column name or a column expression); `case`: one of `upper`, `lower`, `title`, or `sentence` |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think it's better to say what each case is rather than how it is implemented. For example:

...`upper` and `lower` check that all characters are uppercase or lowercase, respectively; `title` checks that the 1st character of each word is uppercase; `sentence` checks that the 1st character of each sentence is uppercase

Comment thread skills/dqx-define-checks/SKILL.md Outdated
## Fallback: custom SQL

**Search `check_funcs` first** the built-ins cover null/empty, range, set membership, regex, referential, aggregate, uniqueness, schema, freshness, comparison, and outlier cases with typed error messages and tested edge handling. Drop down to SQL only when no built-in fits.
**Search `check_funcs` first** - the built-ins cover null/empty, string case, range, set membership, regex, referential, aggregate, uniqueness, schema, freshness, comparison, and outlier cases with typed error messages and tested edge handling. Use `has_valid_string_case` for `upper`, `lower`, `title`, or `sentence` string-case validation. Drop down to SQL only when no built-in fits.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Let's remove the specific sentence about case validation. Details about most individual checks are intentionally left out of the skill file to prevent it from getting too large.

None,
],
[None, None, None, None, None, None],
[None, None, None, violation(" first SEGMENT. second SEGMENT ", "sentence", "sentence"), None, None],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think the title case should have a violation here?

Comment thread src/databricks/labs/dqx/check_funcs.py Outdated
lambda segment: F.concat(
F.regexp_extract(segment, r"^(\s*)", 1),
F.upper(F.substring(F.regexp_replace(segment, r"^\s*", ""), 1, 1)),
F.lower(F.substring(F.regexp_replace(segment, r"^\s*", ""), 2, 2147483647)),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maybe best to use an internal constant for 2147483647

Comment thread src/databricks/labs/dqx/check_funcs.py Outdated
elif case == "lower":
normalized_case = F.lower(col_expr_cast)
elif case == "title":
normalized_case = F.initcap(col_expr_cast)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This will flag strings with uppercased abbreviations like "Notes From IEEE Meeting".

Maybe we should use a pattern similar to what's been used for sentence case?

Comment thread tests/unit/test_build_rules.py Outdated
Comment on lines +59 to +85
def test_has_valid_string_case_rule_round_trip():
rule = DQRowRule(
criticality="warn",
check_func=has_valid_string_case,
column="a",
check_func_kwargs={"case": "lower"},
)
expected_metadata = [
{
"name": "a_has_invalid_lower_string_case",
"criticality": "warn",
"check": {
"function": "has_valid_string_case",
"arguments": {"column": "a", "case": "lower"},
},
}
]

assert serialize_checks([rule]) == expected_metadata

deserialized_rules = deserialize_checks(expected_metadata)
assert len(deserialized_rules) == 1
deserialized_rule = deserialized_rules[0]
assert deserialized_rule.name == "a_has_invalid_lower_string_case"
assert deserialized_rule.check_func is has_valid_string_case
assert deserialized_rule.column == "a"
assert deserialized_rule.check_func_kwargs == {"case": "lower"}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't think we need a specific test for this check. We can add it to the rulesets in the existing tests.

@ghanse ghanse added under-review This PR is currently being reviewed by one of DQX maintainers. needs-changes Changes required after review labels Jul 20, 2026
@SyedIshmumAhnaf

Copy link
Copy Markdown
Author

Thanks for the review and the helpful comments. I’ll work through the requested changes, add the performance test coverage, and update the PR accordingly.

@SyedIshmumAhnaf

Copy link
Copy Markdown
Author

Hi @ghanse, I’ve incorporated the feedback from the review into PR #1347.

The updates include the requested documentation changes, revised title-case behavior for uppercase abbreviations, updated integration expectations, use of an internal substring-length constant, consolidation of the rule-construction coverage, and performance tests for all four supported case modes.

The PR is ready for another review. Thanks!

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

Labels

needs-changes Changes required after review under-review This PR is currently being reviewed by one of DQX maintainers.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: New check to validate string case

3 participants