61122ce437
Issue #11 asks for a tenant column so a multi-tenant caller can isolate rows in the database. Widened to caller-defined dimensions in general, but only the caller's own: model name and friends keep their existing columns, and the library writes nothing into the new container. Two independent findings force tenant_id to be a real column rather than a key inside JSON. An RLS policy on meta->>'tenant_id' parses fine, but the planner discards statistics for non-LEAKPROOF functions under RLS, and ->> is not marked leakproof; the pgsql-general report that hit this ended up moving the indexed column out of JSONB. Separately, the planner has no usable statistics for JSONB at all -- @> falls back to a hardcoded 0.1% selectivity. A configurable promoted-column whitelist is rejected: when two downstreams infer different types for the same key, the second ADD COLUMN is silently skipped by IF NOT EXISTS and the wrong type is written from then on, without an error. The library stops at the column plus a documented policy template. It must never enable RLS itself -- with no matching policy that is default-deny, which would silently fail every write for the two downstreams that are not multi-tenant.
4.5 KiB
4.5 KiB
type, node_id, title, date
| type | node_id | title | date |
|---|---|---|---|
| design | design:issue11-caller-dimensions | 调用方自定义维度设计(issue #11) | 2026-08-17 |
调用方自定义维度设计(issue #11)
正文: 2026-08-17-issue11-caller-dimensions-design.md。状态: 待 Codex 审 → 待人类审批。
- 选定方案:
tenant_id提真实列(RLS 硬需求)+metaJSON 容器承载任意调用方自定义 KV(默认不建索引)。chat()/embed()各增两个带默认值的 keyword-only 参数,签名冻结承诺不破。端口 22 → 24 字段。 - 范围收窄(2026-08-17 人类决策): 只做调用方自定义的维度;请求自带信息(模型名/供应商/源名)继续走现有列,库不往
meta写任何自采信息。issue 第 4 条(保留期与访问控制)另开。 - 为什么必须提列而不能纯 JSON: 两条独立实证。① RLS 挂
meta->>'tenant_id'语法合法但会静默退化——PG 的 Planner Statistics and Security 规则在 RLS 场景下对非 LEAKPROOF 函数当作没有统计信息规划,而->>未标 leakproof;pgsql-general 实证案例的最终解法就是"索引列改成非 JSONB",Tom Lane 警告手工标 leakproof 是安全问题。② 与 RLS 无关的独立问题: planner 对 JSONB 本就无可用统计,@>走硬编码 0.1% 选择率,Heap 复现里行数低估 12 万倍、join 从 300ms 变 584 秒。 - 为什么不做"可配置提升列白名单": dbt/Airbyte/Fivetran 三家一致禁止用户自定义列(Fivetran 的后续 MERGE 直接把用户列置 NULL,官方方案是建视图)。本库场景更糟: 两个下游对同名 key 推断出不同类型时,第二个到达者的
ADD COLUMN被IF NOT EXISTS静默跳过,从此一直静默写错类型——不报错、持续污染。且_COLUMNS/_INSERT从常量变运行时拼接,SQL 注入面从零出现,端口"22 字段冻结"与列序断言全部失效。 - 同类系统佐证: LiteLLM(同为 LLM 网关、同为每调用一行进 PG)的
SpendLogs正是此形态——team_id/organization_id/end_user/session_id全部提列并索引,而metadata/request_tags无任何索引。Grafana Loki 的三层(labels 索引 / structured metadata 不索引但可筛 / log line)是同一分野。六家 LLM 可观测平台无一例外都是"少数物化列 + 一个 KV blob"。没有任何成熟系统允许任意 key 自动获得列/索引待遇;唯一的自动推断派 ES dynamic mapping 也是唯一有公开事故名的(mapping explosion)。 - 库止步于列 + policy 模板,绝不自动 ENABLE RLS: 启用 RLS 而无匹配 policy 是 default-deny(零行可写,静默不报错)。三个下游里只有 GovDoc 多租户,库若自动启用,另两家升级后遥测全量写失败,叠加"遥测写失败静默降级"铁律 = 无声全局丢数据——这才是 issue「不可逆」担忧的真正落点。另三条理由: 库无权知道角色拓扑;按最佳实践部署时库的运行时角色恰好不是表属主、无权
CREATE POLICY;SQLite 无 RLS,承诺它会让两后端语义不对等。先例(graphile-worker/Ent+Atlas/django-multitenant)一致把 policy 授权留给使用方。 - 哨兵值而非 NULL: PG 的
USING表达式返回 false 或 null 的行都不可见且静默跳过,故 NULL 的tenant_id不是"未归属"而是对所有人永久不可见的黑洞。用NOT NULL DEFAULT ''则老行可一条 SQL 审计;同时满足 PG 11+ 加非易失默认值列不重写全表、SQLite 要求 NOT NULL 列必须有非 NULL 常量默认值。 - 超限报错而非静默丢弃: Langfuse 的"value 超 200 字符直接丢弃"不抄,违反 P5。报错点在
chat()入口而非遥测写入点——遥测层一切失败都被降级成 warning,校验放那里等于没有校验(同overlay保护键先例)。 - 不进缓存 key:
cache_namespace已是必填的租户隔离维度并已进 key(ARCH §7.5),重复;且进 key 会让存量缓存全量冷启动。 - 被否决备选: 纯
metaJSON 不提列(RLS 静默退化);可配置提升列白名单(多下游共表静默写错类型);复用cache_namespace传租户(缓存隔离单位 ≠ 数据归属,会让下游无法表达"同租户多命名空间");tenant_id混在meta里当约定 key(拼错不报错,静默降级成普通维度)。 - 另开议题:
_BACKFILL自动 ALTER 是否应降级为默认关闭(HangfireEnableHeavyMigrations先例、APScheduler 4.x 版本不认识即拒绝启动)——与本 issue 同源但属独立架构变更,按反 gold-plating 不纳入本次。