OneUptime API 查询操作符 GreaterThanOrNull 详解:请求格式、语义边界与数据库层实现
【免费下载链接】oneuptimeComplete open-source monitoring and observability platform.项目地址: https://gitcode.com/GitHub_Trending/on/oneuptime
GreaterThanOrNull 是 OneUptime 开放 API 中用于构造查询条件的操作符之一:它匹配"字段值大于指定值,或者该字段为 NULL"的记录。本文以 API 参考文档中的示例为起点,结合仓库源码逐层剖析该操作符的 JSON 请求格式、序列化机制、PostgreSQL 与 ClickHouse 两侧的 SQL 生成逻辑,并给出可验证的测试依据,帮助你在调用 OneUptime 查询 API(或为自定义字段编写过滤条件)时准确使用这一语义。
操作符语义:比"大于"多覆盖一类记录
在 OneUptime 的 API 参考文档中,GreaterThanOrNull的官方定义是:
A query filter that matches objects where a field is greater than the specified value or is null.
(参见 DataTypes.ts 中的类型注册表。)
也就是说,它等价于 SQL 中的field > value OR field IS NULL。与普通的GreaterThan(仅匹配field > value)相比,它额外把"该字段没有值"的记录也纳入结果集。这一语义在两类场景下尤为关键:
- 稀疏数据的统计查询:例如监控事件中某字段尚未被采集端填充(为 NULL),但你希望这类记录仍然出现在"字段值大于阈值"的过滤结果里,而不是被静默丢弃;
- 自定义字段(custom fields)过滤:项目中的自定义字段并非每行记录都会填写,使用
GreaterThanOrNull可以保证"未填写的行"与"填写且超过阈值的行"同时命中。
请求格式:一个 JSON 片段即可表达
API 参考文档中给出的示例(原文档位于 GreaterThanOrNull.md)是一个可直接放入查询体query字段的 JSON 对象:
{ "query": { "age": { "_type": "GreaterThanOrNull", "value": 10 } } }其中:
| 字段 | 类型 | 说明 |
|---|---|---|
query | 对象 | 查询条件容器,键为字段名,值为该字段的条件表达式 |
age | 字段名 | 被过滤的目标字段,可以是模型上的普通列,也可以是 jsonb 自定义字段 |
_type | 字符串 | 操作符类型标识,固定为"GreaterThanOrNull",服务端据此反序列化为对应的操作符实例 |
value | number / Date / string | 比较基准值;下例为10,表示"age 大于 10 或 age 为 NULL" |
value的合法类型由CompareType约束为number | Date | string,定义见 CompareBase.ts。因此下面的写法同样合法:
{ "query": { "createdAt": { "_type": "GreaterThanOrNull", "value": "2026-01-01T00:00:00.000Z" } } }序列化与反序列化:客户端到服务端的类型往返
GreaterThanOrNull在 TypeScript 侧是一个继承自CompareBase<T>的值对象类,核心实现位于 GreaterThanOrNull.ts:
toJSON()返回{ _type: "GreaterThanOrNull", value: <原始值> }。这里刻意携带原始值(而非toString()的结果),因为toString()对Date会调用asDateForDatabaseQuery,在本地时区将日期折叠为"仅日期"字符串,导致从浏览器发出的查询边界最多偏移一天;而JSON.stringify会把原始Date渲染为完整 ISO 时间戳,服务端能以全精度绑定参数。这一点在类内的注释以及测试 GreaterThanOrNull.test.ts 中均有明确说明。toString()对Date值会先经OneUptimeDate.asDateForDatabaseQuery归一化后再输出字符串,用于服务端生成 SQL 时的参数文本。fromJSON()校验_type必须等于"GreaterThanOrNull",否则抛出BadDataException(见 GreaterThanOrNull.ts),保证反序列化不静默接受非法输入。
服务端转换:QueryUtil 如何识别并翻译该操作符
请求到达服务端后,查询构建器会遍历query中的每个键,并在 QueryUtil.ts 中识别GreaterThanOrNull实例:
} else if ( query[key] && query[key] instanceof GreaterThanOrNull && tableColumnMetadata ) { query[key] = QueryHelper.greaterThanOrNull( (query[key] as LessThanOrNull<CompareType>).toString() as any, ) as any; }随后调用 QueryHelper.ts 中的greaterThanOrNull生成 SQL 片段:
return Raw( (alias: string) => { return `(${alias} > :${rid} or ${alias} IS NULL)`; }, { [rid]: value }, ) as FindWhereProperty<any>;可以看到,普通关系列上的最终 SQL 语义正是(列 > :value or 列 IS NULL),参数通过随机命名的绑定参数传入,避免直接拼接用户输入。@CaptureSpan()装饰器同时为该操作接入链路追踪,便于在 OneUptime 可观测体系中定位慢查询。
jsonb 自定义字段场景:更复杂的组合谓词
当目标字段位于 jsonb 列(例如customFields)内部时,GreaterThanOrNull走的是另一条独立的执行路径。在 JSONColumnQuery.ts 中:
if (value instanceof GreaterThanOrNull) { return this.join( [this.compareNumeric(">=", value.value), this.isEmpty()], "OR", ); }这段代码揭示了两个值得注意的实现细节:
数值比较受类型保护。jsonb 键对应的值是什么类型只有运行时才知道,因此比较前会通过
numericExpression把值包进一个CASE WHEN:仅当文本匹配^\s*-?[0-9]+(\.[0-9]+)?\s*$(可选符号的十进制数,见 JSONColumnQuery.ts)时才执行CAST(... AS NUMERIC),否则返回NULL。这避免了CAST('abc' AS NUMERIC)直接中止整条查询的隐患。"空"是一组并集条件。
isEmpty()将以下四种情况统一视为"字段为空"(JSONColumnQuery.ts):- 键缺失(
col -> 'key' IS NULL); - 显式 JSON
null; - 空字符串
''; - 空数组(多选框清空后遗留的
[])。
因此在 jsonb 列上,
GreaterThanOrNull实际表达为"数值>=阈值,或键缺失、为 null、为空串、为空数组"的析取。此外,该模块还设有查询防护上限:单次 JSON 列过滤最多携带 50 个键、每个键最多匹配 200 个值、键名最长 500 字符(JSONColumnQuery.ts),超出即返回 400,防止手工构造的超大请求拖垮数据库。- 键缺失(
ClickHouse 分析库场景:统一的操作符词汇表
OneUptime 的时序/分析类模型(AnalyticsModels)查询经由 ClickHouse 路径执行。在 Statement.ts 中,GreaterThanOrNull与LessThan、GreaterThan、LessThanOrNull等一起被识别为"比较型操作符",其value被直接提取用于后续 SQL 参数绑定:
} else if ( v.value instanceof LessThan || v.value instanceof LessThanOrEqual || v.value instanceof GreaterThan || v.value instanceof GreaterThanOrEqual || v.value instanceof LessThanOrNull || v.value instanceof GreaterThanOrNull || v.value instanceof NotEqual ) { finalValue = v.value.value; }而语句生成器 StatementGenerator.ts 则把操作符翻译为 ClickHouse 的 WHERE 谓词,并在对应属性路径上叠加IS NULL分支。从源码结构看,这套"标准列(PostgreSQL)+ jsonb 列(PostgreSQL)+ 分析列(ClickHouse)"三路并行的实现,正是为了确保同一组过滤条件在产品内不同数据源上读出一致的结果。
测试验证:行为契约的硬性保证
仓库为GreaterThanOrNull提供了独立单元测试 GreaterThanOrNull.test.ts,覆盖以下行为契约:
- 以合法值构造对象并读写
value; toString()输出原始值的字符串形式(42→"42");toJSON()输出{ _type: "GreaterThanOrNull", value: 42 },即携带原始数值而非字符串;- Date 值保留完整时间戳:
new Date("2026-07-21T14:35:12.345Z")经JSON.stringify(obj.toJSON())后仍为"2026-07-21T14:35:12.345Z",验证了"旧实现把 Date 折叠成本地时区日期导致查询边界偏移一天"这一缺陷已修复; fromJSON()对合法输入正确还原实例,对_type错误的输入抛出BadDataException。
此外,在 JSONColumnQuery.test.ts 与 CompareOperatorWireSerialization.test.ts 中也有该操作符参与 jsonb 谓词构建与线格式序列化往返的覆盖,保证它与其他比较操作符共享同一套可序列化协议。
与其他操作符的取舍对比
在 API 参考文档的 DataTypes 目录下(DataTypes),GreaterThanOrNull与一组同族操作符并列存在,选择时可以参考以下边界:
| 操作符 | 语义(匹配条件) | 典型用途 |
|---|---|---|
GreaterThan | 字段值>阈值 | 严格大于 |
GreaterThanOrEqual | 字段值>=阈值 | 含边界的大于 |
GreaterThanOrNull | 字段值>阈值或字段为 NULL | 稀疏字段的"超过阈值或未填写" |
LessThanOrNull | 字段值<阈值或字段为 NULL | 稀疏字段的"低于阈值或未填写" |
EqualToOrNull | 字段值==值或字段为 NULL | 稀疏字段的精确匹配或未填写 |
IsNull/NotNull | 字段为空 / 非空 | 单独判断空值 |
需要说明的一个实现细节是:在普通关系列上,QueryHelper.greaterThanOrNull生成的谓词是严格大于(>);而在 jsonb 自定义字段列上,JSONColumnQuery对GreaterThanOrNull使用的是>=数值比较。两种数据路径下的严格/非严格差异以当前仓库源码为准,编写跨数据源一致的过滤逻辑时应留意这一行为。
小结
GreaterThanOrNull是 OneUptime 查询协议中"带空值兜底"的一类比较操作符:请求侧只需一个_type+value的 JSON 片段,服务端则会依据目标列的类型(普通列、jsonb 自定义字段、ClickHouse 分析列)分别生成对应的 SQL 谓词,并通过绑定参数、数值类型保护和查询上限等手段保证安全性与稳定性。配合 GreaterThanOrNull.test.ts 中的行为契约,你可以放心地在监控、事件与自定义字段过滤场景中使用该操作符,让"未填写的记录"不再从查询结果中丢失。
【免费下载链接】oneuptimeComplete open-source monitoring and observability platform.项目地址: https://gitcode.com/GitHub_Trending/on/oneuptime
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考