API
端点
| 方法 · 路径 | 功能 |
|---|---|
POST /api/v1/query | 读取查询。全部结果以 JSON 数组返回。达到上限时设置 truncated 标志 |
POST /api/v1/query/stream | 将相同查询以 NDJSON 形式返回 — 每行一条记录 |
POST /api/v1/update | 写入查询(拒绝 TQL) |
POST /api/v1/format | 查询语句格式化 · 缩进 |
POST /api/v1/cache | 查询单个内存中的当前值 |
GET·POST·DELETE /api/v1/keys · /keys/{id} | API 密钥列表 · 签发 · 吊销 |
GET·POST /api/v1/login | 控制台登录 — 在 /api/v1 下唯一无需密钥即可通过的路径 |
GET /api/v1/openapi.json | OpenAPI 3.0.3 文档(需要认证) |
GET /api/health | 匿名就绪检查 — 数据库 · 内存 · 查询日志 |
GET /admin/metrics · /ping · /healthcheck · /threads | 运行指标(需要认证) |
列表中未列出的路径采用 fail-closed 策略,返回 401。允许匿名访问的只有 / · /index.html · /api/health · 静态资源,以及 /api/v1/login — 因为它是发放密钥的入口,所以不能要求密钥。
请求头
| 请求头 | 必需 | 值 |
|---|---|---|
X-API-Key | 是 | 已签发的密钥或配置中的默认密钥 |
X-Lang | 是 | PQL · CQL · SQL · TQL — 其他值返回 400 |
X-Params | 否 | 参数绑定。URL 编码的 JSON 对象 |
Content-Type | 是 | application/json |
查询语句本身就是请求体。不需要用 JSON 包裹。
读取 — POST /api/v1/query
curl -X POST -H "X-API-Key: ${PP_DATA_GATEWAY_API_KEY}" -H "X-Lang: PQL" \
-H "Content-Type: application/json" \
-d "SELECT tablename FROM pg_tables WHERE schemaname='pp'" \
https://host:5501/api/v1/query
{"result":"SUCCESS","message":"","truncated":false,
"data":[{"tablename":"mm_asset"},{"tablename":"mm_tag"}]}
truncated当结果达到上限(默认 100,000 行)时,网关将停止读取并断开连接。 如果 truncated 为 true 而您未检查该标志,您会在误以为结果是完整的情况下进行聚合。
流式 — POST /api/v1/query/stream
大数据量结果请使用此方式。服务器不会将全部数据加载到内存中,客户端也能立即收到第一行数据。
curl -X POST -H "X-API-Key: ${PP_DATA_GATEWAY_API_KEY}" -H "X-Lang: PQL" \
-H "Content-Type: application/json" \
-d "SELECT tablename FROM pg_tables WHERE schemaname='pp'" \
https://host:5501/api/v1/query/stream
{"row":{"tablename":"mm_asset"}}
{"row":{"tablename":"mm_tag"}}
{"end":{"count":43,"exec_ms":42,"truncated":false}}
响应为 application/x-ndjson,如果在流式传输过程中发生错误,最后一行将是 error 而不是 end。
{"error":{"message":"..."}}
end 行由于认证在请求头阶段即已完成,因此此响应始终以 200 开始。 后续的失败只会体现在响应体中 — 如果未收到 end,则表明结果并未结束,而是连接已断开;如果 end.truncated 为 true,则表明结果因达到上限而被截断。
参数绑定 — X-Params
在查询语句中使用 :이름 创建占位符,并在 X-Params 中放入 URL 编码的 JSON 对象。
PARAMS=$(python3 -c "import urllib.parse,json;print(urllib.parse.quote(json.dumps({
'site_id': 'SITE_00001',
'from:ts-long': 1772041960266,
'limit:int': 500
})))")
curl -X POST -H "X-API-Key: ${PP_DATA_GATEWAY_API_KEY}" -H "X-Lang: PQL" \
-H "X-Params: ${PARAMS}" -H "Content-Type: application/json" \
-d "SELECT * FROM pp.mm_tag WHERE site_id = :site_id AND created_at >= :from LIMIT :limit" \
https://host:5501/api/v1/query
在键后附加 :타입 以指定转换。如不附加,则按 JSON 原类型传入。
| 后缀 | 含义 |
|---|---|
:int · :long · :float · :double | 数字 |
:string | 字符串 |
:epoch · :ts-long | epoch 毫秒 → 时间戳 |
:ts-string | 日期字符串 → 时间戳 |
未知的类型后缀将返回 400 PARAM_MISSING — 不会被静默忽略。
X-Params 损坏,返回 400 而不是 500无法解码的值(%zz)或非 JSON 值属于客户端错误。以前这会导致 500 INTERNAL_ERROR ,使问题被误读为「服务器故障」。
写入 — POST /api/v1/update
curl -X POST -H "X-API-Key: ${PP_DATA_GATEWAY_API_KEY}" -H "X-Lang: PQL" \
-H "Content-Type: application/json" \
-d "UPDATE pp.mm_tag SET description = 'reactor inlet' WHERE tag_id = 'TAG_001'" \
https://host:5501/api/v1/update
此路径拒绝 TQL — 时序引擎不通过网关接受写入。
错误契约
所有错误都以相同的封装结构返回。
{"result":"ERROR","error_code":"QUERY_SYNTAX",
"message":"DDL statements are not allowed: DROP"}
error_code | HTTP | 含义 |
|---|---|---|
AUTH_MISSING | 401 | 缺少 X-API-Key 请求头 |
AUTH_INVALID | 401 | 密钥不匹配 |
PARAM_MISSING | 400 | 请求头 · 语言 · 参数类型转换失败 |
QUERY_SYNTAX | 400 | 拒绝 DDL · 多条语句 · 语法错误 |
QUERY_DATA | 400 | 数据约束违反 |
QUERY_CONSTRAINT | 409 | 完整性约束 |
QUERY_DEADLOCK | 409 | 死锁 |
QUERY_AUTH | 401 | 后端存储认证失败 |
QUERY_CONNECTION | 503 | 无法连接到后端 |
QUERY_TIMEOUT | 504 | 后端响应超时 |
QUERY_ERROR | 500 | 其他执行失败 |
QUERY_UNSUPPORTED | 400 | 该语言不支持的请求 |
NOT_FOUND | 404 | 路径不存在 |
METHOD_NOT_ALLOWED | 405 | 方法不存在 |
UNSUPPORTED_MEDIA_TYPE | 415 | Content-Type 不匹配 |
API_KEY_NOT_CONFIGURED | 503 | 未设置主密钥 — 并非凭据错误 |
LOGIN_NOT_CONFIGURED | 503 | 未设置控制台账户的两个密钥 |
语法 · 约束 · 数据 · 认证属于客户端原因(4xx),而连接 · 超时属于基础设施原因(5xx)。如果混为一谈,调用方就无法区分「我的查询有误」还是「存储已宕机」。服务器日志也按相同标准区分:4xx 记录为 WARN,5xx 记录为 ERROR。
API 密钥有两种
| 类型 | 位置 | 有效期 |
|---|---|---|
| 默认密钥 | 配置文件 data.gateway.api.key(渲染后) | 直到配置更改 |
| 已签发密钥 | 通过 POST /api/v1/keys 创建。存储仅保留 salt + SHA-256 摘要 | 仅存在于进程内 — 重启后即消失 |
比较操作始终为常数时间。
登录成功后,会直接返回配置中的默认密钥。原因在于引导 — 在全新安装中,密钥存储为空,而所有创建密钥的路径都已要求密钥。首位进入者必须携带比存储「更早」存在的凭据,那只能是配置文件中的密钥。
因此,此路径既不读取也不写入密钥存储。这是因为即使数据库宕机,此入口也必须保持开放。