跳到主要内容

7 篇博文 含有标签「debugging」

查看所有标签

Airflow 触发 DAG 没跑?重复 logical_date 被 409 拒绝

· 阅读需 5 分钟

调用 Airflow 的触发 API,请求正常发出、日志也打了「已触发」——回过头发现 DAG 根本没跑,任务面板里空空如也。

在开发 AI运营 时遇到此问题——基于大语言模型的智能分析,自动洞察市场趋势、用户行为、销售数据,提供精准运营策略。平台的批量补数脚本逐日触发分析 DAG,日期参数一重复,下游就整批静默缺数。

问题现象:API「成功」返回,DAG 却没跑​

触发请求发出后没有抛错,调用方的日志里写着触发成功——但 Airflow 里查不到对应的 run,任务一个都没执行。这类问题最阴险的地方在于:它不出现在触发方的报错里,而是出现在下游「怎么少了几天数据」的追问里。如果你搜的是「Airflow API 触发没反应」「dagRun 创建失败」「trigger dag no run」,都是同一类问题。

根因:Airflow 按 (dag_id, logical_date) 唯一约束拒绝重复触发​

Airflow 里同一个 DAG 的 logical_date 不能重复(dag_run_id 也是主键唯一)。用已存在的 logical_date 再触发一次,实测(Airflow 3)API 直接拒绝:

POST /api/v2/dags/{dag_id}/dagRuns
HTTP 409
{"detail": {"reason": "Unique constraint violation", ...}}

响应是 4xx,状态码和响应体都在说「没触发成功」——问题出在调用方怎么消费这个响应。最常见的三种漏判姿势:

  • 不校验状态码:请求没抛网络异常就当成功,4xx 响应体被扔掉;
  • 只判断「有响应」:把 resp.json() 解析成功当成触发成功,不看里面有没有 dag_run_id;
  • 错误信息被日志噪音淹没:409 的 detail 是一坨约束报错文本,grep 关键字对不上就没人看第二眼。

解决方案:换新 logical_date 触发,校验响应体里的 dag_run_id​

两件事缺一不可。触发侧,批量补数时每次用不同的 logical_date(天然按日期递增):

from datetime import date, timedelta

for i in range(days):
ld = (date(2026, 1, 1) + timedelta(days=i)).isoformat()
trigger(dag_id, logical_date=ld)

调用侧,把「响应体含 dag_run_id」定为唯一的成功判据,状态码只做辅助:

import requests

def trigger(dag_id: str, logical_date: str) -> str:
resp = requests.post(
f"{BASE}/api/v2/dags/{dag_id}/dagRuns",
headers=auth_headers(),
json={"logical_date": logical_date},
)
body = resp.json()
run_id = body.get("dag_run_id")
if not run_id:
# 409 = 该 logical_date 已有 run;其余 4xx/5xx 同样不是成功
raise TriggerFailed(f"{resp.status_code}: {body}")
return run_id

这个判据的好处是状态码无关:不管服务端返回 409 还是别的什么,只要响应里没有 dag_run_id,触发就没成立——一行判断覆盖所有失败形态。

边界与变体:重复触发被拒绝,有时恰恰是保护​

值得说清楚另一面:如果你的 logical_date 就是业务日期,唯一约束本身就是幂等保护。同一天的数据补跑两次,第二次被 409 拒绝,不会产生重复 run——这时该做的不是「想办法触发成功」,而是走 Airflow 的 clear 重跑已有 run。两种场景别搞混:

场景正确姿势
批量补数:每天一个新 run每次用新的 logical_date,触发前换日期
同一业务日重跑不重新触发,对已有 run 做 clear + 重跑
判断触发是否成立只认响应体里的 dag_run_id,状态码辅助

另外,手动触发不传 logical_date 时 Airflow 会生成 manual__<时间戳> 形态的 run_id 和对应 logical_date,天然不重复——踩坑的都是自己构造 logical_date 的调用方。

注意事项

  • 把 dag_run_id 校验写进触发封装,别散落在各调用点——漏一处就是一批静默缺数。
  • 409 的 detail 文本是约束报错,做监控告警时按状态码分类,别指望 grep 消息关键词。
  • logical_date 用业务日期是最佳实践:既语义清晰,又白拿一层幂等保护。

常见问题​

Airflow 用 API 重复触发 DAG 会怎样?​

被唯一约束拒绝:实测重复 logical_date 触发返回 HTTP 409(Unique constraint violation),响应体无 dag_run_id,DAG 不会运行。调用方只打日志不看响应就会静默失败。

为什么 Airflow dagRun 触发了却没实际运行?​

最常见原因是重复 logical_date:Airflow 按 (dag_id, logical_date) 唯一约束拒绝重复 run,请求被 409 拒绝但调用方没校验。结论:收到响应后必须确认响应体含 dag_run_id 才算触发成功。

Airflow 的 logical_date 可以重复吗?​

同一 DAG 下不能重复——(dag_id, logical_date) 有唯一约束,dag_run_id 也是主键。重复触发同一 logical_date 是被设计为拒绝的,这个拒绝恰好可以当幂等保护用。

CCLEE

独立开发者,24年电商行业实战经验,专注将AI能力落地于真实商业场景。

合作咨询

Airflow 3 密码改了不生效?真源是 passwords.json 而非数据库

· 阅读需 5 分钟

在 Airflow 3 上把用户密码更新之后,用新密码请求 /auth/token 仍返回 401——而数据库里的密码哈希确实已经改成了新值。

在开发 AI运营 时遇到此问题——基于大语言模型的智能分析,自动洞察市场趋势、用户行为、销售数据,提供精准运营策略。平台的 DAG 触发链路依赖这套 Airflow 的 JWT 鉴权,密码轮换卡住,管道授权跟着停摆。

问题现象:密码改了,/auth/token 还认旧密码​

新密码请求 /auth/token 返回 401、旧密码仍返回 201——修改动作每一步都「成功」,实际生效的始终是旧密码。这次轮换的场景是安全处置:旧密码已泄露,必须立刻换掉,所以「改了但没生效」的每一分钟都在裸奔。

第一反应是用官方 CLI 重置:

$ airflow users reset-password -u apiuser -p <new>
AttributeError: 'AirflowSecurityManagerV2' object has no attribute 'find_user'

报错指向 FAB(Flask-AppBuilder)认证体系的安全管理器,看起来像 3.x 的版本 bug。既然 CLI 坏了,那就绕过它直接改数据库——这步走岔了,为后面更深的迷惑埋了伏笔。

根因:SimpleAuthManager 不读数据库,密码在 passwords.json​

这套部署的认证管理器不是报错里暗示的 FAB,而是 SimpleAuthManager——用一条命令就能看到真相:

airflow config get-value core auth_manager
# airflow.api_fastapi.auth.managers.simple.simple_auth_manager.SimpleAuthManager

理清这三层,现象就完全解释得通了:

  • CLI 报错是假导。airflow users 系列 CLI 属于 FAB 认证体系;SimpleAuthManager 部署上跑它,内部代码路径直接撞上 AttributeError。报错里的 AirflowSecurityManagerV2 类确实存在(作为遗留组件),但跟当前生效的认证管理器无关。
  • ab_user 表是遗留数据。从 Airflow 2.x 升级上来的部署,库里留着 FAB 时代的用户表。用 SQLAlchemy 直写 UPDATE ab_user SET password=... (werkzeug generate_password_hash 生成 scrypt 哈希),返回 rowcount=1,再用 check_password_hash 验证——新密码与库里的哈希完全匹配。可 /auth/token 照旧 401:表是真改了,只是认证流程根本不读它。
  • 真源是一个 JSON 文件。SimpleAuthManager 的用户密码来自 AIRFLOW__CORE__SIMPLE_AUTH_MANAGER_PASSWORDS_FILE 指向的 passwords.json,内容就是扁平映射:
{
"apiuser": "<密码>",
"admin": "<密码>"
}

用户-角色对应关系则由 AIRFLOW__CORE__SIMPLE_AUTH_MANAGER_USERS(形如 apiuser:admin,admin:admin)声明。排查这类问题的第一步应该是确认「谁在管认证」,而不是急着修「认证坏了」的表象。

解决方案:改 passwords.json 后必须 force-recreate 容器​

三步:

  1. 改宿主机上的 passwords.json(compose 挂载进容器的那个源文件):
python3 - <<'EOF'
import json
d = json.load(open('/root/workspace/ai_dag/deploy/passwords.json'))
d['apiuser'] = '<新密码>'
json.dump(d, open('/root/workspace/ai_dag/deploy/passwords.json', 'w'), indent=2)
EOF
  1. 重建 api-server 容器。这一步不可省——密码文件仅启动时读取,改文件对运行中的进程没有任何影响:
cd /root/workspace/ai_dag/deploy
docker compose up -d --force-recreate airflow-api-server
  1. 等服务就绪后双向验证——新密码必须通过、旧密码必须失效,两个断言缺一不可:
# 探活:重建后约半分钟内恢复 200
curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/api/v2/monitor/health

# 新密码 → 期望 201
curl -s -o /dev/null -w '%{http_code}' -X POST http://localhost:8080/auth/token \
-H 'Content-Type: application/json' \
-d '{"username":"apiuser","password":"<新密码>"}'

# 旧密码 → 期望 401
curl -s -o /dev/null -w '%{http_code}' -X POST http://localhost:8080/auth/token \
-H 'Content-Type: application/json' \
-d '{"username":"apiuser","password":"<旧密码>"}'

实测结果:新密码 201、旧密码 401,轮换闭环。只验「新密码能用」不验「旧密码失效」是这类操作最常见的收尾漏洞。

注意事项

  • 先确认认证管理器再动手:airflow config get-value core auth_manager 的输出决定密码改在哪——FAB 在数据库,SimpleAuthManager 在文件,两者路径完全不同。
  • 改密码文件必须重建容器:文件仅启动时读取,docker compose restart 不会让它重新加载。
  • 依赖该认证的服务要同步换新密码并重启加载,否则管道在「旧密码 401、新密码未接线」的窗口里全断。
  • 重建 api-server 有秒级到半分钟的服务中断,挑低峰执行,探活循环等 health 回 200 再做验证。

常见问题​

Airflow 2 升级到 3 后重置密码的命令为什么报错?​

3.x 部署若使用 SimpleAuthManager,FAB 的 airflow users 系列 CLI 不再可用(实测报 AttributeError: 'AirflowSecurityManagerV2' object has no attribute 'find_user')。先跑 airflow config get-value core auth_manager 确认实际认证管理器,再决定改数据库还是改文件。

SimpleAuthManager 的用户密码存在哪?​

存在 passwords.json 文件里(flat JSON,形如 {"用户名": "密码"}),路径由 AIRFLOW__CORE__SIMPLE_AUTH_MANAGER_PASSWORDS_FILE 指定。文件仅容器启动时读取,改完必须 docker compose up -d --force-recreate airflow-api-server 才生效。

为什么直接更新 ab_user 表的密码哈希不生效?​

ab_user 是 FAB 认证管理器的用户表。从 2.x 升级到 SimpleAuthManager 的部署里它是遗留数据,认证流程根本不读它——实测 rowcount=1 写入 scrypt 哈希成功,/auth/token 照旧返回 401。

CCLEE

独立开发者,24年电商行业实战经验,专注将AI能力落地于真实商业场景。

合作咨询

pandas NaN 让 json.dumps 报错?异常炸在你的 try 之外

· 阅读需 6 分钟

数据管道里一个任务跑到一半戛然而止:日志的 trace 停在中间步骤,没有任何 error 记录,仿佛进程凭空消失。

在开发 AI运营 时遇到此问题——基于大语言模型的智能分析,自动洞察市场趋势、用户行为、销售数据,提供精准运营策略。分析任务从 pandas 数据起步、以 JSON 落库收尾,NaN 是这条路上最常见的隐形地雷。

问题现象:trace 中断,无 error 记录​

任务状态是失败,但业务日志里找不到任何报错——trace 走到某一步就没了下文。异常确实发生了,只是它炸的位置不在你的 try 覆盖范围内:排查发现是 ti.xcom_push 内部做 JSON 序列化时抛的错,而 xcom_push 这一行在业务 try 块之外(即便包进去,框架内部更深层的序列化点也照样在你的 catch 半径之外)。

真身是这个报错:

ValueError: Out of range float values are not JSON compliant: nan

如果你搜的是「json.dumps NaN 报错」「Out of range float values are not JSON compliant」「任务日志中断无 error」,都是同一类问题。

根因:NaN 是 Python 的合法 float,JSON 却不认识它​

两套规则在这里错位。实测行为矩阵:

import json, math

# NaN 在 Python 世界畅通无阻
isinstance(float('nan'), float) # True —— 合法 float
float('nan') == float('nan') # False —— 连等号都测不出来

# JSON 世界:默认宽松,严格模式直接抛
json.dumps(float('nan')) # 'NaN'(非法 JSON 字面量!)
json.dumps(float('nan'), allow_nan=False) # ValueError: Out of range float values...
json.dumps(math.inf, allow_nan=False) # ValueError(±Inf 同罪)

三个要点:

  • JSON 规范(RFC 8259)的数字语法不含 NaN/Infinity。Python json 模块默认 allow_nan=True,遇到 NaN 输出 NaN 字面量——这是对规范的宽松扩展,产出的字符串下游严格解析器会拒绝。坑分两层:要么现在炸(严格模式),要么埋给下游炸(宽松模式)。
  • NaN 骗过常规判空。它不是 None、不是 0、== 自己都返回 False——if not value 一类的卫语句统统放行,数据一路走到序列化层才爆。
  • 爆点在框架代码里。业务代码把 dict 交给 xcom_push、日志 SDK、HTTP 客户端——序列化发生在这些框架内部,深于你的 try/catch 半径。这解释了「trace 中断且无 error 记录」:异常没进你的日志埋点,直接把 task 掀翻。

解决方案:边界前递归清洗,兜底交给任务级钩子​

两道防线。第一道在数据侧——跨 JSON 边界之前递归清洗,NaN 和 ±Inf 一律转 None:

import math

def json_safe(value):
"""递归把 NaN/±Inf 转成 None,其余原样返回。"""
if isinstance(value, float) and (math.isnan(value) or math.isinf(value)):
return None
if isinstance(value, dict):
return {k: json_safe(v) for k, v in value.items()}
if isinstance(value, list):
return [json_safe(v) for v in value]
return value

import json
payload = json_safe(result_dict)
json.dumps(payload, allow_nan=False) # 现在永不抛

清洗之后仍保留 allow_nan=False——它从「炸雷」变成「哨兵」:万一有漏网的非法值,在自家代码里抛出来,好过流到下游。

第二道在框架侧——给任务挂失败钩子,把 catch 外的异常也落进日志体系(Airflow 场景用 task failure callback 或装饰器包住整个 task callable),保证「trace 中断无 error」变成「trace 末端有 error」。排查存量问题时,去 Airflow 任务日志翻 traceback(容器内 logs/dag_id=.../task_id=.../attempt=N.log)比翻业务日志快。

边界与变体​

  • pandas 自带的 to_json 会在输出层把 NaN 转 null,如果整条链路用它输出,可以不清洗;但数据一旦转成 dict 再走 json.dumps,就得自己清洗——坑出现在「换序列化器」的那一刻。
  • NaN == NaN 为 False,去重、断言、测试里的相等比较都会被它骗;判 NaN 只能用 math.isnan。
  • numpy 数组直接 dumps 也会炸(ndarray 不是 JSON 可序列化类型),先 .tolist();tolist 之后 NaN 还在,清洗逻辑依然需要。
  • 数值语义上 NaN→null 是一次有损转换(「测量失败」变成「没有值」),下游如果依赖这个区分,应另立字段标注,而不是硬转。

注意事项

  • 判 NaN 只用 math.isnan,任何基于 ==、if not x 的判断都不可靠。
  • 清洗放在序列化边界前统一做一次,别在每个调用点各自为战——漏一个调用点就复现一次。
  • 严格模式(allow_nan=False)当哨兵用:清洗后仍报错说明有漏网数据,这是好信号。
  • 框架兜底钩子(failure callback)不是可选品:它能接住你今天想不到的、明天一定出现的「catch 之外的异常」。

常见问题​

json.dumps 遇到 NaN 为什么报 ValueError?​

JSON 规范没有 NaN/Infinity;allow_nan=False 开启严格模式后遇到它们即抛 ValueError: Out of range float values are not JSON compliant。默认 allow_nan=True 会输出非法 JSON 的 NaN 字面量,埋给下游。

pandas 的 NaN 怎么转成 null 再序列化?​

递归遍历数据结构,把 float 类型的 NaN 和 ±Inf 替换为 None 再 dumps;pandas 层可用 df.where(df.notna(), None) 或 to_json(自带 NaN→null)。结论:清洗要在序列化边界前做,别指望 json 模块替你转。

JSON 为什么不支持 NaN 和 Infinity?​

JSON 规范(RFC 8259)的数字语法只覆盖有限数,NaN/Infinity 不是合法值。Python json 默认放行输出了 NaN 字面量,属于对规范的宽松扩展——下游严格解析器会拒绝。

CCLEE

独立开发者,24年电商行业实战经验,专注将AI能力落地于真实商业场景。

合作咨询

跨容器连 PostgreSQL 报 fe_sendauth?exec 免密≠TCP 免密

· 阅读需 5 分钟

在 DB 容器所在宿主机上 docker exec 进容器跑 psql,免密直连一切正常;同样的用户名换到另一个容器里走 TCP 连接,直接报 fe_sendauth: no password supplied。

在开发 AI运营 时遇到此问题——基于大语言模型的智能分析,自动洞察市场趋势、用户行为、销售数据,提供精准运营策略。编排容器里的运维脚本要跨容器访问分析数据库,连接串照搬了 exec 习惯,第一跳就摔在认证上。

问题现象:exec 能连,TCP 报 fe_sendauth: no password supplied​

同一台宿主机、同一个数据库、同一个用户,两种连法两种命运。实测三方对照(Airflow 容器 → PostgreSQL 容器):

连接方式结果
docker exec cclhub-db psql -U postgres(容器内 socket)免密直连成功
跨容器 TCP,DSN 不带密码fe_sendauth: no password supplied
跨容器 TCP,DSN 带密码连接成功

如果你搜的是「fe_sendauth no password supplied」「psql 不带密码报错」「docker 连 postgres 免密失败」,都是这一类。

根因:socket 免密与 TCP 密码认证是两条 pg_hba 路径​

PostgreSQL 的认证由 pg_hba.conf 按连接类型逐条匹配,官方镜像的默认配置对两条路径给的是完全不同的规则:

  • 容器内 docker exec 走 Unix domain socket,对应 local all all trust 一类的规则——信任本地 socket,不问密码;
  • 跨容器走 TCP(host 类型规则),官方镜像默认要求 scram-sha-256 / md5 密码认证。

于是「exec 免密」建立起来的连接习惯,到了 TCP 上就是缺密码——fe_sendauth: no password supplied 的字面意思正是「服务端要密码,客户端一个都没给」。

顺带把两个容易混淆的错误分开:no password supplied 是没带密码,password authentication failed 是带了但错了。前者查连接串里有没有 password,后者查密码对不对——对症的药不同。

解决方案:DSN 带密码,别照搬 exec 的连接习惯​

跨容器脚本统一用带密码的连接串:

postgresql://postgres:<密码>@<db 主机>:5432/<库名>

密码来源推荐直接读 DB 容器的环境变量(官方镜像的 POSTGRES_PASSWORD 就是为初始化设置的),避免二次硬编码:

PW=$(docker exec cclhub-db printenv POSTGRES_PASSWORD)
psql "postgresql://postgres:${PW}@localhost:5432/postgres" -c "SELECT 1"

Python/应用侧同理,DSN 进环境变量管理:

import os
import psycopg

DSN = os.environ["APP_DB_DSN"] # postgresql://user:pass@host:5432/db
with psycopg.connect(DSN) as conn:
conn.execute("SELECT 1")

注意 URL 里的密码若含 @ : / # 等保留字符需要百分号编码——又一个特殊字符咬人的场景,生成数据库密码时规避这类字符能省掉一整类麻烦。

边界与变体​

  • 报错形态随客户端变:libpq 交互式终端(不带 -t 的 psql)会先提示 Password for user postgres: 再因无输入报 fe_sendauth;连接池、GUI 工具(如 pgAdmin)则直接在界面标认证失败——错误文本不同,根因相同。
  • .pgpass 文件与 PGPASSWORD 环境变量是 DSN 之外的两种供密方式,容器场景里 DSN/env 注入比挂 .pgpass 文件更常见。
  • pg_hba 允许改成 trust 让 TCP 也免密——技术上可行,生产环境不要做:任何能触达端口的进程都拿得到无凭据访问。
  • 认证方法版本差异:新版本官方镜像默认 scram-sha-256,老客户端库可能不支持——那是另一类错误(认证方法不支持),与本文的「没带密码」不同。

注意事项

  • 排障先看连接路径:socket 还是 TCP、命中 pg_hba 哪条规则,决定了要不要密码——别把 exec 的行为当成全局行为。
  • 错误语义要分清:no password supplied(没带)与 authentication failed(带错)的修复动作完全不同。
  • DSN 里的密码做特殊字符规避或百分号编码,URL 保留字符会静默改变解析结果。

常见问题​

fe_sendauth: no password supplied 是什么意思?​

服务端 pg_hba.conf 要求密码认证,但客户端连接串里没有提供密码。它不是「密码错误」(那是 authentication failed),是「没带密码」——检查 DSN 是否缺 password 部分。

为什么 docker exec 进容器 psql 不用密码就能连?​

容器内默认走 Unix domain socket,pg_hba 对本地 socket 配置了 trust 免密;跨容器是 TCP 连接,命中要求密码的 host 规则。同一套命令换个连接路径,认证要求完全不同。

容器里脚本连 PostgreSQL 的正确姿势是什么?​

DSN 显式带密码,如 postgresql://user:password@host:5432/db。密码从 DB 容器的 POSTGRES_PASSWORD 环境变量读取注入,不要硬编码,也不要指望 socket 免密在 TCP 上生效。

CCLEE

独立开发者,24年电商行业实战经验,专注将AI能力落地于真实商业场景。

合作咨询

PostgreSQL jsonb 字段判空失效?JSON null 不是 SQL NULL

· 阅读需 5 分钟

用 IS NOT NULL 过滤 jsonb 字段的「空值」,结果 JSON null 的行照样通过——数据明明是空的,判断却为真。

在开发 AI运营 时遇到此问题——基于大语言模型的智能分析,自动洞察市场趋势、用户行为、销售数据,提供精准运营策略。分析结果的决策快照以 jsonb 存储,规则未触发的行写入的是 JSON null,「有没有规则输出」这个判断因此失真。

问题现象:IS NOT NULL 拦不住的「空值」​

字段->'key' IS NOT NULL 对值为 JSON null 的行返回 true——过滤条件形同虚设,本该被排除的行混进了结果集。如果你搜的是「jsonb 判空不生效」「jsonb 空值过滤不掉」「IS NOT NULL 对 json 无效」,都是同一个问题。

根因:JSON null 是合法 jsonb 值,不是 SQL NULL​

PostgreSQL 里 jsonb 有两套「空」:SQL NULL 表示值不存在,JSON null 是一个合法的 jsonb 值。-> 取出一个值为 JSON null 的键,拿回来的是后者——IS NOT NULL 判断的是「拿回来的东西存不存在」,不是「拿回来的是不是有意义的值」。直接看实测矩阵(PostgreSQL 16 容器实测):

表达式结果
('{"a":null}'::jsonb -> 'a') IS NOT NULLtrue
('{"a":1}'::jsonb -> 'b') IS NULLtrue(缺键返回 SQL NULL)
jsonb_typeof('{"a":null}'::jsonb -> 'a')'null'(字符串)
jsonb_typeof('{"a":1}'::jsonb -> 'b')SQL NULL
'{"a":null}'::jsonb ? 'a'true
('{"a":null}'::jsonb ->> 'a') IS NULLtrue

两个最容易踩的组合:

  • IS NOT NULL 分不出「JSON null」和「真值」——第一行,这正是误判的来源。
  • ->> 取文本后判 IS NULL 同样分不出——JSON null 和缺键都变成 SQL NULL,第六行。想用取文本绕过这个坑,会原样掉进去。

解决方案:判键用 ?,判值用 jsonb_typeof​

两件事要在查询里分开表达,用对工具就不会再混:

-- 判「键存在」:值是不是 null 无所谓
SELECT '{"a":null}'::jsonb ? 'a'; -- true

-- 判「有真值」:断言具体 JSON 类型,'null'、缺键一律排除
SELECT jsonb_typeof(config->'rule_output') = 'array'; -- 数组才算
SELECT jsonb_typeof(config->'rule_output') = 'object'; -- 对象才算

-- 找出「写了 JSON null」的行(数据排查用)
SELECT * FROM t WHERE jsonb_typeof(config->'rule_output') = 'null';

我们踩的具体场景:规则引擎在「规则未触发」时往决策快照里写 JSON null,下游用 ->'rule_output' IS NOT NULL 统计「有规则输出的行」,统计口径全错。修复是把断言收紧为 jsonb_typeof(...) = 'array'——只认数组,JSON null、缺键、其他类型一次排除。配套的 JSON null 判定此前用的是 IS NOT NULL,同批修正。

边界与变体:JSON null 不总是错的​

先说清楚:JSON null 本身是合法设计——「键存在但值为空」和「键不存在」是两种业务语义,API 返回 "extra": null 与不返回 extra 键不是一回事。错的不是数据,是拿 IS NOT NULL 去 做「有值」判断。三种意图对应三种写法:

意图写法
键存在(不管值)jsonb ? 'key'
有指定类型的真值jsonb_typeof(x) = 'array' / 'object' / ...
值为 JSON nulljsonb_typeof(x) = 'null'

另一个隐藏差异在更新侧:jsonb_set(config, '{rule_output}', 'null') 会写入 JSON null,jsonb_set(config, '{rule_output}', NULL) 则把整个键删掉——参数给的是 JSON 还是 SQL NULL,语义完全不同。同是「查询结果与预期不符」的排查,跨查询粒度错位导致的悬空引用是另一类高频根因,可以对照着看。

注意事项

  • 存量数据先摸底再改判定:用 jsonb_typeof(...) = 'null' 跑一遍全表,确认 JSON null 行的规模和来源,再决定是修查询还是修写入方。
  • 团队内统一写法:即便某字段当前恒为合法数组、IS NOT NULL 暂时不会出错,也统一用 jsonb_typeof 断言——字段语义一变,宽松写法就是静默错误。
  • ORM/驱动层同理:应用代码里判「JSON 字段非空」时,注意驱动把 JSON null 映射成什么(多数语言映射为语言级 null/None),别把两层空混为一谈。

常见问题​

PostgreSQL jsonb 里 JSON null 和 SQL NULL 有什么区别?​

JSON null 是合法的 jsonb 值,SQL NULL 表示「值不存在」。实测 ('{"a":null}'::jsonb -> 'a') IS NOT NULL 返回 true;jsonb_typeof 对前者返回字符串 'null',对缺键返回 SQL NULL。

为什么 jsonb 字段 IS NOT NULL 过滤不掉空值?​

->'key' 对 JSON null 返回的是合法 jsonb 值而非 SQL NULL,IS NOT NULL 自然为 true。要判「有真值」用 jsonb_typeof(字段->'key') = 'array' 这类类型断言,要判「键存在」用 ? 操作符。

jsonb 怎么判断键存在还是值为 null?​

键存在用 ? 操作符:'{"a":null}'::jsonb ? 'a' 为 true,不受值是否为 null 影响。值为 JSON null 时 jsonb_typeof 返回 'null',缺键时返回 SQL NULL——二者据此区分。

CCLEE

独立开发者,24年电商行业实战经验,专注将AI能力落地于真实商业场景。

合作咨询

停用插件后主题 JS 失效?检查 Query Monitor 的 footer scripts 回调

· 阅读需 4 分钟

在为客户开发 WordPress 主题时遇到此问题,记录根因与解法。

TL;DR​

WordPress 主题的导航滚动变色功能(.is-scrolled 类)在停用 WooCommerce 插件后失效。排查发现是 Query Monitor 插件的 action_print_footer_scripts 回调优先级 9999 太低,提前执行并终止了所有 footer scripts 输出。删除 Query Monitor 后问题解决。