Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/dqx/docs/reference/quality_checks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ You can also define your own custom checks in Python (see [Creating custom check
| `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 string values match the requested letter case: `upper` requires all alphabetic characters to be uppercase; `lower` requires all alphabetic characters to be lowercase; `title` requires the first character of each space-delimited word to be uppercase; `sentence` requires each period-delimited segment's first non-whitespace character to be uppercase. | `column`: column to check (can be a string column name or a column expression); `case`: one of `upper`, `lower`, `title`, or `sentence` |
| `is_valid_national_id` | Checks whether the values in the input column are valid national identification numbers (e.g., US Social Security Numbers) for the given country. | `column`: column to check (can be a string column name or a column expression); `country`: ISO 3166 alpha-2 country code (optional, default: `US`) |
| `is_valid_national_id` | Checks whether the values in the input column match a supported national identification number format. Supports US Social Security Numbers (`US`), UK National Insurance numbers (`GB`), and Indian Permanent Account Numbers (`IN`). These are format checks only; they do not verify whether an identifier was issued. | `column`: column to check (can be a string column name or a column expression); `country`: ISO 3166 alpha-2 country code (optional, default: `US`) |
| `is_valid_uuid` | Checks whether the values in the input column are valid UUIDs (RFC 9562, canonical 8-4-4-4-12 hyphenated hex form). By default validates the shape only; set `strict` to also enforce the version nibble (1-8) and variant bits per RFC 9562. | `column`: column to check (can be a string column name or a column expression); `strict`: if True, also validate the version nibble (1-8) and variant bits (8/9/a/b) per RFC 9562 (default: False) |
| `is_valid_country_code` | Checks whether the values in the input column are valid ISO 3166-1 country codes (alpha-2, e.g. US, alpha-3, e.g. USA, or numeric, e.g. 840). Source: https://www.iso.org/iso-3166-country-codes.html | `column`: column to check (can be a string column name or a column expression); `code_format`: ISO 3166-1 representation, `alpha-2` (default), `alpha-3`, or `numeric`; `case_sensitive`: optional boolean flag for case-sensitive comparison (default: True) |
| `is_valid_currency_code` | Checks whether the values in the input column are valid ISO 4217 currency codes (alphabetic, e.g. USD, or numeric, e.g. 840). Source: https://www.iso.org/iso-4217-currency-codes.html | `column`: column to check (can be a string column name or a column expression); `code_format`: ISO 4217 representation, `alphabetic` (default) or `numeric`; `case_sensitive`: optional boolean flag for case-sensitive comparison (default: True) |
Expand Down
27 changes: 21 additions & 6 deletions src/databricks/labs/dqx/check_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ class DQPattern(Enum):
# none) must be consistent via backreference \1. Excludes invalid ranges - area
# 000/666/9xx (9xx covers ITINs), group 00, serial 0000. Anchored, fixed-width; ReDoS-safe.
SSN_US = r"\A(?!000|666|9\d{2})\d{3}([- ]?)(?!00)\d{2}\1(?!0000)\d{4}\z"
# UK National Insurance Number: two-letter prefix, six digits, and an A-D
# suffix. Exclude prefixes that HMRC does not allocate.
NINO_GB = (
r"\A(?!(?:BG|GB|KN|NK|NT|TN|ZZ))(?!(?:[DFIQUV]))[A-Z]" r"(?![DFIOQUV])[A-Z] ?\d{2} ?\d{2} ?\d{2} ?[ABCD]\z"
)
# Indian Permanent Account Number (PAN): three letters, a holder-type letter,
# another letter, four digits, and a final letter.
PAN_IN = r"\A[A-Z]{3}[ABCFGHJLPT][A-Z]\d{4}[A-Z]\z"

# Canonical UUID form per RFC 9562: 8-4-4-4-12 hex groups. UUID validates the shape
# only, so RFC-defined Nil/Max sentinels and legacy variant GUIDs pass; UUID_STRICT
Expand All @@ -130,6 +138,8 @@ class DQPattern(Enum):
# alpha-2 code here.
_NATIONAL_ID_PATTERNS_BY_COUNTRY: dict[str, DQPattern] = {
"US": DQPattern.SSN_US,
"GB": DQPattern.NINO_GB,
"IN": DQPattern.PAN_IN,
}


Expand Down Expand Up @@ -1173,12 +1183,17 @@ def is_valid_national_id(column: str | Column, country: str = "US") -> Column:
Validation is limited to *format* and *number ranges*; it does not verify that a
number was actually issued.

Supported countries are keyed by ISO 3166 alpha-2 code. Currently only *US* is
supported: the *AAA-GG-SSSS* form is required, where the separators may be all
hyphens, all single spaces, or omitted entirely (e.g. *123-45-6789*, *123 45 6789*
or *123456789*), but must be used consistently. Structurally invalid ranges are
rejected (area *000*, *666* and *900-999* - the latter covering ITINs; group *00*;
serial *0000*).
Supported countries are keyed by ISO 3166 alpha-2 code. For *US*, the
*AAA-GG-SSSS* form is required, where the separators may be all hyphens, all
single spaces, or omitted entirely (e.g. *123-45-6789*, *123 45 6789* or
*123456789*), but must be used consistently. Structurally invalid ranges are
rejected (area *000*, *666* and *900-999* - the latter covering ITINs; group
*00*; serial *0000*). For *GB*, a National Insurance number consists of two
letters, six digits, and a final *A*, *B*, *C*, or *D*; unallocated prefixes
are rejected. For *IN*, a PAN consists of three letters, a holder-type letter
(*A*, *B*, *C*, *F*, *G*, *H*, *J*, *L*, *P*, or *T*), another letter, four
digits, and a final letter. These checks validate format only, not whether an
identifier was issued.

Null values will pass the check with no violation reported.

Expand Down
62 changes: 62 additions & 0 deletions tests/integration/test_row_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2298,6 +2298,68 @@ def violation(value: str) -> str:

assertDataFrameEqual(actual, expected)

schema_nino = "nino: string"
nino_df = spark.createDataFrame(
[
["AB123456A"],
["AB 12 34 56 A"],
["BX586745C"],
["DF123456A"], # invalid first letter
["BG123456A"], # unallocated prefix
["AB123456E"], # invalid suffix
[None],
Comment thread
AtomicGlance marked this conversation as resolved.
Outdated
],
schema_nino,
)
nino_actual = nino_df.select(is_valid_national_id("nino", country="GB"))

def nino_violation(value: str) -> str:
return f"Value '{value}' in Column 'nino' does not match pattern 'NINO_GB'"

nino_expected = spark.createDataFrame(
[
[None],
[None],
[None],
[nino_violation("DF123456A")],
[nino_violation("BG123456A")],
[nino_violation("AB123456E")],
[None],
],
"nino_does_not_match_pattern_nino_gb: string",
)
assertDataFrameEqual(nino_actual, nino_expected)

schema_pan = "pan: string"
pan_df = spark.createDataFrame(
[
["ABCPD1234F"],
["AACTA1234A"],
["ABCZD1234F"], # Z is not a valid holder type
["AB12E1234F"], # letters and digits in the wrong positions
["ABCPD12345"], # final character must be a letter
[None],
Comment thread
AtomicGlance marked this conversation as resolved.
Outdated
],
schema_pan,
)
pan_actual = pan_df.select(is_valid_national_id("pan", country="IN"))

def pan_violation(value: str) -> str:
return f"Value '{value}' in Column 'pan' does not match pattern 'PAN_IN'"

pan_expected = spark.createDataFrame(
[
[None],
[None],
[pan_violation("ABCZD1234F")],
[pan_violation("AB12E1234F")],
[pan_violation("ABCPD12345")],
[None],
],
"pan_does_not_match_pattern_pan_in: string",
)
assertDataFrameEqual(pan_actual, pan_expected)


def test_col_is_valid_national_id_column_expr_and_lowercase_country(spark):
schema_ssn = "a: string"
Expand Down