Great Expectations 于 2026 年 9 月 28 日发布 1.23.2,修复 SQLAlchemy 2.1.0 造成的 import 失败。 前置部署 Forward Deployed Engineer(FDE)在客户现场遇到的事故,多数不是模型不准,而是「上周还跑得好好的数据校验任务,这周连包都导入不进来」。前置部署 Forward Deployed Engineer(FDE)交付的企业数据质量 enterprise data quality 管道,底层通常就是一个 SQLAlchemy 连接层加一套断言;SQLAlchemy 一升级,断言结论就可能静默翻面,而这次升级往往是被更上层的依赖顺手带进来的,没有人主动决定过。前置部署 Forward Deployed Engineer(FDE)需要的不是「某框架出了新版本」,而是三个具体答案:坏在哪一行、压测数字是多少、退回去要敲哪条命令。本条把 Great Expectations 1.23.2 与 SQLAlchemy 2.1.0 之间这三天里的连锁反应拆成可核对的根因、作者自己贴出的验证数字、以及一条两步回滚路径。 ## 一、三天内连发三个版本:时间线与口径差异 - **2026-09-24**:SQLAlchemy 2.1.0 发布,发布说明写的是 Released: September 24, 2026。 - **2026-09-25**:SQLAlchemy 2.1.1 发布。它修的不是运行时问题,而是 2.1.0 的源码包在 uv 这类严格执行规范的安装器下构建失败:pyproject.toml 里保留了 mssql_pymssql、postgresql_psycopg 这种下划线分隔的 extra 名,归一化后与破折号名重复,违反 PEP 685,引用编号 13604。 - **2026-09-18**:Great Expectations 1.23.1 发布,早于 SQLAlchemy 2.1.0 六天,因此 1.23.1 是按 2.0.x 写的。 - **2026-09-25**:Great Expectations 侧同一天提了三个 PR。第 12267 号先在整个库的共享约束里把 SQLAlchemy 钉在 2.1 以下止血;第 12268 号放宽 snowflake 的导入守卫;第 12269 号才是真正恢复 2.1 支持。 - **2026-09-28**:1.23.2 在 GitHub 发布页发布,时间戳是 2026-09-28T08:20:15Z,发布人 joshua-stauffer;同版本的 tag 提交时间是 2026-09-25 16:26:48 UTC。 时间口径上有一处不一致,值得写清楚:PyPI 上 1.23.2 的 wheel 上传时间是 2026-09-25T18:48:25Z,比 GitHub 发布页早约三天。按发布页引用是 9 月 28 日,按 PyPI 引用是 9 月 25 日,两个日期都能自证,选哪个要先说明。 还有一处归属变化值得记下来:本条引用的发布页与三个 PR,GitHub 给出的规范地址都在 Fivetran 组织下的 great_expectations 仓库,不再是原来那个组织名下。前置部署 Forward Deployed Engineer(FDE)把这条写进依赖台账时,顺手核对仓库归属,比事后发现镜像站或内部 fork 停在旧路径要省事。 ## 二、根因一:可选依赖在 import 期把整个包拖死 snowflake-sqlalchemy 1.11.1 是当时的最新版,它继承了 sqlalchemy.orm.context.ORMSelectCompileState。SQLAlchemy 2.1 把这个类改名成 _ORMSelectCompileState,也就是变成了私有名。 后果比「Snowflake 不能用」重得多:导入时直接抛 AttributeError。Great Expectations 的 SQL 执行引擎在包导入阶段就会加载这个兼容模块,于是异常从 `import great_expectations` 逃了出来。命中的是所有装了 snowflake-sqlalchemy 的环境,哪怕这个环境根本不用 Snowflake。 修法有两层。第一层是把 snowflake extra 钉在 2.1 以下,让装机阶段就绕开。第二层是把 compatibility/snowflake.py 里那处只捕获 ImportError 的守卫放宽为同时捕获 ImportError 与 AttributeError,与同模块其他守卫保持一致:一个装上了却导入失败的方言,降级成「未安装」,只坏它自己提供的那一个后端,不再污染整包导入。前置部署 Forward Deployed Engineer(FDE)在客户环境里排查这类导入失败时,正确顺序是先确认报错来自哪一层依赖、再确认它是不是可选后端,最后才动 pyproject 里的钉版本,因为把一个可选方言的故障当成整包故障去降级,会把能用的后端一起拖回旧版。 ## 三、根因二:driverless 的 postgresql:// 默认驱动换成 psycopg3 SQLAlchemy 2.1 起,不带驱动的 postgresql:// URL 默认选 psycopg(v3)。而 Great Expectations 的 postgresql extra 只安装 psycopg2,所以建 engine 时报的是 `No module named 'psycopg'`。 修法是新增 resolve_postgresql_driver:当 SQLAlchemy 的默认驱动不可导入、而 psycopg2 可导入时,回退到 psycopg2。它的边界写得很细,可以照抄进自己的项目: - 显式写了 postgresql+psycopg2:// 或 postgresql+psycopg:// 的 URL 永远不会被改写。 - SQLAlchemy 2.0 及更早版本默认就是 psycopg2,行为不变。 - 没有给 postgresql extra 增加任何新驱动依赖;开发过程中确实有一版提交加过 psycopg[binary],后来又被移除。 - 「默认驱动可用」的判定标准是能导入,而不是找到了。装 psycopg 但没装它的 binary 扩展、宿主机又没有 libpq 的情况下,能找到但导入失败,此时仍然按回退处理。 ## 四、根因三:Databricks 返回的行对象是 tuple 子类 SQLAlchemy 2.1 的 compiled row class 拒绝 tuple 的子类,而 databricks-sql-connector 返回的 Row 恰好就是 tuple 子类。 结果是最直白的一种失败:`TypeError: Expected tuple, got Row`,每一条 Databricks 查询都跑不通,没有部分可用一说。这一条 Great Expectations 自己修不了,只能等上游,所以 databricks extra 的上限被保留了下来。 ## 五、根因四:Float 被移出 Numeric 继承,类型断言静默翻面 SQLAlchemy 2.1 把 Float 从 Numeric 的继承关系里移走了。这一改动不会报任何错,而是让 expect_column_values_to_be_of_type(type_="Numeric") 以及它的 in_type_list 兄弟断言,在 float 列上从通过翻成失败。 这类「不报错、只改结论」的破坏对数据质量管道最危险:任务照常跑完,告警照常安静,只有校验结论反了。前置部署 Forward Deployed Engineer(FDE)在升级依赖后如果只跑一遍任务是否成功,这一处永远不会被发现,必须比对断言结果本身是否翻转。修法是让 "Numeric" 这个类型名同时也匹配 Float;在 2.1 之前 Float 本来就是 Numeric,所以老版本上没有任何变化。 作者对这一处做了最细的核对:把每个期望类型名对每个反射类型跑一遍,在 2.0 与 2.1 两个版本各跑一次,逐方言检查后只有一条结果发生变化。 ## 六、另外四处的具体报错与修法 - **BigQuery 的参数类型**。2.1 把每个 Python float 绑定参数定型为 Double,而 sqlalchemy-bigquery 没有对应规则,于是任何与浮点字面量比较的查询都失败,报 `The given parameter type, DOUBLE, … is not a valid BigQuery scalar type`。修法是注册一条只作用于 BigQuery 方言的编译规则,把 Double 渲染成 FLOAT64;测试覆盖了浮点字面量、比较、BETWEEN 与 CAST 四种写法,并用一个 sqlite 对照组证明它不会泄漏到别的方言。 - **SQL Server 的表名回映**。2.1 的 SQL Server 方言把单表反射走多表路径,然后用 `.lower()` 把服务器端的写法回映给调用方,而 quoted_name 在被降低时不会折叠大小写。后果是引号包裹的混合大小写表与大写表被报成缺失,table.columns 也一起失败。修法是按纯表名反射;SQL Server 本来就按排序规则匹配对象名而不是按引号,所以传纯字符串在 2.0 上没有副作用。 - **被百分号编码的数据库路径**。PasswordMasker.mask_db_url 与 URL.render_as_string 在 2.1 下把数据库名百分号编码,Windows 上的 C:\path\to\foo.db 变成 C%3A%5C…。同时,2.1 加只有 psycopg2 的环境下,driverless URL 建 engine 失败会退回 urlparse 掩码,丢掉 ?sslmode=require 这类查询串并打警告。 - **被丢掉的列类型**。Great Expectations 的自检数据加载器按驱动无关的 PGDialect 识别 PostgreSQL engine;2.1 下 psycopg2 的 engine 不再被识别,列类型被丢弃。另外,它的近似分位数回退路径原本只覆盖 psycopg2 engine,现在 psycopg engine 也走同一条回退,这条改动在所有 SQLAlchemy 版本上都生效,保证同一个服务端不管用哪个驱动接进来,分位数行为一致。 ## 七、修复后的依赖布局:上限只留两处 - 库级 <2.1 上限被移除,只在 snowflake 与 databricks 两个 extra 上保留:great_expectations[snowflake] 与 great_expectations[databricks],以及任何包含这两者之一的安装,都停在 2.1 以下。 - 其余 SQL extra 在 Python 3.11 及以上解析到 2.1,包括 postgresql、mysql、bigquery、sql-server 与 fabric、oracle、trino、redshift、athena 等。 - Python 3.10 解析到 2.0.x,因为 SQLAlchemy 2.1 要求 Python 3.11 或更高;3.10 从头到尾不受这次破坏影响。 - 官方给的升级命令带着 extra:`pip install --upgrade 'great_expectations[snowflake]'`,注释写得很直白——必须带上 extra,SQLAlchemy 的上限才会生效。 - 裸 `pip install great_expectations` 本身不声明 SQLAlchemy 依赖,这一路径没有任何变化。 ## 八、作者贴出的验证数字 本机环境是 Python 3.13,SQLAlchemy 2.1.0 对 2.0.54 做对照,lane 按 invoke ci-tests 的实际组法挑: | lane | SQLAlchemy 2.1.0 | SQLAlchemy 2.0.54 | |---|---|---| | postgresql(只装 psycopg2,与 CI 一致) | 543 passed | 543 passed | | postgresql(另装 psycopg) | 543 passed | 未跑 | | postgresql gallery(只装 psycopg2) | 52 passed | 52 passed | | postgresql gallery(另装 psycopg) | 52 passed | 未跑 | | sql_server(仅 SQL Server 那个提交) | 260 passed | 260 passed | | sqlite 与 project | 1011 passed | 未跑 | | filesystem | 575 passed | 未跑 | | BigQuery 离线编译测试 | 5 passed | 5 passed | | unit | 5228 passed | 5228 passed | - unit lane 里有 2 个用例在两个版本上一样失败,作者说明都是已知的本地环境效应:一个是冷导入超时,另一个是只在装了 google-cloud-storage 才跑的 GCS 连接器用例,而 unit lane 不装它。 - 全量后端矩阵走的是 ci.yml 的 workflow_dispatch:运行号 36132781885 在 e6c60b3 上 58 个 job 全过,其中 PostgreSQL lane 在 2.1.0 加仅 psycopg2 的情况下 543 与 52 通过;更早的运行号 36130297586 里 BigQuery 报 232 加 52 通过、SQL Server 报 260 通过,同样跑在 2.1.0 上。 - 这条稿子里的失败因果链可以直接用 CI 红绿证明:同一个 SHA,发布前的定时任务 36039036989 是绿的,发布后的 36059219443 是红的,所有开着的 PR 在同一批 lane 上一起失败。 前置部署 Forward Deployed Engineer(FDE)在客户现场做升级验收时,可以照抄这张表当对照基线:只要自己环境里对应的 lane 跑出来的通过数与表中同列不一致,就说明本地方言或驱动组合与官方 CI 有差异,应该先查驱动再查代码。这张表的另一个用处是它给了一个明确的量级——真正需要重点回归的是 PostgreSQL 与 SQL Server 两条 lane,unit 那一列的 5228 项在两个版本上完全一致,说明改动没有扩散到与数据库无关的逻辑。 ## 九、作者承认的局限 - 第三方方言的类型模块没有做离线核对;只有内置方言那一轮做到了逐类型穷举。 - BigQuery 的编译规则是进程级生效的(虽然限定在 BigQuery 方言内),最终只能靠线上 CI 确认 BigQuery 与 SQL Server 合并后的差异。 - 类型检查环境实际解析到 SQLAlchemy 2.0,因为 requirements-dev-sqlalchemy2.txt 引用了 databricks 的需求文件。 - PostgreSQL lane 的连接峰值吃到服务器 1000 个连接里的约 858 个,这个问题先于本 PR 存在,这次没有修。 - snowflake 与 databricks 仍未支持 2.1。snowflake 侧没有任何稳定版 snowflake-sqlalchemy 支持 2.1,上游跟踪号是 SNOW-3121122,对应 snowflakedb/snowflake-sqlalchemy 仓库的第 652 号;databricks 侧卡在 2.1 拒绝 tuple 子类行。 - 第 12268 号 PR 的作者自己标了 out of scope:把「装了但不兼容」显示成「未安装」,这个提示措辞会误导,它没有改。 - 版本日期口径不一致:PyPI 上是 2026-09-25T18:48:25Z 上传,GitHub 发布页是 2026-09-28T08:20:15Z,两者差约三天,写进变更记录时要选一个并注明另一个。 前置部署 Forward Deployed Engineer(FDE)在写交付文档时应当把上面这几条原样保留,尤其是第一与第二条:第三方方言没有离线核对、BigQuery 与 SQL Server 的合并差异只能靠线上 CI 确认,意味着客户环境如果用的是非内置方言或自定义方言,这次修复并不保证覆盖。把这些空缺写进验收标准,比在事故复盘里补写要便宜得多。 ## 十、谁该动手,怎么升,怎么退 该动手的是三类环境:Python 3.11 及以上、装了任意 SQL extra 的 Great Expectations 用户;装了 snowflake-sqlalchemy 但根本不用 Snowflake 的环境,它们的 `import great_expectations` 会直接失败;Databricks、BigQuery、SQL Server 三种后端的用户。 升级:`pip install --upgrade 'great_expectations[snowflake]'`,把实际用的 extra 写进去,上限才会生效。 前置部署 Forward Deployed Engineer(FDE)在客户现场执行这一步时有一个常见误操作要避开:只敲 `pip install --upgrade great_expectations`。这样装出来的包不声明 SQLAlchemy 依赖,环境里那个 2.1 会原封不动留在原地,问题看起来「没修好」,实际是 extra 没带上。 暂时升不了:`pip install 'sqlalchemy<2.1'`。如果 snowflake-sqlalchemy 或 databricks-sqlalchemy 是绕开 GX extra 单独装的,或者环境因为别的原因必须钉 2.1,那么这里的钉必须自己补齐,不能指望 extra 的上限。另外要留意,一个在别处钉了 SQLAlchemy 2.1 的环境,装上 GX 的 SQL extra 后会得到依赖解析冲突,而不是运行时报错——冲突比报错好排查。 升完先跑这五条检查,前四条是显性报错,第五条是静默的:`import great_expectations` 是否成功;Databricks 查询是否还报 `TypeError: Expected tuple, got Row`;BigQuery 是否还报 DOUBLE 参数类型不合法;SQL Server 里引号包裹的混合大小写表是否还被报成缺失;expect_column_values_to_be_of_type(type_="Numeric") 在 float 列上的结果有没有变。 回滚:`pip install 'great_expectations==1.23.1' 'sqlalchemy<2.1'`。这两步必须一起退,因为 1.23.1 配 SQLAlchemy 2.1.0 本身就是坏组合。 适用条件:Python 3.10 完全不受影响;用 snowflake 或 databricks extra 的装机停在 2.0.x 是设计行为,不是装错;只有裸装 great_expectations 的用户本来就不受这条变更影响。 ## 十一、同一版本顺带修掉的后端行为差异 1.23.2 覆盖 11 个 PR,署名贡献者 6 位,其中 2 位标注为首次贡献。除 SQLAlchemy 2.1 这条主线之外,还改了三处跨后端一致性,都会改变校验结论: - ClickHouse 不支持 regexp_like(),四条正则 Expectation 现在在 ClickHouse 上可用,并且进了集成测试。 - Snowflake 的 REGEXP 运算符会把模式锚定到整个值,导致不带锚的写法在 Snowflake 与其他后端结果不同;现在统一为子串语义,例如 ExpectColumnValuesToMatchRegex(column="name", regex="ell") 在两边一致。 - Spark 引擎的 column.value_counts 指标现在支持 address.city 这类点分嵌套 struct 路径,依赖它的 ExpectColumnMostCommonValueToBeInSet 与 ExpectColumnKLDivergenceToBeLessThan 不再返回空结果加 unresolved-column 报错。 顺带给出的还有四处较小的行为修正:ClickHouse 的可空列类型比较现在针对底层 SQL 类型而不是 Nullable(T) 包装;UnexpectedRowsExpectation 不再把字符串字面量、join_date 这类列名、引号标识符或注释里出现的 JOIN 字样误判成 JOIN,MySQL 与 SQL Server 上不再报语法错误;多线程构建 validator 在 Python 3.10 与 3.11 上不再串行或死锁;验证结果里的无穷 Decimal 改为序列化成浮点无穷大,不再抛 decimal.InvalidOperation。 ## 十二、与本站既有条目的差异 本站此前讲自托管组件升级风险的条目,一条针对 Prefect 3.8.7 的数据库迁移与 worker 配置项,一条针对 DuckDB 的 hf:// 直查写法,作用对象分别是工作流引擎的表结构与查询写法。 本条的作用对象不同:它落在数据质量框架与底层数据库抽象层之间的依赖面上,读者要动手的地方是依赖上限、驱动回退与方言级编译规则,不是自己写的业务 SQL。前置部署 Forward Deployed Engineer(FDE)可以把这两类条目配合使用——一条说明客户现场自托管组件的升级风险长什么样,另一条说明依赖面出问题时,怎样在不改业务代码的前提下退回去。 ## 原始来源 原始来源: https://github.com/fivetran/great_expectations/releases/tag/1.23.2 https://github.com/fivetran/great_expectations/pull/12269 https://github.com/fivetran/great_expectations/pull/12267 https://github.com/fivetran/great_expectations/pull/12268 https://github.com/sqlalchemy/sqlalchemy/releases/tag/2.1.0 https://github.com/sqlalchemy/sqlalchemy/releases/tag/2.1.1 https://pypi.org/project/great_expectations/1.23.2/ https://github.com/snowflakedb/snowflake-sqlalchemy/issues/652