[BUG] ExpectColumnValuesToBeOfType rejects the type name it reports: a type the dialect module does not export silently becomes a guaranteed failure
What's wrong
ExpectColumnValuesToBeOfType and ExpectColumnValuesToBeInTypeList resolve the user's type_ string by attribute lookup on the SQLAlchemy dialect module (great_expectations/expectations/type_comparison.py:208, getattr(type_module, expected_type)). When the dialect module does not export that name, the AttributeError is swallowed at DEBUG level (:211), the candidate-type list comes back empty, and the comparison at :116 / :156 becomes isinstance(actual_column_type, ()) — which is unconditionally False. An unresolvable type name therefore produces a guaranteed failure that is indistinguishable from a genuine type mismatch: no exception, no warning, raised_exception: false.
On Oracle this makes the Expectation reject the very type name it reports. A column created as INTEGER is reflected as a generic sqlalchemy.INTEGER, so observed_value is "INTEGER" — but sqlalchemy.dialects.oracle exports no INTEGER, so asking type_="INTEGER" fails. There is no name a user can supply that passes, because the one GX itself reports is unresolvable.
Impact
- Fails closed, and lies about why. A correct assertion on a correctly typed column reports failure, with an
observed_valueequal to the type asked for. The only clue is a DEBUG log line. - On Oracle the two Expectations are effectively unusable for any type the dialect module does not re-export (
INTEGERis one; the module exports its ownNUMBER,VARCHAR2, etc.). - On every dialect taking the
isinstancepath — Athena, BigQuery, ClickHouse, Dremio, Hive, MySQL, Oracle, Redshift, SingleStore, SQLite, Teradata, Vertica — a typo intype_("INTGER") reports as an ordinary mismatch rather than an error. Confirmed on SQLite:type_="INTGER"→success=False, observed_value='INTEGER', raised_exception=False. - Dialects in
CASE_INSENSITIVE_DIALECTS(:53— Databricks, PostgreSQL, Snowflake, SQL Server, Trino) take a string-comparison branch and are unaffected. - Present since the lookup was introduced (
0.8.0a1, 2019) through1.23.0. Workaround: none on Oracle for a generic type name.
Reproduction
Reproduced on 1.23.0 (673082d4b), Python 3.11, against the repository's Oracle container (assets/docker/oracle).
Add to tests/integration/data_sources_and_expectations/expectations/test_expect_column_values_to_be_of_type.py (import OracleDatasourceTestConfig alongside the module's other configs):
@parameterize_batch_for_data_sources(
data_source_configs=[OracleDatasourceTestConfig()],
data=DATA,
)
def test_success_for_type__INTEGER_oracle(batch_for_datasource: Batch) -> None:
"""Oracle reflects this column as a generic ``sqlalchemy.INTEGER`` and reports it as such.
``sqlalchemy.dialects.oracle`` does not export ``INTEGER``, so the expected-type lookup on
the dialect module comes back empty and ``isinstance(value, ())`` is unconditionally False:
the expectation rejects the very type name it reports as ``observed_value``.
"""
result = batch_for_datasource.validate(
gxe.ExpectColumnValuesToBeOfType(column=INTEGER_COLUMN, type_="INTEGER")
)
assert result.result["observed_value"] == "INTEGER"
assert result.success, result.resultpytest tests/integration/data_sources_and_expectations/expectations/test_expect_column_values_to_be_of_type.py::test_success_for_type__INTEGER_oracle -m oracleObserved (first assertion passes — the column is reported as INTEGER — the second fails):
E AssertionError: {'observed_value': 'INTEGER'}
E assert False
"result": {"observed_value": "INTEGER"},
"exception_info": {"raised_exception": false, "exception_traceback": null, "exception_message": null}Expected:
success=True # the column is INTEGER, and INTEGER was asked forRequirements
- When
type_(or atype_listmember) names a type the dialect module does not export but the genericsqlalchemynamespace does, the comparison must resolve it against the generic namespace — soExpectColumnValuesToBeOfType(type_="INTEGER")on an OracleINTEGERcolumn returnssuccess=True, on any dialect that reflects the column as a generic SQLAlchemy type. - When a type name cannot be resolved in either namespace, the result must carry a clear exception naming the unresolvable type and the dialect (
raised_exception: true), on everyisinstance-path dialect — never asuccess=Falsethat looks like a data mismatch. A typo must be told apart from a type mismatch. - Results for type names the dialect module does export must be unchanged on every dialect, and the
CASE_INSENSITIVE_DIALECTSstring-comparison branch must be unchanged. - The public signatures and parameters of both Expectations must not change.
- The two behaviours above must each have a regression case in
test_expect_column_values_to_be_of_type.pyandtest_expect_column_values_to_be_in_type_list.py: Oracle withtype_="INTEGER"(passes), and an unresolvable name on at least oneisinstance-path dialect (raises).
Out of scope: the LIKE-pattern allow-list on Oracle and SingleStore (#12214 — the same "dialect module is not a complete namespace" mistake, different code path); Snowflake regex anchoring (#12213); dialect-specific type vocabularies (ClickHouse's Int32/String naming is a separate question); the new schema-level ExpectColumnTypeToBe (#12186), which is converging on the same "raise clearly on an unknown type" rule independently.
Context
- Root cause:
type_comparison.py:176-213(_get_potential_sqlalchemy_types) —getattr(type_module, expected_type)at:208, swallowed at:211, thenisinstance(actual_column_type, tuple(types))at:116(single type) and:156(type list) with an empty tuple.observed_valueand the comparison use different sources:type(actual_column_type).__name__versus a lookup on the dialect module, so they can disagree exactly as shown above. - Fix shapes, each verified to turn the test above green on Oracle at
673082d4b:- A (requirement 1): after the dialect-module lookup, if
typesis still empty andhasattr(sa, expected_type), appendgetattr(sa, expected_type). Two lines at:212-213, where the empty list is already detected and only logged. - C (alternative, implementer's call): add
GXSqlDialect.ORACLEtoCASE_INSENSITIVE_DIALECTSat:53, so Oracle takes the string-comparison branch the five listed dialects already use. One line; also makesNUMBER,VARCHAR2, etc. compare by name. It changes Oracle's comparison fromisinstanceto case-insensitive string equality, which is a wider semantic shift than requirement 1 needs — and it does nothing for requirement 2 on the other elevenisinstance-path dialects. Reasonable if the implementer argues for it; A + B is the narrower shape.
- A (requirement 1): after the dialect-module lookup, if
- B (requirement 2) is independent of A/C: the empty-tuple
isinstanceat:116/:156should never be reached. Whether the exception is raised in_get_potential_sqlalchemy_typesor checked by the two callers is the implementer's choice; either way it must surface inexception_info, not as asuccess=False. - Ruled out: an Oracle reflection problem —
hasattr(sqlalchemy.dialects.oracle, "INTEGER")isFalseandisinstance(reflected_type, sqlalchemy.INTEGER)isTrue, verified against the live container; the column really is a genericINTEGERand only the lookup namespace is wrong. Not verified: which other (dialect, generic type name) pairs are affected — Oracle/INTEGERis one instance, and dialect modules re-export whatever subset of generic types they choose. - Also recorded in
tests/integration/data_sources_and_expectations/data_source_backlog.md("Fifteen candidates were measured…"): these two type cases are two of Oracle's six failures against the gallery-wide suite; the other four are #12214.
Source: fivetran/great_expectations