Fixes 21482: only compare a column against '' when the type can hold it - #31130
Fixes 21482: only compare a column against '' when the type can hold it#31130TeddyCr wants to merge 4 commits into
Conversation
The columnValuesMissingCount test always emitted `col = ''` alongside the NULL check, whatever the column type. On PostgreSQL an INTEGER column makes the test abort with `invalid input syntax for type integer: ""`; on MySQL the comparison succeeds but coerces '' to 0, counting every zero row as missing. MariaDB does the same for TIME columns via '00:00:00'. Guard the empty-string branch with a fail-open predicate: quantifiable, date/time, boolean, UUID, interval, enum and IP types are excluded, and any type we don't recognize keeps the comparison so dialect-specific string types are unaffected. The pandas path is left alone: pandas comparisons never coerce, and an object-dtype datalake column typed as INT really can carry "". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
❌ 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 |
Code Review ✅ ApprovedRestricts empty-string comparisons in column missing-count metrics to types that support them, preventing PostgreSQL syntax errors and false positives on numeric and boolean types. No issues found. 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 |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR updates the NullMissingCount metric to avoid comparing non-string columns against the empty string (''), preventing runtime errors on strict engines and incorrect coercion/counts on MySQL/MariaDB. It also adds unit and integration coverage to ensure the metric only checks '' for types that can meaningfully hold it.
Changes:
- Add a type guard (
can_hold_empty_string) soNullMissingCountonly compares= ''for string-capable types. - Add SQL compilation unit tests across Postgres/MySQL dialects for both string and non-string ORM/OM types.
- Add integration tests using real Postgres/MySQL engines to validate correct missing counts.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| ingestion/src/metadata/profiler/metrics/static/null_missing_count.py | Adds can_hold_empty_string and conditionally includes the empty-string comparison in SQL generation. |
| ingestion/tests/unit/observability/profiler/sqlalchemy/test_null_missing_count_metric.py | Adds dialect compilation tests ensuring = '' is excluded for non-string types and included for string-like/unknown types. |
| ingestion/tests/integration/profiler/test_null_missing_count_metric.py | Adds engine-level validation against Postgres/MySQL using testcontainers to ensure counts match expected semantics. |
| # CustomTime is listed here rather than in registry.is_date_time because that | ||
| # helper also drives min/max/mean/stddev and columnValuesToBeBetween, which we are | ||
| # not changing here. TODO(#21482): close that registry gap separately. | ||
| NON_EMPTY_STRING_TYPES = (Boolean, UUIDString, CustomIP, CustomTime, Interval, SqlEnum) |
There was a problem hiding this comment.
JSON and array columns can hit this too — col = '' errors on both Postgres and MySQL, so sqlalchemy.JSON, sqlalchemy.ARRAY and CustomArray could go in here as well.
Binary types are fine as-is though — an empty blob really does match '', so excluding those would drop real counts.
Describe your changes:
Fixes #21482
The
columnValuesMissingCount("Column Values Missing Count") test always compared the column against the empty string, whatever its type:On PostgreSQL an
INTEGERcolumn makes this fail outright —psycopg2.errors.InvalidTextRepresentation: invalid input syntax for type integer: ""— so the test aborts. On MySQL it does not fail:''is coerced to0, so every zero row is counted as missing and the test silently reports a wrong number.The empty-string branch comes from
NullMissingCount(added in #16017 for #14812, which only considered string columns). It is now emitted only for types that can actually hold''. The predicate fails open: quantifiable, date/time, boolean, UUID, interval, enum and IP types are excluded; anything else — including dialect-specific string types we don't recognize — keeps counting empty strings exactly as before.CustomTime(the MariaDBTIMEmapping) is listed in the metric's exclusion tuple rather than added toregistry.is_date_time, because that helper also drivesmin/max/mean/stddevandcolumnValuesToBeBetween; widening it would change those paths too, which is out of scope for this fix.missingValueMatchis unaffected: user-supplied missing values go through the separatecountInSetmetric.The pandas/DataFrame path is deliberately left alone. A CSV/datalake column typed as
INTgenuinely can carry""for a missing value, so guarding there would drop real missing values; the SQL path has no such case because the engine enforces the type.Type of change:
High-level design:
N/A — small change.
Tests:
Use cases covered
Column Values Missing Counton a PostgreSQLINTEGERcolumn runs and reports only the NULLs, instead of aborting withinvalid input syntax for type integer: ""INT/BOOLEANcolumn no longer counts0/falserows as missingTIMEcolumn no longer counts'00:00:00'rows as missingDATE,BOOLEAN,UUID,INTERVAL,ENUMandIPV4/IPV6columns no longer errors on PostgreSQL (and onDATEfor MySQL, which raisedIncorrect DATE value: ''in strict mode)''as missing — no behavior changeUnit tests
ingestion/tests/unit/observability/profiler/sqlalchemy/test_null_missing_count_metric.py= ''forInteger,SmallInteger,BIGINT,Numeric,DECIMAL,Float,Date,DateTime,Time,CustomTimestamp,CustomTime,Boolean,UUIDString,Interval,Enum,CustomIP;= ''preserved forString,VARCHAR,TEXT,CHAR,NVARCHARand the unrecognized-type fail-open case.CommonMapTypes._TYPE_MAPfor the 18 OMDataTypes that cannot hold'', so a converter change that reintroduces the bug fails the suite.Backend integration tests
Ingestion integration tests
ingestion/tests/integration/profiler/test_null_missing_count_metric.pyPlaywright (UI) tests
Manual testing performed
Verified the generated SQL before/after, and the engine behavior on throwaway containers with a table holding 3 NULLs and 2 zeros:
integerERROR: invalid input syntax for type integer: ""intbooleantime'00:00:00'counted)date/boolean/uuid/interval/inet/enumERROR: invalid input syntax for type …: ""dateERROR 1525: Incorrect DATE value: ''Full
ingestion/tests/unit/observability/profiler+ingestion/tests/unit/observability/data_qualitysuites plus the new integration file pass (819 passed, 1 skipped). The new tests were confirmed RED against the unpatched metric.Note for reviewers — behavior change on MySQL / MariaDB
On MySQL, integer and boolean columns previously had their
0/falserows counted as missing; on MariaDB,TIMEcolumns had their'00:00:00'rows counted. Anyone who calibratedmissingCountValueagainst those inflated numbers will see their test go from green to red after this fix. The new number is the correct one, but it is worth a release note.Blast radius is limited to this DQ test:
NullMissingCountis a distinct metric from the profiler'sNullCount(static/null_count.py), which never had the= ''branch, so stored profiles andnullProportionhistory are untouched.One deliberate trade-off: MySQL permits
''as a realENUMmember, so an enum column with an empty label will no longer have those rows counted. PostgreSQL native enums raise on the comparison, so avoiding the hard error was preferred over that edge case. Anyone affected can add""explicitly viamissingValueMatch, which is routed throughcountInSetand unaffected by this change.The predicate fails open by design, so six types in
supportedDataTypesthat are not inCommonMapTypes._TYPE_MAP(TIMESTAMPZ,VARIANT,GEOMETRY,POINT,POLYGON,LOWCARDINALITY) still emit the comparison. That is intentional —LOWCARDINALITY(String)genuinely can hold'', andTIMESTAMPZis unreachable from SQL ingestion becausecolumn_type_parsernormalizesTIMESTAMPTZtoTIMESTAMP. If one of the exotic ones ever surfaces apoint = ''report, it belongs inNON_EMPTY_STRING_TYPES.UI screen recording / screenshots:
Not applicable.
Checklist:
Fixes <issue-number>: <short explanation>Fixes #<issue-number>above.