[BUG] ExpectColumnValuesToBeOfType rejects the type name it reports: a type the dialect module does not export silently becomes a guaranteed failure

Author: joshua-staufferCreated Sep 17, 2026Updated Sep 17, 2026
Labelsbughelp wantedready-for-workclaimed

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_value equal 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 (INTEGER is one; the module exports its own NUMBER, VARCHAR2, etc.).
  • On every dialect taking the isinstance path — Athena, BigQuery, ClickHouse, Dremio, Hive, MySQL, Oracle, Redshift, SingleStore, SQLite, Teradata, Vertica — a typo in type_ ("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) through 1.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):

python
@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.result
bash
pytest tests/integration/data_sources_and_expectations/expectations/test_expect_column_values_to_be_of_type.py::test_success_for_type__INTEGER_oracle -m oracle

Observed (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 for

Requirements

  1. When type_ (or a type_list member) names a type the dialect module does not export but the generic sqlalchemy namespace does, the comparison must resolve it against the generic namespace — so ExpectColumnValuesToBeOfType(type_="INTEGER") on an Oracle INTEGER column returns success=True, on any dialect that reflects the column as a generic SQLAlchemy type.
  2. 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 every isinstance-path dialect — never a success=False that looks like a data mismatch. A typo must be told apart from a type mismatch.
  3. Results for type names the dialect module does export must be unchanged on every dialect, and the CASE_INSENSITIVE_DIALECTS string-comparison branch must be unchanged.
  4. The public signatures and parameters of both Expectations must not change.
  5. The two behaviours above must each have a regression case in test_expect_column_values_to_be_of_type.py and test_expect_column_values_to_be_in_type_list.py: Oracle with type_="INTEGER" (passes), and an unresolvable name on at least one isinstance-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, then isinstance(actual_column_type, tuple(types)) at :116 (single type) and :156 (type list) with an empty tuple. observed_value and 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 types is still empty and hasattr(sa, expected_type), append getattr(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.ORACLE to CASE_INSENSITIVE_DIALECTS at :53, so Oracle takes the string-comparison branch the five listed dialects already use. One line; also makes NUMBER, VARCHAR2, etc. compare by name. It changes Oracle's comparison from isinstance to case-insensitive string equality, which is a wider semantic shift than requirement 1 needs — and it does nothing for requirement 2 on the other eleven isinstance-path dialects. Reasonable if the implementer argues for it; A + B is the narrower shape.
  • B (requirement 2) is independent of A/C: the empty-tuple isinstance at :116 / :156 should never be reached. Whether the exception is raised in _get_potential_sqlalchemy_types or checked by the two callers is the implementer's choice; either way it must surface in exception_info, not as a success=False.
  • Ruled out: an Oracle reflection problem — hasattr(sqlalchemy.dialects.oracle, "INTEGER") is False and isinstance(reflected_type, sqlalchemy.INTEGER) is True, verified against the live container; the column really is a generic INTEGER and only the lookup namespace is wrong. Not verified: which other (dialect, generic type name) pairs are affected — Oracle/INTEGER is 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