Skip to content

Hbo structinfo - #67549

Closed
englefly wants to merge 164 commits into
apache:masterfrom
englefly:hbo-structinfo
Closed

englefly wants to merge 164 commits into
apache:masterfrom
englefly:hbo-structinfo

Conversation

@englefly

@englefly englefly commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: close #xxx

Related PR: #xxx

Problem Summary:

Release note

None

Check List (For Author)

  • Test

    • Regression test
    • Unit Test
    • Manual test (add detailed scripts or steps below)
    • No need to test or manual test. Explain why:
      • This is a refactor/code format and no logic has been changed.
      • Previous test can cover this change.
      • No code files have been changed.
      • Other reason
  • Behavior changed:

    • No.
    • Yes.
  • Does this need documentation?

    • No.
    • Yes.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@englefly

englefly commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

run buildall

…l stats injection

### What problem does this PR solve?

Problem Summary: 现 HBO 的 fingerprint 由每个候选 group expression 每次
stats derive / cost 评估现算 plan-tree 子树字符串得到,key 语义是"子树形态"
而非"结构等价",且热路径重复计算、缺少人工修正入口。本改动:

1. 为 memo 中每个 group 生成"精简 struct info"(仅保留子树匹配所需:
scan 表 token/出现序/裁剪分区/可见版本、filter 谓词、join 条件与 joinType、
agg groupBy/函数;去掉 MV shuttle/lineage/per-query id 等无关信息),在
Group 上惰性生成一次并缓存(GroupStructInfo);
2. 用其 sha256 fingerprint 替换现有 HBO tree fingerprint(读侧
HboStatsCalculator/CostModel 从候选节点回链 group 取缓存 fingerprint;写侧
collectHboPlanInfo 规划期按 nodeId 快照 fingerprint,profile 发布期直接查
快照,不再现算子树指纹);Config.hbo_use_struct_info_fingerprint 默认 true,
置 false 走旧树指纹路径以对照/回退;
3. 新增 MySQL 风格人工注入命令:HBO SET/DELETE STATISTICS <fingerprint> =
<rows>;pinned 统计独立存储、优先级高于自动收集的 learned 条目、不被自动
publish 覆盖(pinned 命中直接 withRowCountAndHboFlag 覆盖行数估计)。

设计文档(含 6 轮 v4pro 评审记录与 TPCDS sf1 99+ explain 基准)见
data_test/tasks/hbo/struct-info-hbo-design-v4.md。基准结论:struct-info
fingerprint 模式 median 与旧树指纹持平,sum/p95 更快(惰性按 group 缓存,
不再每次评估现算子树字符串)。

### Release note

None(实验特性 HBO 内部改造;新增实验性管理命令与 FE Config 开关)

### Check List (For Author)

- Test: Manual test(FE 构建通过;本地集群 explain 冒烟;HBO SET/DELETE
STATISTICS 命令可执行;TPCDS sf1 全量 explain 基准 off/on 双轮对照)
- Behavior changed: Yes(实验特性,默认关闭;开关 hbo_use_struct_info_fingerprint 默认 true 仅影响启用 HBO 的查询的 key 生成)
- Does this need documentation: No
…n regression test

### What problem does this PR solve?

Problem Summary:

1. EXPLAIN 输出的 physical plan 无法直接获取各节点的 hbo fingerprint,导致
HBO SET STATISTICS 注入需要用户手工推算 key。本改动新增会话变量
show_hbo_fingerprint(默认 false,不影响既有 explain 输出),开启后 EXPLAIN
末尾追加 HBO fingerprint annotations:对 join / aggregation / filter 节点输
出节点 id、kind 与 fingerprint(含精简 structInfo 规范串);filter-on-scan
节点标注其 scan group 的 fingerprint(即读侧 lookup key,filter 输出行数受
该 key 约束)。注解来源:节点 group 回链/KEY_GROUP(memo 存活时)或规划期
collectHboPlanInfo 的 nodeId 快照(memo 释放后,physical-plan explain 路径)。

2. 回归测试 regression-test/suites/query_p0/hbo/hbo_structinfo_inject_test.groovy:
两表 join T join filter(R),正常估算 |T|(100k) >> |filter(R)|(~200);从
physical plan 注解提取 filter(R) 的 fingerprint,HBO SET STATISTICS 注入
500000(>|T|),验证注入后 join 翻转:filter(R) 变为 probe 侧、T 变为
broadcast 侧(断言 fragment0 内表归属翻转)。本地回归通过(Test 1 suites,
failed 0)。

3. 修复:collectHboPlanInfo 在内部语句(如 ANALYZE 的统计插入)规划时
cascadesContext.memo 可能为空导致 NPE,补 memo 判空(信息收集仅对常规查询
产生快照)。

### Release note

None(实验特性;新增会话变量 show_hbo_fingerprint 默认关闭)

### Check List (For Author)

- Test: Manual test(本地集群 EXPLAIN 注解与注入翻转验证)+ Regression
test(hbo_structinfo_inject_test 通过)
- Behavior changed: Yes(新增默认关闭的会话变量与注解输出;修复内部语句在
全局 enable_hbo_info_collection 下潜在 NPE)
- Does this need documentation: No
…plan nodes

Problem Summary: 会话变量 show_hbo_fingerprint 之前只在 EXPLAIN 末尾追加
"HBO fingerprint annotations" 块,用户需要把节点与块内条目人工对应。本改动
把 fingerprint 与精简 structInfo 直接内联打印到 physical plan 节点文本中:

- 规划期(chooseBestPlan -> postProcess 之后, memo 存活时)把
join / aggregation / filter 节点的 hbo fingerprint + structInfo 规范串写入
节点 MutableState(KEY_HBO_FP / KEY_HBO_STRUCT;filter-on-scan 写入其 scan
group 的 fingerprint,即读侧 lookup key);
- 上述节点 toString() 在 show_hbo_fingerprint=true 时追加
"hboFingerprint=<hash> hboStruct=<canonical>"(AbstractPlan.withHboExplainInfo
统一封装,默认输出不变);
- 因此 physical plan 文本中可直接复制 fingerprint 用于 HBO SET STATISTICS,
且打印不依赖 memo 是否存活、也不依赖 enable_hbo_info_collection(attach 在
常规规划路径执行,信息随节点对象携带)。explain 末尾的注解块保留(覆盖默认
fragment 形态输出)。
- 回归 hbo_structinfo_inject_test 仍通过(Test 1 suites, failed 0)。

None(实验特性;新增默认关闭的会话变量输出)

- Test: Manual test(physical plan 输出内联 hboFingerprint/hboStruct 验证,
默认关闭无输出、enable_hbo_info_collection=false 仍输出)+ Regression test
- Behavior changed: Yes(show_hbo_fingerprint=true 时 explain 物理计划节点内联
追加 hbo 信息;默认 false 无变化)
- Does this need documentation: No
…UCT param + tests

Problem Summary:

1. join/aggregation 节点缺 fingerprint: memo 会把逻辑等价表达式(如交换律
inner join)归并进同一 group,原先"多 logical expression → invalid"导致整组
无 fingerprint;改为取 first logical expression 计算(等价表达式规范结构相
同),join/agg/filter 在 show_hbo_fingerprint=true 时均内联输出
hboFingerprint/hboStruct。
2. 内联输出改为"规划期挂状态即打印"(不再要求渲染时会话变量),因此 explain
/ explain physical plan / profile 的 physical plan 附录(同一 toString 链路)
都能显示;默认(未开 show_hbo_fingerprint)仍完全不变。
3. 使用 hbo 的节点标注:节点估算来自 hbo(learned/pinned,withRowCountAndHboFlag)
时 stats 显示 (hbo),show 开关下节点另内联 hboUsed=true。
4. HBO SET STATISTICS 支持可选 STRUCT '<规范串>' 参数,把可读的精简 structInfo
一并存入 pinned。
5. 回归扩展:验证 join/filter/agg 节点 fingerprint 内联显示;注入后顶层 join
翻转(probe 由 T 变 filter(R));filter 节点 hboUsed=true;Test 1 suites, failed 0。

None(实验特性;默认关闭的 show_hbo_fingerprint 输出增强)

- Test: Manual test(FE 构建、默认关闭无输出、STRUCT 参数可执行)+ Regression
test(hbo_structinfo_inject_test 扩展通过)
- Behavior changed: Yes(show_hbo_fingerprint=true 时 join/agg/filter 内联
fingerprint/structInfo 与 hboUsed;命令新增可选 STRUCT;多 logical group 不再
整组 invalid)
- Does this need documentation: No
### What problem does this PR solve?

Problem Summary: 依据 v4pro 代码评审(code-review.round1.md,18 条)修订:

- M1/m1/m7: collectHboPlanInfo 的 fingerprint 快照 key 统一为真实 PlanNodeId
  (planId.asInt()),与 profile publish 的 runtime node_id 对齐;struct 模式下
  快照缺失时打 WARN(不再静默跳过)。
- M2: pinned 注入表由无界 ConcurrentHashMap 改为 Caffeine LRU,新增
  Config.hbo_pinned_stats_cache_num(默认 5000)兜底。
- m2: filter-on-scan 判定收紧为"filter→(project*)→OlapScan"链,避免把
  join 之上的 filter 误标为内层 scan 的 fingerprint。
- m4/m5: GroupStructInfo.of 整体捕获 RuntimeException 回退 INVALID(debug
  日志);版本获取异常收敛到 RpcException/RuntimeException。
- m6: 注解块在 enable_hbo_optimization=false 时输出提示(注入不生效)。
- m8: HBO SET 校验 fingerprint(64 hex)与 rows>=0。
- m10: 回归用例尾部还原 enable_hbo_info_collection 全局变量。
- m11: 聚合签名区分 DISTINCT 语义。
- n2: explain 的 hint 分支不再跳过注解追加;n3: withHboExplainInfo 下沉到
  AbstractPhysicalPlan;n4: attach 入口统一判 show 开关。
- 记录未做项(m3 learned hboUsed 单测、m9 单 FE 范围提示、m12 出现序依赖、
  n1 工具类抽取)至设计文档。
- 回归 hbo_structinfo_inject_test 通过(Test 1 suites, failed 0)。

### Release note

None(实验特性内部修订)

### Check List (For Author)

- Test: FE 构建通过;hbo_structinfo_inject_test 回归通过
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?

Problem Summary: v4pro round2 复审(11 条:1 Major + 6 Minor + 4 Nit)修订:

- N-1(修复全部 fe-core/fe-common checkstyle 违规):import 顺序(含
  commands 内部类父-子顺序、log4j 组序)、DeclarationOrder(GroupStructInfo
  字段序、AbstractPhysicalPlan helper 下沉到类尾)、行超 120、多余空行、
  HboStatisticsCommand 补 license header、planner 去 unused import。
- N-2:EXPLAIN 注解块改为直接读节点 MutableState(fingerprint/struct 规划期
  已挂),移除按 nereids id 查快照的 fallback,消除 id 口径不一致。
- N-3:Config.hbo_pinned_stats_cache_num 改为非 mutable(启动生效),避免
  "mutable 但只在构造期生效"的名不副实。
- N-4:genPlanStatisticsMap 缺快照的 WARN 改为循环后一次汇总,避免逐节点噪声。
- N-5:回归 suite 全局变量还原放入 try/finally。
- N-6:appendScan 版本获取失败的 catch 增加 debug 日志。
- N-8:HBO fingerprint 校验改为预编译 Pattern 常量。
- 记录在案(m3 learned hboUsed 补测、m9 单 FE 范围提示、m12 出现序依赖、
  n1 工具类抽取、N-9 内联提示)。
- checkstyle:fe-core 与 fe-common 均 0 违规;回归 hbo_structinfo_inject_test
  通过(Test 1 suites, failed 0)。

### Release note

None(实验特性内部修订)

### Check List (For Author)

- Test: FE 构建通过;fe-core/fe-common checkstyle 0 违规;回归通过
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?

Problem Summary: v4pro round3 复审(0 Blocker/0 Major/3 Minor/3 Nit)修订:
- N-6:appendScan 版本获取异常 catch 收窄为 RpcException(其余交外层
  GroupStructInfo.of catch -> INVALID + debug)。
- N-7:普通(非 scan 之上)filter 不再 attach/输出 fingerprint(读侧不可注入,
  避免误导);注解块与内联口径一致。
- R3-1:show_hbo_fingerprint=true 但无节点挂上 fingerprint 时,注解块输出
  一行 note(检查 hbo_use_struct_info_fingerprint 与规划期 attach)。
- Nit(suite):pinned 清理移入 finally,断言失败路径也还原全局变量。
- 记录在案:N-9(hbo off 提示未覆盖内联/profile)、N-11(occurrence 序依赖)。
- checkstyle fe-core 0 违规;回归 hbo_structinfo_inject_test 通过
  (Test 1 suites, failed 0)。

### Release note

None(实验特性内部修订)

### Check List (For Author)

- Test: FE 构建通过;checkstyle 0 违规;回归通过
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?

Problem Summary: 手动注入(HBO SET/DELETE STATISTICS)的 pinned 统计此前只存于
单 FE 内存,FE 重启即丢失。本次将 pinned 条目落盘到 internal db 表
`__internal_schema.hbo_statistics`,由 `Config.hbo_persist_pinned_to_internal_db`
(default false,hot-mutable)控制:

- 模型:内存(memory)仍权威,读侧只走 HboPlanStatisticsManager 的 Caffeine LRU;
  SET/DELETE 同步 write-through(尽力而为,失败仅记 WARN 不阻断命令);
  表在首次使用时由 `CREATE TABLE IF NOT EXISTS` 自动创建,FE 重启后经
  `ensurePinnedLoaded()` 惰性全量加载(hboPinnedLoaded 标志 + 失败重试)。
- 存储列:UNIQUE KEY(fingerprint varchar(64));row_count bigint(rows 为保留字)、
  node_type varchar(1024) NULL、struct_info varchar(65533) NULL(超长截断)、
  create_time_ms bigint;HASH 1 bucket、replication_num=1,与 column_statistics
  等共用 __internal_schema(FeConstants.enableInternalSchemaDb 门控)。
- SQL 经 StatisticsUtil.execUpdate/execStatisticQuery 以 internal context 执行,
  字符串字面量统一单引号转义。
- 验证:手工全链路 SET -> 内库行存在 -> FE 重启后 explain 翻转仍生效(日志
  "loaded N hbo pinned statistics entries from internal table")-> DELETE 清行恢复;
  新回归 hbo_persist_internal_db_test 热开配置断言 SET 写行 / DELETE 清行并在
  finally 还原;既有 hbo_structinfo_inject_test 仍通过。

### Release note

None(实验特性内部行为,默认关闭)

### Check List (For Author)

- Test: FE 构建通过(checkstyle 0 违规);回归 hbo_structinfo_inject_test 与
  hbo_persist_internal_db_test 均 Test 1 suites, failed 0;手工验证重启恢复
- Behavior changed: No(默认 false 保持原内存行为)
- Does this need documentation: No
### What problem does this PR solve?

Problem Summary: v4pro 审阅 commit 3962b55(pinned 落盘 internal db)后
0 Blocker / 0 Major / 7 Minor / 6 Nit,结论可合入;本轮修订全部 Minor 与
可落地 Nit:

- P-1:加载与 SET/DELETE 并发互斥 —— 内存阶段(load 批量写入、SET put、
  DELETE invalidate)统一走 pinnedLoadLock;加载改用
  `asMap().putIfAbsent` 合并,杜绝加载覆盖并发 SET 新值/复活已删条目。
- P-2:加载失败不再每个 group 重复 DDL+SELECT —— loadAll 失败返回 null
  (原返回空表语义无法区分),manager 侧 30s backoff(lastLoadFailedMs)后
  才重试。
- P-3:多 FE 语义文档化 —— 每个 FE 进程只加载一次快照、不刷新其它 FE
  的 SET(类 javadoc + Config javadoc)。
- P-4:转义复用 StatisticsUtil.escapeSQL(同时双写 ' 与 \\,internal ctx
  默认 sql_mode 无 NO_BACKSLASH_ESCAPES 时反斜杠不再被解析吞掉)。
- P-5:struct_info 截断改按 UTF-8 字节并在字符边界截断(varchar 上限按
  字节计,原 UTF-16 char 截断对多字节文本可能超限被拒)。
- P-6:replication_num 从硬编码 1 改为 Math.max(1,
  Config.min_replication_num_per_tablet),与 InternalSchemaInitializer
  约定一致,min_replication>1 集群不再静默建表失败。
- P-7:配置热切换语义文档化(默认 false 期 SET 不落盘、DELETE 不清行;
  重新开启后加载以 putIfAbsent 合并、内存新值优先)+ putIfAbsent 落实。
- N-1/N-2/N-3:删除本地 escape(null)死代码、改用
  InternalCatalog.INTERNAL_CATALOG_NAME、catch Throwable 收窄为
  Exception。
- N-4:回归 finally 先还原 config 再 DELETE,清理失败不再泄漏热配置。
- 记录在案:N-5(.out 尾随空白为生成器固有,惯例接受)、N-6(空串/NULL
  以内存 '' 为规范,读侧 NULL->'' 已统一;DB 列保持 NULL 兼容)。

验证:build.sh --fe EXIT=0(checkstyle 0 违规);70k ASCII 与 30k 多字节
STRUCT 截断实测(65533 字节、21844 个完整 3 字节字符、无截断字符);含
引号/反斜杠 STRUCT 落盘往返无损;重启后惰性加载恢复 explain 翻转
(loaded 1 entries);hbo_structinfo_inject_test 与
hbo_persist_internal_db_test 均 Test 1 suites, failed 0。

### Release note

None(实验特性内部修订,默认关闭)

### Check List (For Author)

- Test: FE 构建通过;回归两套通过;手工验证转义/截断/重启恢复
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?

Problem Summary: v4pro 复审 round2(commit c94cb29)给出 Minor 1 / Nit 2,
总体可合入;本轮修订:

- M-1(DELETE 复活窗口,P-1 残余):removePinnedPlanStatistics 的 DB 删除
  原在 pinnedLoadLock 之外执行,与锁内懒加载 SELECT 存在重叠:并发形态
  (R1)加载可能在 invalidate 与 DB 删除之间读到旧行、putIfAbsent 把已删
  条目补回内存;确定性形态(R2)内部库未就绪窗口内 DB 删除失败被吞、库行
  保留,30s 重试加载成功后被删除条目放回内存,且 hboPinnedLoaded=true 后
  不再自愈。修复:
  - DB 删除移入同一 synchronized 块(与 loadAll 锁内 DB 往返模式一致),
    消除 R1;
  - HboStatisticsStore.delete 返回成败;失败且懒加载仍未完成时记录
    pendingLoadTombstones,加载跳过 tombstone 键、成功后清空;SET 清除同
    键 tombstone(重建意图优先),消除 R2。
- N-1:Config javadoc 修正 —— "OFF 期 DELETE 的库行在 re-enable 后重载"
  仅对从未加载过的 FE 成立(FE 每进程至多加载一次),补限定语并说明
  pending 删除被加载尊重。
- N-2:回归 finally 改嵌套 try/finally —— 内层在配置仍为 ON 时 DELETE
  清理(SET 后、用例内 DELETE 前失败不留垃圾行),外层兜底还原配置。

验证:build.sh --fe EXIT=0(checkstyle 0 违规);重启后懒加载恢复翻转
(loaded 1 entries)、DELETE 清行;hbo_structinfo_inject_test 与
hbo_persist_internal_db_test 均 Test 1 suites, failed 0。

### Release note

None(实验特性内部修订,默认关闭)

### Check List (For Author)

- Test: FE 构建通过;回归两套通过;手工验证重启恢复与清理
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?

Problem Summary: v4pro 复审 round3(commit f908d4d)给出 0 Blocker / 0
Major / 0 Minor / 3 Nit;本轮修订:

- M-1-R(墓碑键跨重启复活):未就绪窗口内 DB 删除失败的 DELETE 由墓碑在
  进程内封死复活,但首次成功 load(证明 DB 可达)只是清空墓碑、未对墓碑
  键补发 DB 删除,库行残留导致 FE 重启后仍复活。修复:load 成功后对墓碑
  键逐个 best-effort 重发幂等 HboStatisticsStore.delete(失败仍仅 warn,
  行保留并已在 javadoc 注明),再清空墓碑。
- N-3:Config javadoc 对"pending load 期间 honored 的删除"补
  "issued while this config is true" 限定,与 OFF 期删除行为区分清楚;
  manager 类 javadoc 同步墓碑语义(待加载删除被墓碑化、load 成功后重删)。
- N-4:墓碑字段/类 javadoc 点明跨 FE 弱一致模型角落 —— 本 FE 墓碑可能压
  制他 FE 在加载窗口内重建的同键行,重启后随一次性加载恢复。

验证:build.sh --fe EXIT=0(checkstyle 0 违规);SET/重启加载/翻转/DELETE
清行手工链路通过;hbo_structinfo_inject_test 与
hbo_persist_internal_db_test 均 Test 1 suites, failed 0。

### Release note

None(实验特性内部修订,默认关闭)

### Check List (For Author)

- Test: FE 构建通过;回归两套通过;手工验证持久化链路
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?

Problem Summary: v4pro 复审 round4(commit 1005da0)给出 Minor 1 / Nit 1;
本轮修订:

- M-4:load 成功后的 re-delete 此前对全部墓碑键补发 DB 删除,会把"他 FE
  在加载窗口内重建的同键行"从共享表删除,与本 commit 自述的跨 FE javadoc
  语义矛盾。修复:merge 阶段收集"本次 load 快照中实际出现且被墓碑压制"的
  指纹(suppressedByTombstone),re-delete 只针对这些行 —— 完整保留
  M-1-R(需删的旧行必在快照中),且不再删除快照之后他 FE 新建的行;快照
  之前他 FE 已建的行仍可能被删,类 javadoc/墓碑字段注释/Config javadoc
  已如实改写(该行被删则重启后不再重现,仅当重删也失败时重启后重现),
  删除指向相反语义的引用。
- N-5:re-delete 循环统计失败数,>0 时汇总 warn
  ("failed to remove N of M tombstoned ... rows; will reappear after the
  next FE restart"),提升运维可观测性。

验证:build.sh --fe EXIT=0(checkstyle 0 违规);hbo_structinfo_inject_test
与 hbo_persist_internal_db_test 均 Test 1 suites, failed 0。

### Release note

None(实验特性内部修订,默认关闭)

### Check List (For Author)

- Test: FE 构建通过;回归两套通过
- Behavior changed: No
- Does this need documentation: No
… rows

### What problem does this PR solve?

Problem Summary: v4pro 复审 round5(commit e781b09)给出 Nit 2;本轮修订:

- N-6:多副本读滞后下,"需删旧行必在加载快照中"不严格成立 —— load 快照
  未见的墓碑行不会被重删,墓碑随后被静默清空,行在每次 FE 重启后复活且
  无日志。修复:manager 类 javadoc 与墓碑字段注释补限定("快照未见的行不
  补删,可能重启后重现");load 成功后对"墓碑数 - 快照命中数"未命中>0 打
  info 计数日志(多数未命中为良性,故不用 warn)。
- N-7:Config javadoc 恢复通用残余句 —— "rows whose best-effort removal
  from the table fails (during or after the one-time load, e.g. internal
  table temporarily unreachable) reappear after a FE restart; re-issue the
  DELETE once the table is reachable",覆盖 load 完成后 DB 删除失败与快照
  未见行两类残余。

验证:build.sh --fe EXIT=0(checkstyle 0 违规);hbo_structinfo_inject_test
与 hbo_persist_internal_db_test 均 Test 1 suites, failed 0。

### Release note

None(实验特性内部修订,默认关闭)

### Check List (For Author)

- Test: FE 构建通过;回归两套通过
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?

Problem Summary: v4pro 复审 round6(commit 291ce7d)给出 Nit 1;本轮修订:

- N-8:未命中墓碑的 info 日志原为完成时断言 "were left in the table",
  与同段注释 "mostly benign (the row is already gone)" 自相矛盾 —— 多数
  良性未命中(歧义提交实际已删、他 FE 已删等)下行并不在表中,运维按
  字面会误判存在残留;仅多副本读滞后场景下原措辞才成立。改为或然表述
  "were not seen in the loaded snapshot and may still be stored in the
  table; they may reappear after the next FE restart"。纯诊断可读性修订,
  零行为变更。

验证:build.sh --fe EXIT=0(checkstyle 0 违规);hbo_persist_internal_db_test
Test 1 suites, failed 0。

### Release note

None(实验特性内部修订,默认关闭)

### Check List (For Author)

- Test: FE 构建通过;回归通过
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?

Problem Summary: 按 doris-skills(doris-repo-review 方法论:风险扫描 → 分模块
并发 v4pro full-review → 共享 ledger 收敛)对 master..hbo-structinfo 全量
diff(25 文件)复审。合并后 A1 Major / A2 Minor / C1 Minor 及 Nit C2-C4、
N-B1 全部落实:

- A1(Major,pinned filter-on-scan 谓词盲):注入 key 为 scan-group
  fingerprint(表 token 不含 filter 常量),pinned 绕过 learned 侧
  ScanPlanStatistics 谓词匹配 → 同表同版本同裁剪下不同常量共享同一 pin。
  该语义为 scan-group key 模型的固有权衡(SET 无 plan 上下文,谓词敏感
  key 不可表达),修复=精确文档化(HboStatsCalculator 注释 + 设计文档
  §4.1)+ 回归锁定:同扫描不同常量(R.b=1 vs b=2)共享 pin 且同样翻转,
  防止未来误改 key 模型。
- A2/N-B2(Minor):HboUtils.genPlanStatisticsMap struct 模式下快照整体为
  空(全部节点无 fingerprint)时单独 warn(原逻辑仅部分缺失时告警,最坏
  诊断场景反而静默)。
- C1(Minor):putPinnedPlanStatistics 的 DB persist 移入 pinnedLoadLock,
  与 DELETE 的 DB 删除同锁 —— 同键并发 SET/DELETE 的 DB 写序与内存序
  一致,库行恒等于最后内存写入者(修复 round2 "last memory writer ==
  last DB writer" 不成立的残余)。
- C2(Nit):pinned cache 上限语义对齐兄弟 hbo cache(num<=0 为无界而非
  静默禁用注入),Config javadoc 注明。
- C3(Nit):HboStatisticsCommand fingerprint 统一小写(ROOT),SET 与
  DELETE 均做 64-hex 校验 —— 大写/畸形 DELETE 不再静默无效。
- C4(Nit):builder 行数 Long.parseLong 溢出 → 清晰 ParseException。
- N-B1(Nit):EXPLAIN 注解块仅在物理层级输出,排除 PARSED/ANALYZED/
  REWRITTEN 的误导性 "(no hbo fingerprint attached ...)" 回退。
- B-open(filter 发布 key 一致性)经代码证据排除:扫描父 filter 折叠进干净
  scan 时不注册 nereids→planId 映射(仅建 SelectNode 路径注册),collect
  不会覆盖 scan 条目;SelectNode filter 条目以其自身组 fp 发布,读侧无
  任何 lookup 使用该 key,惰性无害。

验证:build.sh --fe EXIT=0(checkstyle 0 违规);hbo_structinfo_inject_test
(含新增 b=2 共享 pin 断言)与 hbo_persist_internal_db_test 均 Test 1
suites, failed 0;手工负例:大写 SET→小写 DELETE 命中、畸形 DELETE 报错、
rows 溢出报 "out of range"、PARSED/REWRITTEN explain 无注解块而默认
explain 有块。

### Release note

None(实验特性内部修订,默认关闭)

### Check List (For Author)

- Test: FE 构建通过;回归两套通过;手工负例验证
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?

Problem Summary: skill 评审 round2(commit 66c9704)确认 A1/A2/C1/C2/C3/
C4/N-B1 全部修复正确且完整、B-open 排除成立,仅 Nit 1 条:

- N-R2-1:C1 新增注释 "the stored row always matches the last memory
  writer" 措辞过强 —— 持久化为 best-effort(persist/delete 吞错仅 warn),
  写失败时库行并不匹配最后内存写入者、可能在 FE 重启后重现(既有已文档
  化模型)。补限定语 "when the best-effort writes succeed"(失败仅告警、
  重启后可能重现)。纯注释修订,零行为变更。

验证:注释修订不改变字节码,前一构建(EXIT=0,checkstyle 0 违规)与两套
回归结果仍有效。

### Release note

None(实验特性内部修订,默认关闭)

### Check List (For Author)

- Test: 注释修订无需重构建;回归沿用上一轮结果
- Behavior changed: No
- Does this need documentation: No
Problem Summary: 精简 structInfo fingerprint 已成为 HBO 的唯一 key(读侧、
写侧快照、explain 展示全量切换),旧 plan-tree fingerprint 路径仅剩
`Config.hbo_use_struct_info_fingerprint=false` 的对照/回退用途;旧 hbo 未发布,
无兼容负担,故整链删除(问答 Q5):

- `Config.hbo_use_struct_info_fingerprint`;
- `HboUtils`:`getHboPlanNodeAndHash` 的 legacy 分支、`getPlanNodeHash`、
  `getPlanFingerprintHash`、`collectScanQualifierList`(含其 helper);
- `genPlanStatisticsMap` 的 legacyHash 分支与 `buildHboInputTableStatisticsInfo`
  的 needHash 参数(发布期不再计算子树 hash);
- 旧树指纹生成链:`AbstractPlan.getPlanTreeFingerprint/getFingerprint/
  isLocalAggPhysicalNode`、`GroupPlan.getFingerprint`,以及 8 个 plan 类
  (LogicalAggregate/LogicalFilter/LogicalJoin/LogicalOlapScan/PhysicalFilter/
  PhysicalHashAggregate/AbstractPhysicalJoin/PhysicalOlapScan)的 `getFingerprint`
  覆写与已无引用的 `getQualifierWithRelationId`;
- `InputTableStatisticsInfo.hash`(恒空、无消费方);
- `NereidsPlanner` 中 5 处配置门控与注解 note 文案。

净删除 ~355 行,14 文件;struct fingerprint 为唯一 key 后行为不变。

验证:./build.sh --fe EXIT=0(checkstyle 0 违规,含 UnusedImports 校验);
本地集群重启后 hbo_structinfo_inject_test 与 hbo_persist_internal_db_test 均
Test 1 suites, failed 0。

None(实验特性内部清理,旧 hbo 未发布)

- Test: FE 构建通过;两套 hbo 回归通过
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?

Problem Summary: 此前只有 SET/DELETE,无法查看 FE 上已记录的 hbo 统计(排查依赖
explain 注解或(开持久化后)直查内库)。新增问答 Q1 的查看命令:

```
HBO SHOW STATISTICS [LIKE '<pattern>'];          -- pinned + learned(默认)
HBO SHOW PINNED STATISTICS [LIKE '<pattern>'];   -- 仅人工注入
HBO SHOW LEARNED STATISTICS [LIKE '<pattern>'];  -- 仅自动收集
```

- 语法:`DorisParser.g4` 的 `hboStatement` 增加 `hboShowStatistics` 分支
  (`SHOW` 为词法关键字;scope 复用 identifier,LIKE 用 LIKE 词法),builder 生成
  `HboShowStatisticsCommand`;
- 实现:`HboShowStatisticsCommand extends ShowCommand`(ADMIN 权限、
  FORWARD_NO_SYNC),输出列 `Kind`/`Fingerprint`/`NodeType`/`Rows`/`StructInfo`/
  `Detail`,按 (Kind, Fingerprint) 排序;`LIKE` 支持 SQL 语义(`%`/`_`,其余字面量),
  scope 非法值报 `invalid hbo show scope, expect PINNED or LEARNED`;learned 经
  新增的 `HboPlanStatisticsProvider.getAllHboPlanStats()` 只读枚举
  (`MemoryHboPlanStatisticsProvider` 的 recent-runs cache),`Rows` 为最近一次
  运行的输出行数摘要、`Detail` 给出 `runs=N`;pinned 的 `Rows`/`StructInfo`/`Detail`
  分别来自注入行数/STRUCT 规范串/创建时间;
- 回归:`hbo_show_statistics_test`(断言式而非 qt:Detail 含创建时间不可复现,
  查询均按精确 fingerprint 过滤保证确定性),覆盖 pinned/全部/learned 三个 scope、
  LIKE 前缀与不匹配、DELETE 后消失、非法 scope 报错。

验证:./build.sh --fe EXIT=0(checkstyle 0 违规);手工验证三 scope 与错误路径;
本地集群 hbo_show_statistics_test / hbo_structinfo_inject_test /
hbo_persist_internal_db_test 均 Test 1 suites, failed 0。

### Release note

None(实验特性新增只读查看命令,默认关闭特性下无副作用)

### Check List (For Author)

- Test: FE 构建通过;三套 hbo 回归通过;手工验证
- Behavior changed: No
- Does this need documentation: No
…ned injection

### What problem does this PR solve?

Problem Summary: 按用户需求把 HBO 注入能力细化为"带常量/不带常量"两种粒度,
并补齐守卫生效、learned 注入与展示:

1. 两种指纹粒度(`GroupStructInfo.LiteralMode`):
   - filter 根:两个 fp —— 带常量(精确,`lit(10:INT)`)与不带常量(形态,
     `lit(*)`);`Group` 双缓存,每 group 每模式至多算一次;
   - join/agg 根:只用不带常量形式(其读侧 key);
   - 不带常量不保留字面量类型(Nereids 会为类型不匹配插入 Cast,语义不同
     的写法本就有 Cast 差异;语义等价的写法允许合并);
   - EXPLAIN 打印:filter 节点 `fingerprint`(带常量)+ `fingerprintNoLiteral`
     (不带常量);join/agg 只打 `fingerprint`(不带常量)。
2. 读侧查找顺序(filter):带常量 → 不带常量 → learned(filter 根两模式 →
   scan 级 key);join/agg:不带常量 → learned。filter 注入范围从
   "scan 之上"扩展到**所有 filter 根**。
3. `TYPE EXACT|FILTER_SMALL` 守卫:`FILTER_SMALL` 仅在优化器自身推导处于
   病态极小(`E<=1` 或 `E<=I*hbo_filter_small_ratio`,默认 1e-3)时生效,
   否则跳过并在 EXPLAIN 输出 `skipped=filterSmallGuard(E=…,I=…)`(按 query
   记录,查两个粒度)。
4. learned 注入:`HBO SET/DELETE LEARNED STATISTICS '<fp>' = <rows>`(便于
   测试 learned 路径,无需真实 profile 发布);注入条目不带输入表统计,按
   fingerprint 直接匹配(真实 learned 条目必带 ≥1 个 scan,空列表无歧义)。
5. SHOW:`Kind/Fingerprint/Granularity/Type/Rows/StructInfo/Detail`;pinned 的
   粒度在首次命中时惰性绑定并在 SHOW 展示;learned 显示 `-`。落盘表把占位列
   `node_type` 改为 `stats_type`(+ `fingerprint_kind`)。
6. 回归(全部通过):新增 `hbo_filter_fingerprint_granularity_test`(带常量
   注入只影响同常量、不带常量跨常量命中、join fp 跨常量相同)、
   `hbo_learned_injection_test`(learned 注入生效/展示/删除、TYPE/STRUCT
   对 learned 报错)、`hbo_filter_small_guard_test`(病态区间生效、健康区间
   跳过并输出 guard 标记、EXACT 对照);更新 `hbo_structinfo_inject_test`
   (精确注入不泄漏到其它常量 + 形态注入共享)、`hbo_show_statistics_test`
   (新列)、`hbo_persist_internal_db_test`(stats_type/fingerprint_kind)。

验证:./build.sh --fe EXIT=0(checkstyle 0 违规);6 个 hbo 回归套件均
Test 1 suites, failed 0;手工验证 EXPLAIN 双 fp、粒度隔离、守卫跳过标记、
learned 注入与 SHOW 粒度绑定。

### Release note

None(实验特性内部能力增强,默认关闭)

### Check List (For Author)

- Test: FE 构建通过;6 套回归通过;手工验证
- Behavior changed: 注入条目可按粒度精确/形态生效(实验特性)
- Does this need documentation: No
### What problem does this PR solve?

Problem Summary: 手工注入"某个等值连接的膨胀倍数",让优化器把这个已知会
爆中间结果的 join 尽量排到最后(先做其它 join)。这是**join 顺序控制**
手段,不追求估算更准:

- key:只含等值连接条件(列对),canonical `JE{EqualTo(...);...}`(对内部
  可交换、列对排序,复用 GroupStructInfo.normalizeExpression)→ sha256
  condFingerprint;**不含 join 类型、不含子树结构**,因此 inner/outer 共享
  同一条注入、跨查询复用;cross join 无等值条件 → 无 key;
- 命令:
  `HBO SET EXPANSION '<condFp>' = <exp>=1 [COND 'JE{...}']`、
  `HBO DELETE EXPANSION '<condFp>'`、`HBO SHOW EXPANSION STATISTICS [LIKE …]`
  (EXPANSION/HBO 等词均按 identifier 匹配并在 builder 校验,避免新增保留字
  破坏用户标识符;SET/DELETE EXPANSION 由 statistics 形式按词分发);
- 读侧(computeJoin):精确 group fp 的 pinned 行数覆盖未命中时,按条件查
  expansion 条目;适用类型 INNER / LEFT / RIGHT / FULL OUTER,semi / anti /
  asof 与 mark 类不适用(输出不可能超过左侧行数)并在 EXPLAIN 输出
  `expansion=skipped=<type>-join-never-expands`;
  估计 `est = clamp(exp × max(Lest,Rest), 1, Lest×Rest)`(上界笛卡尔积),
  外层 join 再取语义下限(LEFT ≥ Lest、RIGHT ≥ Rest、FULL ≥ max);
  应用后标注 `hboUsed=true`;
- EXPLAIN:join 节点始终打印 `condFingerprint` / `cond`(供复制注入),应用时
  打印 `expansion=exp=1000x (left=…,right=…,est=…)`;
- 配置 `hbo_expansion_cache_num`(默认 5000,<=0 无界);本版纯内存(learned
  关闭,不做落盘)。

验证:./build.sh --fe EXIT=0(checkstyle 0 违规);新增回归
`hbo_join_expansion_inject_test`(注入后目标 join 从"最先做"变为"最外层最后
做",join order 由 [(b=b),(a=a)] 变为 [(a=a),(b=b)],DELETE 后恢复;SHOW/
DELETE/非法值/非法指纹;SEMI join 不适用并给出 skipped 原因)通过;hbo 目录
其余自有套件全部通过(8 个失败为既有 master 套件缺 `hbo_slow_query_threshold_ms`
配置,与本改动无关)。

### Release note

None(实验特性新增手工注入能力,默认关闭)

### Check List (For Author)

- Test: FE 构建通过;新增回归 + 既有 hbo 套件通过;手工验证排序翻转
- Behavior changed: 新增 HBO SET/DELETE/SHOW EXPANSION 语句(实验特性)
- Does this need documentation: No
### What problem does this PR solve?

Problem Summary: 命中 pinned 条目时,EXPLAIN 只显示"是否生效(hboUsed)"
与"是否被守卫跳过(skipped=filterSmallGuard(...))",看不到条目类型,排查
"这条注入是什么类型、为什么没生效"需要另查 HBO SHOW。

- 命中 pinned 条目时按 fingerprint 记录其类型(EXACT / FILTER_SMALL)到
  per-query 表(`HboPlanInfoProvider.putPinnedEntryType`),attach 时按节点的
  带常量 / 不带常量 fingerprint 取回并挂到节点状态(`MutableState.KEY_HBO_TYPE`);
- 输出:内联(physical plan 节点文本)`hboType=exact|filter_small`;
  注解块 `type=exact|filter_small`(与 `skipped=filterSmallGuard(E=…,I=…)`
  相邻,一轮就能看出"类型 + 是否应用 + 未应用原因");
- 明确 skipped 记录语义(文档同步):仅当**匹配到 FILTER_SMALL 条目但本次推导
  不满足极小判定**时记录;`E` = 本次推导的 filter 输出行数(未被 hbo 覆盖前),
  `I` = 本次推导的 filter 输入行数,均为该次规划的值,不是注入时的历史值。
  判定极小:`E<=1` 或 `E<=I*hbo_filter_small_ratio`(默认 1e-3);例如应用
  `E=1,I=100000`、跳过 `E=20000,I=20000`(文档中原先写的
  `hboSkipped=filterSmallGuard(E=1,I=100000)` 属笔误:`E=1` 会命中而非跳过,
  已改正为 `skipped=filterSmallGuard(E=20000,I=20000)`)。

验证:./build.sh --fe EXIT=0(checkstyle 0 违规);`hbo_filter_small_guard_test`
新增断言(应用时 `hboType=filter_small`+`hboUsed=true`;跳过时
`type=filter_small skipped=filterSmallGuard(E=…,I=…)` 且无 hboUsed;EXACT
对照为 `hboType=exact`)通过;自有的 7 个 hbo 套件全部 failed 0。

### Release note

None(实验特性可观测性增强)

### Check List (For Author)

- Test: FE 构建通过;回归新增断言并通过
- Behavior changed: No(仅 EXPLAIN 输出)
- Does this need documentation: No
### What problem does this PR solve?

Problem Summary: hbo 的 struct info(其 sha256 即 hbo 缓存键 fingerprint)原先是对
`group.getFirstLogicalExpression()` 做结构递归得到的。同一个 memo group 里的所有表达
式逻辑等价,但形状不同:exploration 规则(JoinCommute、InnerJoinLAsscomProject、
InnerJoinRightAssociateProject、JoinExchangeBothProject 等)会把 `(A join B) join C`
与 `A join (B join C)` 放进**同一个 group**(规则结果按 ApplyRuleJob 落回被匹配表达式
的 owner group,只有新造出的中间 join 才开新 group),DPhyp 的 join reorder 也是把各种
join order 灌进同一个 group。于是"取组内第一个表达式"使描述符取决于哪个表达式先被
copyIn:同一组表 + 同一组谓词的不同分组会得到不同 fingerprint,pin / learned 全部 miss,
连 join 链之上的聚合指纹也跟着分组变。

实测(hbo_je_t1/t2/t3,同表同谓词):
- `(t1 join t2) join t3`:join fp bb2a145a…,agg fp 82fef985…
- `t1 join (t2 join t3)`:join fp bdbd27df…,agg fp 07736ec5…

改为把 struct 定义为 group 的属性(与 MV 的 StructInfo 同一思路:用关系集合 + 层级无关
的谓词标识一棵子树),去掉所有与生成 fp 无关的东西(不引入 hyper graph、shuttle map、
equivalence class / lineage),并补上结合律归一:

- inner / cross join 链整体拍平成一个节点 `J{inner,c:[…]}(leaf;leaf;…)`:链上每个
  join 的条件合并成一个有序集合(单个谓词算 hash 还是 other 取决于分组,不能进描述符),
  叶子(scan / filter / agg / 不可拍平的 join)按自身 canonical 串排序;
- cross 与 inner 合并(无条件的 cross 就是无条件的 inner,输出行数相同);
- outer / semi / anti / asof 不可结合,保留 join 类型与 children 的 memo 顺序;
- 聚合只保留 group by keys 与孩子:聚合的输出行数是分组个数,与聚合函数、输出表达式
  无关,因此 `count(*) … group by a` 与 `sum(b) … group by a` 共用一条条目;
- 去掉 scan 的 occurrence 序号(`S{table#0,…}` → `S{table,…}`):occurrence 的角色由
  外层条件里的 alias 体现,而同表、同分区裁剪、同 visible version 的两次 occurrence 行数
  本就相同。

验证:./build.sh --fe EXIT=0(checkstyle 通过);新增回归
`hbo_struct_group_level_test`:`T1 join T2 join T3` 与 `T1 join (T2 join T3)` 的 join 链
struct 逐字节相同(并断言整条链的精确 canonical 串 `J{inner,c:[…]}(S{…t1,…};S{…t2,…};S{…t3,…})`)
且 fingerprint 相同、其上层聚合的 struct / fingerprint 也相同;交换 join 顺序结果不变;两表
子链在三表查询里保持同一 struct,故子链上的注入可复用;同 group by 不同聚合函数共用一条 agg
条目;同 group by 不同子链不塌陷;hbo 目录既有自有套件(structinfo inject、
show statistics、learned injection、filter granularity、filter small guard、join
expansion、persist)全部通过。

注意:描述符变化 ⇒ 已持久化到 __internal_schema.hbo_statistics 的旧行、以及按旧
canonical 写的 `STRUCT` 字面量失效(STRUCT 由 sha256(canonical) == fingerprint 校验,
旧字面量会被直接拒绝而不会误用);hbo 仍是默认关闭的实验特性。

### Release note

None(实验特性,默认关闭;仅 struct 描述符 / fingerprint 的生成规则变化)

### Check List (For Author)

- Test: FE 构建通过;新增回归套件 + 既有 hbo 套件全部通过
- Behavior changed: Yes(struct info / fingerprint 的计算规则变化,见上)
- Does this need documentation: No
### What problem does this PR solve?

Problem Summary: canonical struct info(指纹输入)长且密,用它定位一条 hbo 条目很不方便,而
"这是哪棵子树"往往一眼就能判断——join 类型、连接条件、聚合函数对判断没有帮助(join 链已经
拍平、聚合行数只由 group by 决定),scan 的 catalog / visible version 也只是噪音。同时
`HBO SHOW STATISTICS ... LIKE` 原先匹配的是 fingerprint,而用户手上通常只有 EXPLAIN 打印的
struct。

本次改动:

1. **simple struct**(`SimpleStructInfo`,canonical 的纯函数,不持久化也不会过期):

   | 节点 | canonical | simple |
   |---|---|---|
   | scan | `S{internal.hbo_test.t,p1/2,v3}` | `S{hbo_test.t,p1/2}`(去 catalog(非 internal 保留)、去 version;同名表重复时加 `#k`) |
   | filter | `F{EqualTo(col(…x.b),lit(1:INT))}(…)` | `F{x.b = 1}(…)`(算子符号化;`;`=AND、`|`=OR、嵌套加括号;字面量写值,no_literal 粒度写 `*`;单表结构里该表的列省略限定符) |
   | join | `J{inner,c:[…]}(S{…t1…};S{…t2…};S{…t3…})` | `J{S{hbo_test.t1}, S{hbo_test.t2}, S{hbo_test.t3}}`(不显示 join 类型与连接条件) |
   | agg | `A{gb:col(…x.a)}(…)` | `A{x.a}(…)`(不显示聚合函数与输出列) |

   simple 允许碰撞(要区分就看 canonical),它的全部用途就是让人快速定位条目。

2. **SHOW 接口**:`HBO SHOW [PINNED|LEARNED] STATISTICS [FULL] [LIKE '<pattern>']`
   - 默认打印 simple(列名 `SimpleStruct`),`FULL` 打印 canonical(列名 `StructInfo`);
   - `LIKE` 匹配**被打印的那一列**(不再匹配 fingerprint),支持 `%`/`_` 与反斜杠转义
     (表名里的 `_` 很常见,与 Doris SQL LIKE 语义一致:`%hbo\_test%` 才是字面下划线);
   - 新增 `State` 列:`live` / `stale`(记录的表 visible version 已前进或表已删除)/
     `unknown`(struct 无法解析),由 `HboStructFreshness` 从 canonical 里的 `S{…,vN}` 与目录
     当前版本对比得到,不需要额外持久化;
   - learned 条目:注入时若带 `STRUCT`,其 canonical 记入内存(`HboPlanStatisticsManager`),
     SHOW 才能显示/匹配;只按 fingerprint 注入时该列为 `-`(列表里仍能看到)。

3. **SET 的 STRUCT 校验**:pinned 的 `STRUCT` 变为强制(缺失即报错,提示从 EXPLAIN 复制
   `struct=`),并按 `sha256(canonical) == fingerprint` 强校验;若把 canonical 里的字面量全部
   通配成 `lit(*)` 后能匹配也算通过(这样 filter 的"不带常量"指纹可以直接复用同一份 struct)。
   字面量里的 `)`(如 `lit(abc:VARCHAR(10))`)用括号计数扫描,不用正则。learned 的 `STRUCT`
   可选,给了就同样校验(此前 learned 直接拒绝 STRUCT)。

验证:./build.sh --fe EXIT=0;BEL 无关。回归:hbo 目录自有套件 8 个全部通过
(`hbo_show_statistics_test` 已扩展为 FULL / simple / LIKE 目标 / 转义 / `State` live→stale 的
覆盖,`hbo_struct_group_level_test`、`hbo_structinfo_inject_test`、`hbo_learned_injection_test`、
`hbo_filter_fingerprint_granularity_test`、`hbo_filter_small_guard_test`、
`hbo_join_expansion_inject_test`、`hbo_persist_internal_db_test`),各套件改为从 EXPLAIN 的
`struct=` 取 STRUCT;`hbo_persist_internal_db_test.out` 由 `-forceGenOut` 重新生成。

未包含:`HBO DELETE STALE STATISTICS [OLDER_THAN <sec>]`(复用 `HboStructFreshness` 判定)留待后续。

### Release note

None(实验特性,默认关闭;SHOW 的输出列与 LIKE 语义、SET 的 STRUCT 要求有变化,见上)

### Check List (For Author)

- Test: FE 构建通过;hbo 自有套件 8 个全部通过(其中 SHOW 套件覆盖 simple/FULL/LIKE/State)
- Behavior changed: Yes(SHOW 默认列改为 simple、LIKE 改为匹配 struct、SET pinned 必须带 STRUCT)
- Does this need documentation: No
### What problem does this PR solve?

Problem Summary:

1. `__internal_schema.hbo_statistics` 的创建时间列是 `create_time_ms bigint`(epoch 毫秒),
   直接 `SELECT` 只能看到一个 13 位数字,与 `__internal_schema` 其它表(`table_statistics.update_time
   datetime`、`audit_log.time datetimev2(3)`)的惯例不一致。改为 `create_time datetime(3)`:
   写入用 `yyyy-MM-dd HH:mm:ss.SSS` 字面量(system 时区,与 SHOW 的 `TimeUtils` 显示一致),读回用
   ISO 解析(Doris 会把小数末尾的 0 去掉,固定 pattern 解析不了)。`ensureTable` 增加一次列探测
   (`SELECT create_time ... LIMIT 0`,每 FE 一次),列不存在时在日志里直接给出
   `DROP TABLE __internal_schema.hbo_statistics`,避免"表已存在但列不对"时持久化静默失效。

2. 新增 `HBO DELETE STALE STATISTICS [OLDER_THAN <sec>]`:hbo 指纹里含表的 visible version,所以
   表版本前进(或表被删除)后旧条目永远不会再命中,只是白占缓存位。命令删除这类条目 ——
   `stale`(版本前进/表已删)直接删;`unknown`(struct 里的表无法解析,例如别的集群写入的条目)
   默认保留,只有给了 `OLDER_THAN` 且创建时间超过该秒数时才删;`live` 保留。仅处理 pinned 条目
   (learned 按内部生成的指纹索引,不带表版本),删除复用 `removePinnedPlanStatistics`,因此内存与
   内部表 best-effort 行一起清理。语法沿用 identifier 词 + builder 校验(`STALE`/`OLDER_THAN`),
   不新增保留字;与 `HBO DELETE STATISTICS '<fp>'` 无歧义(后者要求字符串字面量)。

验证:./build.sh --fe EXIT=0。新增回归 `hbo_delete_stale_statistics_test`:持久化行的时间列是
`datetime`(cast 成字符串后按 `yyyy-MM-dd HH:mm:ss` 断言)、live 条目不被清理、unknown 条目在无
`OLDER_THAN` 时保留而在 `OLDER_THAN 1` 时被删除(内部表行同时消失)、表 insert 后条目变 stale 并
被下一次清理删除、`DELETE BOGUS STATISTICS` / `DELETE STALE STATISTICS WRONG_WORD 5` 报错。hbo
目录自有套件 9 个全部通过(含 `hbo_persist_internal_db_test`)。本机环境因列改名需要重建内部表,
已手工 `DROP TABLE __internal_schema.hbo_statistics`(FE 会自动重建,设计文档已记录)。

### Release note

None(实验特性,默认关闭;内部表列名/类型变化,旧表需重建)

### Check List (For Author)

- Test: FE 构建通过;新增回归套件 + hbo 目录 9 个套件全部通过
- Behavior changed: Yes(新增 HBO DELETE STALE STATISTICS;内部表 create_time_ms → create_time datetime(3))
- Does this need documentation: No
### What problem does this PR solve?

Problem Summary: rebase the hbo feature branch onto the current master (fe92389). Two upstream
changes have to be picked up by the hbo code:

- `org.apache.doris.statistics.Statistics` / `ResultRow` moved (`Statistics` to
  `...statistics.model`, `ResultRow` to `...statistics.repository`): the moved imports are applied
  (the two conflicts raised by the rebase were exactly these import hunks);
- a `@ConfField` description of `hbo_filter_small_ratio` was longer than the 120 character
  checkstyle limit, which is fixed by wrapping it (it passed the FE build but broke the checkstyle
  run of `build.sh --be`).

验证:./build.sh --fe EXIT=0、./build.sh --be EXIT=0(DORIS_THIRDPARTY 指向本机已升级的
thirdparty,因为 master 已把 thrift 升到 0.24.0);FE/BE 已用新构建重启,`show backends` Alive=true,
旧库表可查,hbo 套件全部通过。

### Release note

None

### Check List (For Author)

- Test: FE/BE 构建通过;hbo 套件在 rebase 后全部通过
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?

Problem Summary:

1. **expansion 成为 hbo statistics 的一个类型**(与 filter_small 平级):它原先只存在内存(独立
   Caffeine 缓存,FE 重启即丢、多 FE 不共享)。现在是 `PinnedType.JOIN_EXPANSION`
   (`stats_type='join_expansion'`),与其它 pinned 条目共用同一张
   `__internal_schema.hbo_statistics` 表(新增 `expansion double NULL` 列,`row_count` 记 0)、
   同一套 write-through / 启动懒加载 / tombstones / LRU(`hbo_pinned_stats_cache_num`),因此
   FE 重启后自动恢复。`DELETE STALE` 对 expansion 一律跳过(它按 join 条件建键、不含表版本,
   既不会 stale,也不该被 `OLDER_THAN` 误删)。原独立缓存与 `hbo_expansion_cache_num` 配置删除。

2. **命令统一为具名参数**,取消 EXPANSION 专用语句:

   ```sql
   HBO SET STATISTICS VALUE=<n> [TYPE=EXACT|FILTER_SMALL|JOIN_EXPANSION] FINGERPRINT='<fp>' STRUCT='<struct>';
   HBO DELETE STATISTICS [LEARNED] FINGERPRINT='<fp>';
   ```
   `HBO SET EXPANSION` / `HBO DELETE EXPANSION` / `HBO SHOW EXPANSION STATISTICS` 删除(后者的
   内容并入 `HBO SHOW STATISTICS`)。JOIN_EXPANSION 的 VALUE 是膨胀倍数(允许小数,>= 1,
   key = sha256(STRUCT),STRUCT 即 `JE{...}`);其它类型的 VALUE 是行数(必须非负整数)。
   语法细节:`TYPE`/`VALUE`/`STRUCT` 都在 nonReserved 里(也可作 identifier),单个可选组会被
   ANTLR 读成"下一个参数名是 TYPE",因此参数名改用 `hboWord`(仅 IDENTIFIER / quotedIdentifier),
   并把带 TYPE 的形式拆成独立分支,同时补上 `TYPE` 后缺失的 `EQ`。

3. **EXPLAIN 便于复制**:`fingerprint` / `fingerprintNoLiteral` / `struct` / `condFingerprint` / `cond`
   以及物理计划节点内联的 `hboFingerprint` / `hboFingerprintNoLiteral` / `hboStruct` /
   `hboCondFingerprint` / `hboCond` 一律加单引号,可直接粘进 `HBO SET STATISTICS`,不必手工补引号。

4. **SHOW 统一**:`Rows` 列改名 `Value`(行数;JOIN_EXPANSION 显示 `1000x`),JOIN_EXPANSION 的
   `Granularity` / `State` 显示 `-`。

验证:./build.sh --fe EXIT=0;hbo 自有套件 9 个全部通过(expansion 套件改为新语法并新增 struct 与
fingerprint 不匹配的用例;persist 套件新增"expansion 落库 / DELETE STALE 不删 / DELETE 删除"三条
断言,`.out` 由 `-forceGenOut` 重新生成);本机手工验证两种类型的落库行分别为
`(exact, 123456, NULL)` 与 `(join_expansion, 0, 1000)`,并在 FE 重启后 SHOW 仍可见。

### Release note

None(实验特性,默认关闭;命令语法、SHOW 列与 explain 输出格式有变化,见上)

### Check List (For Author)

- Test: FE 构建通过;hbo 目录 9 个套件全部通过;手工验证入库与重启恢复
- Behavior changed: Yes(命令语法统一、expansion 持久化、explain 值加引号、SHOW 列变化)
- Does this need documentation: No
@englefly

Copy link
Copy Markdown
Contributor Author

/review

@github-actions

Copy link
Copy Markdown
Contributor

Codex automated review failed and did not complete.

Error: Review context preparation failed before Codex ran; inspect the 'Prepare authoritative PR context and required AGENTS guides' step.
Workflow run: https://github.com/apache/doris/actions/runs/39257617829

Please inspect the workflow logs and rerun the review after the underlying issue is resolved.

…ered

### What problem does this solve?

Problem Summary: a set operation branch writes the columns it feeds its outputs with, and the mapping
named only the output slots. When a transparent project inside the branch computes such an output
(`Project(u.a % n AS x)`), the value the branch produces is that definition, not the slot: two unions
which differ only by `n` rendered one mapping (`[col(x)]`) and shared an exact key, although for
t.a = 0 and u.a = 2 they produce one versus two distinct rows.

The mapping is now resolved through the definitions the branch exposes before it is rendered (the same
substitution every node above a project uses). The mapping is also rendered in the literal mode of the
node, so the constant agnostic form of a mapped union still folds a constant of a branch definition
exactly like the fold of a pasted struct info does (the general fold test covers this now).

### Release note

Two set operations whose branches compute their output differently no longer share an hbo statistics
entry.

### Check List (For Author)

- Test: Unit Test
    - Two corpus cases pin a union whose second branch computes `u.a % 2 AS x` and the same union with
      `% 3`; a test asserts the exact keys differ and that the mapping names `Mod(...)`. The regenerated
      file only gained those cases and its count line - no existing key moved - and 191 cases of the
      stats package plus the HBO command suite pass.
- Behavior changed: Yes - the mapping of a branch which computes an output names its definition.
- Does this need documentation: No
@englefly

Copy link
Copy Markdown
Contributor Author

/review

…c key

### What problem does this solve?

Problem Summary: the literal mode of a join folded the literals of its conditions, so the published
learned key of `Join(t.a = u.a AND t.b > u.b + 1000)` and of the same query with `+ 1` was one entry.
The learned input matching only compares the scan predicates of the two queries, and they are equal
here: a profile collected for `+ 1000` (output zero) was applied to the `+ 1` query, which produces 1000
rows. The condition of a join decides which pairs of rows survive, exactly like the order literal of a
top-n, so it cannot be folded.

A join now renders its conditions with their literals in both modes (the header is protected, so the
fold of a pasted struct info copies it verbatim as well), which keeps the two bounds apart in the
published and learned keys. The constant agnostic key of a join therefore names its residual constants
now; the exact key is unchanged.

### Release note

A learned hbo statistics entry of a join with a residual constant is no longer applied to a query with
another constant.

### Check List (For Author)

- Test: Unit Test
    - Two corpus cases pin a join with `t.b > u.b + 1000` and the same join with `+ 1`; a test asserts
      their keys differ in both literal modes and that the constant is not folded. The regenerated file
      gains those cases and moves the constant agnostic keys of the join cases which carry a literal
      (the intended change of this commit); 192 cases of the stats package plus the HBO command suite
      pass.
- Behavior changed: Yes - the constant agnostic key of a join keeps the literals of its conditions.
- Does this need documentation: No
@englefly

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes. Static review of head 555693ea33bf0b4e4dc326e3bc2b501cbe24cd93 verified seven new P2 findings and three already-reported P1 findings that still apply. The new inline comments cover alias serialization, computed JE operands, outer-join null extension, ordered aggregate identity, simplified SHOW output, learned-input freshness and injected-history replacement.

The existing blocking findings are retained without duplicate comments:

  • 4237343730: computed grouping producers are now represented for exact pins, but automatic publication/lookup still uses NO_LITERAL. An unchanged full-read scan can therefore reuse the two-group history of a % 2 for the three-group query a % 3; input matching only checks scan predicates.
  • 4237343778: real zero counters after storage pruning still take the “not measured” branch. A zero-result grouped query can publish a nonempty input list, then reuse zero after a large matching-row insertion because freshness returns UNKNOWN/applicable.
  • 4238272481: the ordinary local replacement/delete race is repaired, but a newer SET from an FE with a different system timezone can still be suppressed/deleted by an older tombstone. Persisted naive timestamps are written and parsed in each FE's system zone, so their reconstructed generation order differs.

The repeated pinned-only producer report 4244350575 is fixed for its exact-pin scenario and is not carried as an outstanding blocker. Other previously reported issues remain duplicate fences; their applicable residuals were recorded in the review ledger rather than reposted. Repaired P1 mechanisms include sample/partition/tablet/table identity, physical-only groups, null-safe JE comparisons, UDF scope, literal escaping, MATCH/VARIANT/SEARCH metadata, set mappings, windows/TopN and aggregate-output lineage.

All 115 authoritative changed paths were reviewed, including every physical state-preserving rebuild, six UDF implementations, parser/admin commands, runtime publication/persistence, unit fixtures, both golden corpora, all regressions and eight legacy suite renames. Two reviewers covered the full scope and a separate reviewer audited the identified risk mechanisms; the main reviewer independently verified and deduplicated accepted findings. The supplied user-focus file specified no additional focus.

Required checkpoint Conclusion
Goal and proof Stable group-level history, manual injection, freshness and inspection are implemented, but the inline and existing findings prevent reliable matching/reuse. Added tests characterize many paths; they do not prove the full feature correct.
Scope and abstraction The broad change is cohesive HBO work and reuses providers/caches. Multiple canonical/display representations require parity; the new findings expose missing semantics at those boundaries.
Concurrency and locks Foreground admin operations and the background loader serialize pinned memory/SQL phases under pinnedLoadLock; planning reads do not acquire it. Profile child traversal uses its child lock. SQL under the admin monitor can delay other admin operations. No new deadlock path was established; historical cache-swap/reload races remain separately fenced.
Lifecycle and ownership Memo caches and PlanNodeId fingerprint snapshots were traced through optimization/publication. Rebuilds copy state holders; parent registration precedes runtime-filter processing. The loader exits after its documented one-time load. Java changes have no cross-TU initialization dependency. Injected learned-entry lifetime has the new wildcard replacement defect.
Configuration Mutable ratios and persistence gates are read on their affected paths; pinned cache capacity is startup-bound. The scan-count session setting is forwarded. No additional missed dynamic update was substantiated.
Compatibility and upgrades Identity grammar/hash changes invalidate old matches by design; SQL schema/struct-length guards were checked. No executable ABI, user-data storage format or FE/BE wire change was introduced. Multi-FE timestamp ordering still has the existing residual above.
Parallel paths Logical/physical representatives, physical-only/partial aggregate guards, all modified reset methods, local routing, input-source eligibility, exact/agnostic lookup and final runtime publication were traced. The distinct JE consumer misses producer resolution despite the repaired J path.
Conditions and failures Scope/type/finite-value/duplicate-parameter/length validation and unsupported-plan fallback were checked. StatisticsUtil.execUpdate inspects ERR and throws, so store catches observe SQL failure. Runtime counter population and zero-sentinel assumptions remain faulty.
Tests and negative cases Unit/regression additions cover composition, selectors, injection, guards, parser rejection and replacement-delete sequencing. Important gaps include real counter populations, wildcard-to-measured replacement, ordered aggregates and outer null extension. No build or test was run under the static-only contract.
Result files All 76 actual generated-corpus PLAN cases had consistent shape SHA pairs under static inspection. The 21 captured cluster cases are historical fixtures. Simplified SHOW expectations fail by the new finding; other raw-marker/pruned-row assertions are existing review context. Output generation provenance was not verified.
Observability EXPLAIN applied/skipped/mode details, SHOW baselines and warnings support diagnosis. SHOW's simplified identity text and newest-run display can disagree with actual matching, as described inline. No additional critical logging/metric omission was substantiated.
Persistence and failover Advisory statistics use internal SQL, a one-time per-FE loader, tombstones and explicit best-effort writes rather than a user-data EditLog path. Local replacement protection was checked. Cross-FE timezone order and previously reported reload limits remain; restart/failover behavior was not executed.
Writes, crashes and atomicity Reviewed writes affect advisory HBO entries. Best-effort persistence can diverge from local cache by its documented contract. User transaction visibility, visible-version rules and delete bitmaps are unchanged by these paths; no new user-data atomicity defect was established.
FE/BE propagation Existing profile fields carry the counters; no new execution variable/wire field was added. BE producers were traced to key/index/zone-map pruning and scanner returns, confirming the freshness population mismatch.
Performance and memory Empty-cache/relation/scan-count gates and memoization bound common planning work. State/snapshot retention and loader/admin locking were reviewed. No additional substantiated performance or ownership defect was found beyond already-known cache concerns.
Other issues and final recheck Rechecked metadata outside expression children, aliases/ExprIds, null-supplying join sides, duplicate runtime keys, same-table alignment, actual-zero measurements and persistence ordering. Every concrete candidate and R1-R4 risk has an accepted, duplicate or evidence-backed dismissed disposition. No changed path or concrete candidate remains unreviewed.

Validation is static source/diff/API analysis only. No builds, tests, source changes, live query execution or multi-FE experiments were performed; runtime values/interleavings and generated-output provenance were not experimentally verified. The review's convergence result is reported separately in the delivery metadata.

Convergence: false after three rounds. The final round required correcting O1-3's reduced Project to show its forwarded join key. The substantive finding and anchor remain verified, but the delivered wording changed after the final freeze. No concrete production question or changed-path coverage gap remains; the three-round cap requires delivery of this verified set.

Review did not converge after 3 rounds. This automated review cannot pass the review check.

Round 3 required adding forwarded join key q.k to O1-3's reduced Project illustration after the final set was frozen. Not all assigned reviewers returned NO_NEW_VALUABLE_FINDINGS for an unchanged delivered set. All seven substantive findings, anchors and three existing blockers are verified; all 115 changed paths are covered, with no unresolved concrete candidate. Validation remains static only: no builds/tests/live queries/multi-FE experiments or output-generation provenance verification.

Existing P0/P1 findings confirmed for this head: #67549 (comment), #67549 (comment), #67549 (comment)

// only is described through its physical scan, and its key must name the same relation as the
// logical description of the same scan
if (scan instanceof LogicalOlapScan) {
return escapeNamePart(((LogicalOlapScan) scan).getTableAlias());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Escape commas in scan aliases before composing identity

A quoted alias such as x,y is valid SQL and reaches tableAlias unchanged, but escapeNamePart leaves its comma in the new #ra component. Thus SELECT * FROM t AS `x,y` WHERE `x,y`.a IS NULL produces a scan token like S{internal.db.t#ti:42#ra:x,y,v2,r1000}. The structured shape hashes the entire alias, whereas stripScanBaseline truncates this token at its first comma, leaving only #ra:x. Consequently HBO SET rejects the fingerprint/STRUCT pair copied directly from EXPLAIN in either literal mode. Encode commas in this component so alias identity survives the canonical-to-shape round trip.

List<String> pairs = new ArrayList<>();
for (Expression conjunct : join.getHashJoinConjuncts()) {
if (conjunct instanceof EqualTo || conjunct instanceof NullSafeEqual) {
pairs.add(GroupStructInfo.normalizeExpression(conjunct));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Resolve computed hash-key producers in expansion identity

For SELECT * FROM (SELECT a % 2 AS x FROM t) q JOIN u ON q.x=u.k, the reduced tree is Join(q.x=u.k, Project(a % 2 AS x, Scan(t)), Scan(u)). The hash conjunct reaching this method is the slot equality q.x=u.k; the modulo remains in the child Project. Changing % 2 to % 3 therefore yields the same JE key here, since this path never resolves the slots' producers. With 1000 rows of a=2 and 1000 rows of k=0, the two joins produce 1000000 and zero rows, yet an expansion pinned for the first forces the same estimate onto the second. The separate J fingerprint's producer resolution does not repair expansion lookup. Resolve computed operands, or decline expansion keys whose definitions are unavailable.

private String renderScan(String header) {
String token = scanToken(header);
String[] parts = header.split(",");
StringBuilder sb = new StringBuilder("S{").append(simplifiedTableName(parts[0]));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Remove physical identity markers from simplified SHOW text

Real scan headers now contain #ti and optionally #ra, but this renderer treats the whole first comma-separated component as a table name. For example, a filter on internal.hbo_test.hbo_sp_r.b displays as F{hbo_sp_r.b = 1}(S{hbo_test.hbo_sp_r#ti:123}) instead of F{b = 1}(S{hbo_test.hbo_sp_r}). The same raw-header handling in tableName prevents the single-table qualifier from being elided. Besides breaking the new SHOW and learned-injection expectations, this makes their HBO SHOW PINNED STATISTICS LIKE '%F{b = 1}%' queries miss the actual entry. Derive the display name and owner from the parsed descriptor's table field while retaining any deliberate self-join disambiguation.

String table = HboScanDescriptor.qualifiedNameOf(scan.getTable());
// the input rows of a scan node are the rows it read, i.e. the number a catalog row
// count (which also ignores the predicates) can be compared with
recordedRowsByTable.merge(table, input.getInputRows(), Long::sum);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Compare learned freshness using the same input population

The newly collected ScanRows/RowsRead values are not the selected partition's catalog row count: BE applies key/index/zone-map pruning before incrementing raw_rows_read for ScanRows, and RowsRead counts rows returned by the tablet reader. An indexed equality that reads one row from an unchanged 1000-row partition can therefore record a positive input of 1 here. ofLearnedRows compares that with the live scan descriptor's 1000 and marks the history STALE, so the identical query cannot reuse its learned filter-on-scan and ancestor estimates despite no data change. Retain a comparable catalog baseline at publication or compare populations with the same meaning; transferring the runtime counters alone does not provide that baseline.

// an entry injected by HBO SET LEARNED STATISTICS carries no input table statistics; it is a
// wildcard that matches by fingerprint alone. A published entry always carries the rows of at
// least one scan (see InputTableStatisticsInfo#hasInputTables, the publish path drops entries
// which recorded none), so an empty input list can only be a manual injection

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Allow real observations to supersede injected learned entries

HBO SET LEARNED creates an entry with an empty input list, which this branch always returns first. If a later qualifying execution publishes an aggregate under the same NO_LITERAL key, Profile.updatePlanStatistics cannot replace that wildcard: getAccurateStatsIndex requires equal input-list sizes, so the measured entry is appended instead. Subsequent executions only replace that measured entry, leaving the wildcard first indefinitely with the default recent-run limit. Planning consequently keeps the injected row count even after real profiles arrive, while SHOW displays the newer measured value. Remove/supersede the injected wildcard during real publication, or prefer matching measured observations before falling back to it.

// arguments keep a different number of rows in another time zone
return className + '<' + encodeSlotComponent(sessionTimeZone()) + '>';
}
return className;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Include ordered-aggregate flags in expression identity

OrderExpression stores ASC/DESC and null ordering in its orderKey, outside children(). This generic rendering therefore gives GROUP_CONCAT(v, ',' ORDER BY ord ASC) and its DESC variant identical expressions. Those wrappers survive NormalizeAggregate and their flags reach execution. In the reduced tree Filter(s='a,b') -> Aggregate(k, GroupConcat(...) AS s) -> Scan(t), the two order variants keep one row versus zero for a group containing (v='a',ord=1) and (v='b',ord=2). Yet the resolved aggregate producer produces the same exact F fingerprint and unchanged scans pass freshness, so an exact pinned row count crosses between the queries. Serialize the direction and null-order flags here, as the explicit window/TopN order renderers already do.

}
Map<ExprId, Expression> definitions = new LinkedHashMap<>();
for (Child child : node.children) {
definitions.putAll(child.node.columnDefinitions);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve null extension when forwarding producer definitions

Compare these reduced shapes, using nullable BIGINT u.v and the same scan/slot qualifiers:

Aggregate(gb=x)
  LeftJoin(t.k=q.k, Scan(t), Project(q.k, COALESCE(q.v,0) AS x, Scan(u AS q)))
Aggregate(gb=x)
  Project(COALESCE(q.v,0) AS x)
    LeftJoin(t.k=q.k, Scan(t), Scan(u AS q))

This merge carries the first x's definition through the nullable side, so both grouping expressions resolve to the same COALESCE and their exact A keys match. For t keys [1,2] and u row (k=1,v=0), the lower expression is null-extended on the unmatched row, giving two groups [0,NULL]; the upper expression gives one group [0]. Nested q aliases retain identical qualifiers and the computed Projects survive normalization. An exact pin therefore crosses different cardinalities on unchanged scans. Preserve the null-extension boundary in producer lineage, or decline these definitions.

### What problem does this solve?

Problem Summary: the text of a literal was the text of its value, and the value of a null is the same as
the text of the string `null`: `CAST(NULL AS STRING)` and `'null'` have one data type and rendered one
`lit(null:STRING)`. A predicate over the two arguments keeps a different number of rows (`CONCAT_WS`
skips the null and keeps the string), so an exact pin of one applied to the other on an unchanged scan
baseline.

A null literal now renders a reserved character in front of its text, which the encoder turns into an
escape that no ordinary value produces (`lit(%00null:STRING)` vs `lit(null:STRING)`). The text of every
other literal, and therefore every existing key, is unchanged.

### Release note

Two plans which differ only by a null argument and the string `null` no longer share an hbo statistics
entry.

### Check List (For Author)

- Test: Unit Test
    - Two corpus cases pin a filter over the null literal and the same filter over `'null'`; a test
      asserts their exact keys differ and that the null literal carries its marker. The regenerated
      file only gained those cases and its count line - no existing key moved - and 193 cases of the
      stats package plus the HBO command suite pass.
- Behavior changed: Yes - the exact key of a plan with a null literal moved once.
- Does this need documentation: No
…aries

### What problem does this solve?

Problem Summary: the text of a literal was the text of its value, and the value of an array is a list
whose text separates its elements with an apostrophe a value may carry: the two-element array
`['a','b']` and the one-element array `['a', 'b']` (value `a', 'b`) rendered one
`lit([a', 'b]:ARRAY<TEXT>)`. A filter over `ARRAY_OVERLAP(t.arr, <the literal>)` keeps a different
number of rows for the two arrays, so an exact pin of one applied to the other on an unchanged scan
baseline.

The elements of a composite literal are literals themselves and are now rendered by the same rule as
every literal of a canonical (`lit(<value>:<type>)`, separated by a comma), so an apostrophe inside an
element cannot be taken for the boundary between two elements. A scalar literal, and therefore every
existing key, is unchanged.

### Release note

Two plans which differ only by the elements of an array literal no longer share an hbo statistics entry.

### Check List (For Author)

- Test: Unit Test
    - Two corpus cases pin the two arrays above and a test asserts their exact keys differ; the
      regenerated file only gained those cases and its count line - no existing key moved - and 194
      cases of the stats package plus the HBO command suite pass.
- Behavior changed: Yes - the exact key of a plan with an array literal moved once.
- Does this need documentation: No
@englefly

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes on head 4326f45. This static review covers all 115 changed paths. Three new P2 findings are attached inline: external scan alias identity, JOIN_EXPANSION relation binding, and merged scan-input populations.

Existing P1 comments 4237343730 and 4244350575 still apply to the learned path and are carried forward without reposting. The producer-definition fix now distinguishes exact keys, but automatic publication uses NO_LITERAL:

Aggregate(gb=x)
  Project(a % n AS x)
    Scan(t)

With unchanged a=0..999, n=2 gives 2 groups and n=1000 gives 1000. Their NO_LITERAL A keys coincide (the committed %2/%3 corpus already demonstrates this folding); input matching checks scan predicates/partitions, and identical full-table inputs pass freshness. The second query can therefore inherit the first aggregate's learned count. Preserve grouping-definition literals or validate them before reusing history. The other prior P1 reports were rechecked against their actual fixes, including global-UDF scope, selectors/table identity, SHOW routing, stale removal, complete learned inputs, physical-only groups, window/TopN/UNION/Generate identity and snapshot sentinels. Existing P2/P3 issues remain in their original threads and were not duplicated.

Critical checkpoint conclusions:

  1. Goal and proof: group fingerprints, injection, inspection, growth guards and pinned persistence are implemented; the accuracy goal remains incomplete for the verified findings. Authored tests cover many mechanisms, with the gaps below.
  2. Scope and reuse: the broad FE change is related to HBO. Shared identity/state-copy helpers replace the old tree fingerprint path; the eight legacy suite moves are unchanged-content renames.
  3. Concurrency and locks: memo/render caches are planner-thread local. Profile publication, admin sessions and the loader share concurrent caches; pinned mutation/loading serializes on pinnedLoadLock. Internal SQL holds that lock and can delay writers; planner lookup is memory-only, so the investigated recursive-load deadlock path is absent. The existing config-swap race remains known context.
  4. Lifecycle: fingerprints are snapshotted as strings before memo disposal; copied mutable-state holders are independent. Reset passes precede parent/runtime-filter registration. The loader performs one successful load per FE; cache/reload limitations remain in existing threads. C++ static initialization is unaffected.
  5. Configuration: ratios and persistence are consulted dynamically; pinned capacity is explicitly startup-only, and the scan-count session limit is forwarded. The new relation-ID cache is not rebuilt by the config callback; no additional material failure was established from that omission.
  6. Compatibility: no FE-BE wire field changes. Canonical identity changes can invalidate old keys. The internal table checks required columns and reports old schemas; automatic schema migration is absent.
  7. Parallel paths: logical/physical plans, internal/external scans, cloud/local metadata, filter/join/aggregate lookup, pinned/learned entries and both literal modes were traced. The two alias findings identify separate remaining branches.
  8. Conditions and errors: UNKNOWN soft application, strict-version mode, FILTER_SMALL, runtime-filter safety and unsupported/partial-stage gates were checked against callers. Internal SQL errors propagate; old unsupported snapshots use a non-null sentinel.
  9. Test coverage: inspected FE unit tests, both corpora, all regression changes and negative SQL cases. Current tests miss the newly reported alias/population cases and rely heavily on injected entries. The window association fixture changes the filtered output name too, and composite-literal fixtures miss syntax-valued elements. No builds or tests were run under this review contract.
  10. Result artifacts: all 158 generated shape fingerprints match their stored SHA256 values in a read-only consistency check. The changed regression .out content was inspected; generation provenance and runtime success were not established. The captured cluster corpus is historical evidence, not a current EXPLAIN execution.
  11. Observability: EXPLAIN modes/keys/skip states and SHOW freshness/detail provide useful diagnostics. Existing simplified-display and incomplete-load reports still limit them; persistence failures include fingerprints in warnings.
  12. Persistence and failover: pinned writes/deletes use internal SQL transactions rather than new EditLog records. FE-local memory and one-time reload semantics are documented. Generation-bound deletes and stale-entry identity checks address earlier races; unordered bounded reload and cross-FE time interpretation remain existing concerns. Restart/failover behavior was inspected statically.
  13. Writes and crashes: user-data transaction/visibility/delete-bitmap paths are unchanged. Internal persistence is best effort, with documented restart resurrection after failed deletion; no additional verified crash-related failure was found.
  14. FE-BE values and invariants: existing runtime counters are consumed without new thrift fields. The known runtime-counter versus catalog-population issue remains separately reported. No changed BE memory ownership or data-version protocol is involved.
  15. Performance: empty-cache/relation prefilters, scan limits and memoized rendering constrain common work. Append-only relation indexes and unordered full reload retain previously reported limitations. No additional proved performance finding was added.
  16. Other issues and deduplication: all 158 existing comments/replies were checked, candidates have explicit accepted/duplicate/dismissed dispositions, and every changed path has recorded reviewer coverage. No additional user focus was supplied. No unresolved candidate or unreviewed changed path remains; execution, restart and multi-FE validation remain outside this static-only review.

Convergence: true after 2 review rounds. All three latest reviewers returned NO_NEW_VALUABLE_FINDINGS on the same frozen comment set; all candidates and risk foci have explicit dispositions. Validation remained static only.

Review converged after 2 round(s).

Existing P0/P1 findings confirmed for this head: #67549 (comment), #67549 (comment)

if (explicitlyVersioned) {
return invalidNode("selected file scan " + name, ctx);
}
return scanRecord(Kind.FILE_SCAN, HboScanDescriptor.ofUnreadableState(table));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve alias bindings in external scan keys

The OLAP #ra fix does not cover this FileScan branch. Compare these reduced trees in one external catalog/database:

Join(x.a=y.a AND x.b<y.b, FileScan(t AS x), FileScan(u AS y))
Join(x.a=y.a AND x.b<y.b, FileScan(u AS x), FileScan(t AS y))

For NOT NULL BIGINT (a,b) data t=(1,1), u=(1,2), they return one versus zero rows. Alias lowering preserves x/y in forwarding Projects, but ofUnreadableState(table) drops tableAlias; sorting the t/u leaves therefore gives identical exact J keys. The relation prefilter matches, and UNKNOWN external freshness permits the first pin to override the second estimate. This is the separate external-scan instance left after the existing OLAP alias-binding report. Carry the file scan's alias-to-table binding into its descriptor, or resolve predicates to scan occurrences before sorting.

List<String> pairs = new ArrayList<>();
for (Expression conjunct : join.getHashJoinConjuncts()) {
if (conjunct instanceof EqualTo || conjunct instanceof NullSafeEqual) {
pairs.add(GroupStructInfo.normalizeExpression(conjunct));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Bind expansion operands to their underlying relations

For the reduced trees Join(x.a=y.a AND x.b<y.b, Scan(t AS x), Scan(u AS y)) and the same join over Scan(u AS x), Scan(t AS y), these bare slot expressions retain identical x/y qualifiers. With 1000 copies of (a=1,b=1) in t and 1000 copies of (a=1,b=2) in u, the joins return 1000000 versus zero rows, yet both JE keys match. A factor of 1000 pinned for the first therefore gives the second an estimate of 1000000. The OLAP J key's new #ra fields do not participate in this separate lookup. Unlike the existing computed-key-producer report, every operand here is a bare scan slot; its missing identity is the table that the SQL alias names. Resolve JE operands to stable relation/column identities before normalizing them.

PlanStatisticsWithInputInfo published = outputPlanStatisticsMap.get(publishedKey);
outputPlanStatisticsMap.put(publishedKey, new PlanStatisticsWithInputInfo(
published.getId(), published.getPlanStatistics(),
published.getInputTableInfo().merge(inputTableStatisticsInfo)));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve separate input populations for same-key scan runs

Consider a qualifying execution with runtime filters OFF of SELECT count(*) FROM (SELECT a FROM t WHERE b=1) x JOIN (SELECT a FROM t WHERE b=2) y ON x.a=y.a. The derived aliases wrap Projects, so alias lowering leaves both underlying scan aliases empty and they share the S fingerprint. If both scans read all N mixed rows before filtering, this merge retains one node's output but concatenates both inputs into one run. On the identical next query, historical predicate matching accepts both recorded RelationIds, then scan fallback compares that run's recorded 2N with the single current scan's N and rejects it as STALE at the default tolerance. This is a new consequence of the same-key publication fix, distinct from the existing pruning-counter mismatch: each original scan population is individually comparable. Keep alternative node observations as separate runs under the fingerprint instead of concatenating their inputs.

### What problem does this solve?

Problem Summary: the identity of an external scan was its table name only, because a file scan has no
data state to describe. The alias a query gives a relation is what binds its predicates to a table, and
nothing else named it for such a scan: `Join(x.a = y.a AND x.b < y.b, FileScan(t AS x), FileScan(u AS
y))` and the same join over `FileScan(u AS x), FileScan(t AS y)` render the same operands, and the
leaves of both plans sorted by their table names coincide, so an exact pin of the first applied to the
second (their external data state is unknown, which the lookup accepts).

A file scan now carries the alias of its relation in its descriptor (the `#ra` marker the olap scan
already writes), so the two bindings keep different keys. A file scan without an alias is unchanged, so
no existing key of such a scan moved.

### Release note

An hbo statistics entry of an external scan is no longer applied to a query which binds the same
conditions to other tables.

### Check List (For Author)

- Test: Unit Test
    - `HboScanDescriptorIdentityTest` pins the shape identity of an aliased file scan and that a scan
      without an alias keeps its old identity; 15 cases of that suite and the stats package pass, and
      the hbo regression suite keeps its legacy-config baseline.
- Behavior changed: Yes - the alias of a file scan is part of its key.
- Does this need documentation: No
…name

### What problem does this solve?

Problem Summary: an injected join expansion is keyed by its join conditions, and those render the alias
a query gave a relation (`x.a = y.a`). Two queries which give the aliases the other way round
(`t AS x, u AS y` against `u AS x, t AS y`) render the same operands while they join other tables, so
one factor applied to both: with 1000 rows of (a=1, b=1) in t and 1000 rows of (a=1, b=2) in u, the two
joins return 1000000 and zero rows under the same key.

The key now names the table every relation below each side of the join is bound to. An inner join is
commutative, so the bindings of both sides are one set for it; an outer join preserves a side, so each
side keeps its own (with the table bindings next to the qualifiers it already rendered). The key of a
join expansion whose aliases name the same tables is unchanged.

### Release note

An hbo join expansion entry is no longer applied to a query which binds the same operands to other
tables.

### Check List (For Author)

- Test: Unit Test
    - `HboJoinConditionsTest` pins two inner joins with the same operand text and swapped alias
      bindings: their keys differ while the outer join side form and the residual literal form are
      unchanged; the stats package passes and the hbo regression suite keeps its legacy-config baseline.
- Behavior changed: Yes - the table each alias is bound to is part of a join expansion key.
- Does this need documentation: No
…are a fingerprint

### What problem does this solve?

Problem Summary: the provider stores one entry per fingerprint, so the input statistics of two runtime
nodes which carry the same fingerprint (the two scans of a self join, for instance) were folded into
that entry by concatenating their lists. Both nodes read the same table population - the scan token
names the table, its index and its partitions, not the predicate - so the entry described a table which
holds the rows of both scans at once: the drift comparison sums the rows of every recorded scan of a
table, and the next query of a single scan was rejected as STALE by construction.

Folding now keeps one measurement per population (the same table, index and partitions), the larger of
the two, while two different scans are both kept. The measured rows of a table are a property of the
population, not of how many nodes happened to read it.

### Release note

A learned hbo statistics entry of a query with two scans of one table is no longer judged stale by the
next query which reads that table once.

### Check List (For Author)

- Test: Unit Test
    - `InputTableStatisticsInfoMergeTest` pins that two nodes reading one population fold into one
      entry and that scans of two tables are both kept. The existing fold test now gives its two nodes
      distinct ids; the stats package passes and the hbo regression suite keeps its legacy-config
      baseline (11 of 19 suites pass, 8 legacy suites fail).
- Behavior changed: Yes - the folded input statistics keep one measurement per scan population.
- Does this need documentation: No
@englefly

Copy link
Copy Markdown
Contributor Author

/review

### What problem does this solve?

Problem Summary: a grouping key which names a definition of the sub tree is rendered in the key of the
aggregation, and the literal mode folded the literals of that definition. The learned (constant
agnostic) key of

    Aggregate(group by x) <- Project(a % n AS x) <- Scan(t)

was therefore one for `n = 2` and for `n = 1000`, although with unchanged a = 0..999 the first produces
2 groups and the second 1000. The learned input matching only compares the scans (unchanged here), so a
count collected for the first query was applied to the second.

The grouping keys of an aggregation now keep their literals in both modes, exactly like the conditions
of a join: a definition inside a key decides the number of groups, so it cannot be folded. The
aggregation header is protected as well, so the fold of a pasted struct info copies it verbatim. A
grouping key without a literal (a plain column) renders as before.

### Release note

A learned hbo statistics entry of an aggregation is no longer applied to a query with another grouping
definition.

### Check List (For Author)

- Test: Unit Test
    - The two corpus cases `aggregate_grouping_by_a_computed_project` and
      `aggregate_grouping_by_another_computed_project` now differ in their constant agnostic keys, and a
      test pins that the definition is part of that key. The regenerated file moves only those two
      agnostic keys (the intended change); 197 cases of the stats package plus the HBO command suite
      pass.
- Behavior changed: Yes - the constant agnostic key of an aggregation keeps the literals of its
  grouping keys.
- Does this need documentation: No
@englefly

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional corrections are needed for safe learned reuse and usable expansion injection. This review adds three P2 findings and two P3 findings: computed set-branch literals disappear from learned keys; JE bindings differ between memo lookup and EXPLAIN; measured zero bypasses learned freshness; identical literal-mode keys hide the learned-use marker; and repeated filters share incompatible guard diagnostics.

Reviewed head: 232ef31. All 115 changed paths, including unit/golden/regression artifacts and eight unchanged legacy moves, were covered by the main reviewer, two normal reviewers and a separate focused risk reviewer. No additional user focus was supplied.

Applicable critical checkpoint conclusions:

  • Goal and proof: the change implements reusable HBO group identities, injection, freshness, persistence and diagnostics, but the five inline findings identify gaps. Existing component tests do not cover these complete invocation paths.
  • Scope and reuse: production changes form one coherent HBO feature; physical-plan state propagation is broad but systematic. The custom grammar requires consistent producer and stage handling.
  • Concurrency and locks: planners own their memo; daemon/admin mutations share pinnedLoadLock, while planner cache reads avoid it. Conditional stale deletion protects local replacement. No new deadlock was substantiated; the existing config-cache swap report remains duplicate context.
  • Lifecycle and initialization: loader startup/retry/one-time completion, memo lifetime, physical-plan snapshots and cache expiry were traced. No Java static-initialization cycle or new backend ownership issue was found.
  • Configuration: ratio/persistence fields are read dynamically; pinned-cache size is explicitly startup-only; max-scan and fingerprint settings are session scoped. The loader remains available while persistence is disabled.
  • Compatibility: keys change with the canonical grammar, so older keys can miss. Internal-table schema checks reject missing columns and log the required operator action. No new FE/BE wire format or storage-page format is introduced.
  • Parallel paths: logical/physical scans, memo/chosen joins, final/partial aggregates, file scans, UDF kinds, set operations and scan-filter fallback were checked. The memo/chosen JE mismatch is inline; incomplete external-input automatic publication is now rejected.
  • Conditions and errors: unsupported/arity/sample gates are explicit; internal SQL errors propagate. UNKNOWN baselines intentionally allow reuse, but measured zero is incorrectly routed there in the new finding.
  • Test coverage: unit/golden and regression assertions were inspected, including negative cases. Real collection/reuse across zero growth and memo-to-EXPLAIN key equivalence need coverage; isolated injection tests do not prove those paths.
  • Expected results: the golden computed-branch pair explicitly shares a NO_LITERAL hash. Existing stale raw scan-format regression expectations remain covered by prior thread 4313810872. Generated provenance of the new .out cannot be independently proven by static inspection.
  • Observability: fingerprints, guard reasons, apply states, SHOW baselines and collection warnings exist. The missing learned-use marker and shared guard-outcome diagnostic are inline; no additional metric requirement was substantiated.
  • Persistence and failover: durable HBO state uses internal SQL, not a new EditLog payload; each FE has a local cache and documented one-time load. Known timestamp, incomplete-load reporting and bounded-reload concerns remain existing threads.
  • Writes and crash atomicity: in-memory updates precede documented best-effort persistence; object identity and generation-qualified deletion protect the reviewed replacement cases. Failed durable writes can lose acknowledged pins after restart by the documented policy. Transaction data writes and visibility are unchanged.
  • FE/BE propagation: existing runtime-profile counter fields are consumed; no new transmitted variable needs extra sender/receiver paths.
  • Performance: empty-cache checks, relation summaries, scan-width limits and lazy memoized strings bound normal work. The append-only relation prefilter and reload retention remain known concerns; no further measured performance claim is made.
  • Other core invariants: committed/uncommitted visibility, visible-version read limits, rowsets and MoW delete bitmaps have no changed execution path. No new BE allocation or MemTracker obligation is introduced.

All 161 supplied raw comments/replies were treated as duplicate fences. The original P0/P1 instances were independently checked against current fixes; none is carried as an existing blocking ID. This does not mean all prior lower-priority reports are resolved: Cast/mark identity, null extension, ordered aggregates, comma aliases, simplified SHOW text, computed JE operands, scan population, wildcard precedence and same-key observation coherence remain known context and were not reposted.

Validation is static only: no builds, tests, cluster execution or source modifications were performed, as required. Runtime, restart and multi-FE behavior was traced in code rather than executed. No unresolved candidate or uncovered changed path remains in the accepted review set.

Review converged after 3 round(s).

out.append(SEP);
}
Child child = node.children.get(i);
out.append(branchMappingText(child.mapping, mode));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve computed branch literals in learned keys

Consider the reduced UNION DISTINCT tree Aggregate(gb=x) -> UnionAll(mappings=[t.a],[u.a % n], Scan(t), Scan(u)), with unchanged t.a=[0] and u.a=[2]. The new producer resolution distinguishes %2 from %3 in WITH_LITERAL, but this call renders the mappings in NO_LITERAL during automatic publication, replacing either divisor with lit(*). The grouping above the union retains no other copy of n, so both learned aggregate keys match although execution produces one versus two groups. Scan predicates and input rows also match, allowing the %2 observation to override the %3 estimate. The two computed-branch golden cases already have identical NO_LITERAL hashes. Keep branch mapping expressions as identity in both modes and make the pasted-STRUCT fold agree. This is the remaining automatic learned path after the exact producer-mapping fix.

/** The table each relation of a plan is bound to, as the alias the plan names it by. */
private static Set<String> bindingsOf(Plan plan) {
Set<String> bindings = new TreeSet<>();
List<Plan> relations = plan.collectToList(planNode -> planNode instanceof CatalogRelation);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Follow memo groups when collecting expansion bindings

During statistics derivation the reduced tree is Join(t.a=u.a, GroupPlan(tGroup), GroupPlan(uGroup)): Memo.replaceChildrenToGroupPlan replaces the scan children before StatsCalculator.estimate visits the join. GroupPlan is a leaf, so this ordinary tree traversal finds no CatalogRelation and the lookup key ends in bindings:[]. EXPLAIN instead calls canonicalOf on the chosen PhysicalHashJoin with real scan descendants, producing nonempty table bindings. Consequently, copying its JE fingerprint/STRUCT into HBO SET TYPE=JOIN_EXPANSION succeeds but the next optimizer lookup requests another hash and silently ignores the factor; outer joins have the same mismatch inside sides. Resolve GroupPlan children through their memo groups using the same binding rules as the chosen-plan annotation. The current unit tests construct full join trees and miss this conversion boundary.

// table, so teaching the publish path to read the counter (or to record the catalog count as
// the baseline) has to introduce a distinct sentinel at the same time, or a table which grew
// from empty becomes a silent pass.
if (live == null || recorded == HboScanDescriptor.UNKNOWN || recorded == 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Distinguish measured zero from a missing learned baseline

A qualifying execution with runtime filters OFF of Filter(b=42) -> Scan(t) can genuinely read zero rows when index/key pruning finds no matches in a nonempty table. RuntimeProfile now transfers that measured ScanRows/RowsRead=0, and the nonempty ScanPlanStatistics input list passes publication. After inserting many matching rows, the same scan fingerprint and predicates still match, but this recorded==0 branch skips the only baseline and returns UNKNOWN. applyLearnedStats therefore reuses the old zero output, including through the filter's scan fallback, despite arbitrary growth. This is the opposite of the prior unpopulated-counter false-STALE problem: the counter is now populated and zero is real. Preserve measurement availability separately from the value, or record a comparable catalog baseline, so a measured zero cannot silently bypass growth checking.

Object withLiteralStruct = node.getMutableState(MutableState.KEY_HBO_STRUCT_WITH_LITERAL)
.orElse(null);
if (withLiteralFingerprint != null && withLiteralStruct != null
&& !withLiteralFingerprint.equals(fingerprint)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Preserve the learned-use marker for identical literal keys

For a literal-free Aggregate(gb=k) -> Scan(t) or equijoin, WITH_LITERAL and NO_LITERAL have the same fingerprint. Injecting a LEARNED entry for that printed key applies successfully, but computeAggregate/computeJoin finds it in the first WITH_LITERAL iteration and records appliedModes[key]=with_literal. This branch suppresses the identical WITH_LITERAL line and emits only no_literal; appendHboEntryLine then rejects the mode comparison and omits used=true. Thus EXPLAIN presents an actively used learned entry without its usage marker. Normalize the recorded mode when the keys coincide, or recognize learned use by the shared key on the sole displayed line.

// state of its own and is only reported as used
String skipReason = guardSkips.get(key);
String applyState = applyStates.get(key);
if (skipReason != null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Track guard outcomes per filter occurrence

Consider Join(Project(a, Filter(b>=999, Scan(t))), Project(a, Filter(b>=1, Scan(t)))) over an analyzed 1000-row table with b=0..999 and NDV=1000. Derived aliases wrap the Projects, leaving the underlying scan aliases empty, so both filters share their NO_LITERAL F key. Pin that key as FILTER_SMALL: the first delegate estimate is 1 and applies the pin, while the second is near 1000 and fails the guard. HboPlanInfoProvider retains both skip and apply states using only queryId/fingerprint, and this skip-first branch reads the same entries for both physical nodes. EXPLAIN therefore labels even the applied occurrence as skipped, regardless of evaluation order. Store decisions by plan occurrence or memo group as well as fingerprint, so the healthy occurrence's rejection cannot overwrite the pathological occurrence's diagnostic. This is a manual pinned-guard case and does not involve learned profile merging.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed all 115 changed paths at 232ef31 using the supplied aggregate diff. Three new findings are inline. The existing P1 4238272481 still blocks: a UTC FE's failed DELETE can suppress and delete a newer SET written as a zone-free DATETIME by a UTC-8 FE. Local generation checks do not establish a shared time basis. This is carried as an existing blocker, not reposted. Other revalidated existing P0/P1 reported scenarios have fixes in this head; remaining known P2/P3 concerns are retained as duplicate context.

Applicable critical checkpoint conclusions:

  • Goal and proof: group-level HBO keys, manual pinned/learned management, drift guards, persistence and diagnostics are implemented, but the learned-observation mismatch and existing cross-FE write-loss scenario prevent full correctness. Added unit/regression fixtures provide broad static evidence; no runtime proof is claimed.
  • Scope and reuse: changes are broad but related to HBO/planner/profile/SQL behavior. Central descriptor, renderer, store and state-copy helpers provide reuse; no unrelated source change identified.
  • Concurrency: admin workers, loader daemon, planners, profile publishers and config callbacks were traced. pinnedLoadLock serializes local memory/SQL updates; planner reads remain lock-free. SQL under that monitor serializes admin operations. No new cyclic lock order demonstrated; existing cache copy/swap concern remains known.
  • Lifecycle: planning snapshots survive memo release, unsupported/partial/storage aggregation nodes are explicitly excluded, and rebuilt plan state holders are independent. Loader startup/retry/exit and pending-delete lifecycle were checked; the existing cross-zone replay failure remains.
  • Configuration: ratios and persistence are hot-read, pinned cache size is explicitly startup-only, and session scan cap/annotation controls are observed during planning. New relation-table cache misses mutable policy updates (inline P3).
  • Compatibility: no new BE wire field or EditLog format. Persisted table columns are checked and incompatible older schema requires explicit recreation. Identity additions intentionally change affected internal keys; generation timestamps still lack a shared time basis.
  • Parallel paths: logical/physical plans, Java/Python UDF families, exact/coarse filter/join/aggregate lookups, scan fallback, expansion, cloud/external exclusions and local SHOW routing were inspected. Existing memo JE mismatch is already reported.
  • Conditional behavior: supported shape/arity/scan limits, FILTER_SMALL guards, finite/exact VALUE checks, canonical validation and length gates have concrete roles. UNKNOWN freshness reuse is the documented soft strategy; known zero/runtime-population issues remain duplicate findings.
  • Test coverage: all 16 changed Java test files, both golden resources and all 20 regression/data paths were reviewed. Real multi-occurrence learned matching and cross-zone failed-delete replay lack end-to-end coverage; the new expansion regex failure is inline, and existing identity-display/drift assertions remain known failures.
  • Changed results: the sole .out matches the storage-only statements statically. Fixture-generation provenance and actual execution results cannot be established in this static-only run; no tests/builds were run.
  • Observability: EXPLAIN/SHOW expose keys, baselines, guard/apply/skip states and last use; loader/store/snapshot diagnostics exist. Existing occurrence/mode marker issues are not reposted.
  • Persistence and failover: internal-table writes are best effort and FE memory remains authoritative; one-time per-FE loading/no live refresh is documented. Conditional stale removal fixes the local race, but existing P1 cross-FE replay can still lose an acknowledged statistics write.
  • Writes, atomicity and crashes: this changes statistics storage, not user transaction data. No user-data visibility, MoW bitmap or BE commit-path change. Documented failed-write resurrection and the confirmed generation issue limit statistics durability.
  • FE/BE state: existing runtime counters are consumed without changing thrift schema; all affected collection/lookup paths were traced. Runtime versus catalog input-population mismatch is already reported.
  • Performance and memory: empty-cache/relation prefilters, scan cap, group memoization and lazy rendering bound normal fingerprint work. Java caches own the added state; append-only relation-key overapproximation is an explicit tradeoff, with no additional demonstrated failure in this review. The mutable policy omission is inline.
  • Other issues and final recheck: existing threads/raw replies fence repeated identity, null-extension, timezone, freshness, expansion and display concerns. Each risk/candidate has an accepted, duplicate or evidence-based dismissed disposition in the ledger.

No additional user focus was supplied. Coverage is complete for the required static scope; builds, tests, source edits and live FE restart experiments were prohibited by the review contract. Convergence is recorded separately by the submission helper.

Review converged after 3 round(s).

Existing P0/P1 findings confirmed for this head: #67549 (comment)

merged.merge(identityOf(statistics), statistics, InputTableStatisticsInfo::largerRows);
}
for (PlanStatistics statistics : other.inputTableStatistics.get()) {
merged.merge(identityOf(statistics), statistics, InputTableStatisticsInfo::largerRows);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep the predicate occurrence with its learned output

For UnionAll(Project(a, Filter(b=1, Scan(t))), Project(a, Filter(b=2, Scan(t)))), both unaliased scans have the same S fingerprint. If both read N mixed rows but return 100 and 900 rows, this dedup keeps only the first scan's predicate/RelationId; genPlanStatisticsMap also keeps only its output. On the identical next query, getMatchedEntry fetches predicates using that retained historical RelationId for either filter's scan fallback. It therefore validates the retained occurrence for both filters, and freshness now passes at N versus N, so both receive the same output count. This is the new incorrect reuse after replacing the previously reported 2N concatenation, without data growth or runtime filters. Preserve separate output/input observations and match the requested occurrence before applying one.

def condCanonical = "JE{INNER_JOIN;EqualTo(col(internal.hbo_test.hbo_je_t1.a),col(internal.hbo_test.hbo_je_t2.a))}"
// the expansion entry of the join is listed as its own line of the annotation block
def matcher = (annotation(query) =~
/\] join type=join_expansion literal_mode=no_literal fingerprint='([0-9a-f]+)' struct='JE\{INNER_JOIN;EqualTo\(col\(internal\.hbo_test\.hbo_je_t1\.a\),col\(internal\.hbo_test\.hbo_je_t2\.a\)\)\}'/)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Extract the complete expansion canonical from EXPLAIN

HboJoinConditions.canonicalOf now always appends ;bindings:[...] before the closing brace of an inner-join key. This matcher still requires the brace immediately after the equality, so matcher.find() fails before the suite can inject or check any expansion. The hardcoded condCanonical above is also missing those bindings and would fail the fingerprint check even if only the regex were relaxed. Capture the full emitted canonical together with its fingerprint, as the semi-join case below already does. This test expectation is separate from the existing memo-versus-physical lookup-key issue.

scanToFilterCache.putAll(provider.scanToFilterCache.asMap());
provider.scanToFilterCache = scanToFilterCache;

Cache<String, Map<Integer, String>> nodeIdToFingerprintCache =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Apply changed cache policies to the relation table cache

The new relationTableIdCache is built from hbo_plan_info_cache_num and expire_hbo_plan_info_cache_in_fe_second, but this callback never rebuilds it. Changing either mutable setting therefore updates the other per-query caches while this one retains its startup policy. For example, starting with unbounded size/expiry and later enabling finite limits leaves these per-query relation maps unbounded and unexpired until restart. Rebuild and migrate this cache here too, preserving in-flight writes.

### What problem does this solve?

Problem Summary: the fingerprint of a group description was the sha256 of the printed shape string, so
the injectivity of the text was a correctness requirement of the cache key: every value of the tree (a
table name, an alias, a literal) had to be escaped so that it could not stand for a delimiter of the
grammar, and each such ambiguity that was missed gave two different plans one entry (the `#` of a table
name against the `#ti:` selector, the elements of a composite literal, a NULL against the string
`null`, ...). The description is already a tree of objects, so the key does not have to travel through
its printed form.

The key is now the sha256 of a structural identity of the same tree: every node writes its kind, the
fields the canonical string prints for it and its children, and every field is written with its length
(digits in a fixed width), so the byte stream is injective over the tree by construction. A table name
which carries a character of the printed grammar cannot move into another field, and the two forms of
one plan (a transparent project kept, or inlined) still share one key because such a node writes no
field of its own. The canonical string keeps its role as the readable projection: EXPLAIN prints it,
`HBO SET STATISTICS` stores it for display and for the scan baseline, and it is no longer hashed. All
fingerprints moved once by this change; the shape strings, the scan tokens and the fold are unchanged.

### Release note

The hbo statistics keys of every plan moved once (the entries of the previous version do not match any
plan and are inert); a plan which reads an external table or a name with a special character no longer
shares its key with a plan of another relation.

### Check List (For Author)

- Test: Unit Test
    - The regenerated corpus changes only its fingerprint lines and its header (the shapes, canonicals
      and scan tokens are byte identical), and a test asserts the fingerprint is no longer the sha256 of
      the shape text; 200 cases of the stats package plus the HBO command suite pass.
- Behavior changed: Yes - the fingerprint of a description is a hash of the tree, not of its text.
- Does this need documentation: No
…it against the fingerprint

### What problem does this solve?

Problem Summary: `HBO SET STATISTICS ... FINGERPRINT=... STRUCT=...` rejected the pair when the sha256
of the stripped struct was not the fingerprint. That check existed because the fingerprint was the
sha256 of the printed shape, so a struct which did not hash to the fingerprint was certainly a copy
mistake - and with the key computed from the tree instead of the text, the two values are independent
and the check can no longer be computed at all.

The fingerprint is the key of the entry and the struct is its readable form (the scan baseline the
freshness comparison reads), so the statement stores the pasted struct under the pasted fingerprint
without deriving one from the other. The struct is still folded for the constant agnostic mode, because
that is the form the entry is stored and displayed with. The four regression cases which asserted the
rejection are removed and the comments of the persistence suite are updated.

### Release note

`HBO SET STATISTICS` no longer rejects a STRUCT which does not hash to the FINGERPRINT of the same
statement: copy both values of the target node from EXPLAIN, since the entry is now keyed by the
fingerprint alone.

### Check List (For Author)

- Test: Regression test
    - `query_p0/hbo/hbo_show_statistics_test`, `hbo_learned_injection_test`,
      `hbo_join_expansion_inject_test` and `hbo_persist_internal_db_test` are the suites this commit
      changes: the mismatch rejections they asserted no longer exist, and the persistence suite still
      checks the stored row and its deletion. The directory keeps its legacy-config baseline (11 of 19
      suites pass, 8 legacy suites fail).
- Behavior changed: Yes - a struct which does not hash to its fingerprint is accepted.
- Does this need documentation: Yes - the HBO manual statistics doc should say that FINGERPRINT is the
  key and STRUCT is copied from the same EXPLAIN node.
@englefly

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static review of #67549 at 52e2086, covering all 115 changed paths and related upstream/downstream flows. One new P2 finding: broadcast costing consumes stale learned skew after cardinality lookup rejects that observation.

Critical checkpoint conclusions:

  • Goal and evidence: stable memo identity, manual management and profile feedback are connected end to end. Author tests cover many helper/injection cases; the stale-skew consumer is uncovered, so overall correctness is not established.
  • Scope and clarity: the broad change stays within HBO, physical metadata preservation, parser and related tests. No unrelated modification found.
  • Concurrency: planner, profile/config, admin and loader access was traced. pinnedLoadLock serializes memory and synchronous persistence; SQL can delay admin operations, while planner hot reads avoid that monitor. No additional deadlock proved; the cache replacement race is already reported.
  • Lifecycle: identities are snapshotted before memo release, rebuilt nodes copy independent state holders, and the per-FE loader retries until one successful load. No distinct initialization cycle or ownership defect verified.
  • Configuration: thresholds/persistence toggle are read dynamically; pinned-cache bound is explicitly startup-only. Relation-cache policy omission remains an existing thread.
  • Compatibility: structural keys changed; old keys miss and fall back. Required persisted columns are checked; no new FE-BE protocol field. Rolling upgrades were not executed.
  • Parallel paths: logical/physical F/J/A, partial/storage aggregates, file scans, JE and rebuilds were checked. CostModel omits the freshness policy applied by HboStatsCalculator, producing the new finding.
  • Conditions/errors: sampled/selected unsupported scans, snapshot sentinel, exact row-count tokens, finite factors and STRUCT byte bounds were checked. UNKNOWN freshness and best-effort persistence are deliberate policies. Known identity/occurrence defects remain duplicate context.
  • Test coverage: unit/golden and regressions include negatives, selectors, modes, guards, races, drift, display and persistence. Real automatic feedback followed by growth, restart, concurrent and mixed-FE behavior remain unexecuted. No builds or tests were run under this static-only contract.
  • Changed results: generated corpus, historical cluster capture and persistence fixture were inspected; content-identical legacy moves do not orphan result dependencies. Known marker/regex/display expectation defects were not reposted. Results are not dynamically certified.
  • Observability: used/skipped states, keys, snapshot warnings and store logs support diagnosis. Per-occurrence diagnostic ambiguity and incomplete-load reporting remain existing limits; no additional issue accepted.
  • Persistence/failover: normal internal OLAP SQL stores pins; stale removal checks object identity and deletion tombstones carry generations. One-time per-FE visibility/best-effort failure are documented. Existing timezone and bounded reload concerns remain; crash/failover was not executed.
  • Data writes/core invariants: this manages optimization metadata and adds no source-row transaction, visibility enforcement, delete-bitmap or rowset write path. No new source-data atomicity/durability issue verified.
  • FE-BE passing: existing runtime counters and PlanNodeId fields are reused, and snapshots align with profile IDs. No new variable needs another send path.
  • Performance/memory: summary/cache gates and state copying were checked. Stale skew provably discounts broadcast input CPU contribution tenfold; an exact chosen-plan change or runtime regression magnitude is not claimed. Cache/index retention concerns are existing reports; no BE allocation-tracking change.
  • Other concerns/user focus: no additional user focus supplied. Every candidate/risk has a disposition. The incremental-scan candidate was dismissed because wrapper row-count access fails before a usable key exists.

All 169 historical comments/replies were duplicate fences. Original causes of all 34 historical P1 comments were independently checked as repaired at this head, so existing_blocking_comment_ids is empty. Remaining P2/P3 issues, including learned occurrence/population/zero handling, JE memo binding, identity/display gaps and cache/persistence limits, stay with existing threads; this review does not assert they are fixed. All changed paths received static coverage; runtime behavior and performance remain unverified.

Review converged after 2 round(s).

Optional<PlanNodeAndHash> planNodeAndHashOpt;
// the publish path keys a join with the constant agnostic form, and that is the
// form which the read side publishes for skew ratios as well
planNodeAndHashOpt = HboUtils.getHboPlanNodeAndHash(physicalHashJoin,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Check learned freshness before applying the skew discount

Consider the reduced plan HashJoin(t.k=u.k, Scan(t), Scan(u)) over unpartitioned tables. Collect a qualifying shuffle profile with runtime filters OFF and skew above the threshold, then grow the same inputs from 1,000 to 100,000 rows using uniformly distributed keys. The new version-free fingerprint still matches, and getMatchedPlanStatistics accepts the unchanged scan predicates. HboStatsCalculator rejects that observation as STALE, but this independent lookup still consumes its old skew and multiplies the broadcast input CPU contribution by 0.1. It can therefore bias distribution selection after the measured skew no longer describes the data. Retain the matched entry and check its input freshness against the current group before applying the discount.

### What problem does this solve?

Problem Summary: an injected join expansion is keyed by the sha256 of its condition canonical
(`JE{<type>;...;bindings:[...]}`), so the printed text was again the preimage of a cache key: an alias
or a literal which carries a character of that grammar (a comma inside an alias, a `lit(...)` inside a
value) could stand for a field of its own, and the two sides of the join had to be encoded into the text
so that the key could bind an alias to the table it names.

The key of an expansion entry is now the sha256 of the structural identity of the same conditions: the
join type, the sorted equality pairs, the sorted residuals with their literals and the side information
(the bindings of an inner join, each side of an outer one), each written with its length by the writer
the group description already uses. `canonicalOf` keeps its role as the readable form: EXPLAIN prints
it, the entry stores it as its struct, and it is not hashed. All expansion keys moved once.

### Release note

The keys of the manually pinned join expansion entries moved once (entries pinned with the previous
version do not match any join and are inert).

### Check List (For Author)

- Test: Unit Test
    - `HboJoinConditionsTest` pins that two joins with the same operand text and swapped alias bindings
      keep different keys and that the key is no longer the sha256 of the canonical string; 200 cases of
      the stats package plus the HBO command suite pass, and the expansion regression suite reads its
      fingerprints from the EXPLAIN annotation (so it follows the new keys).
- Behavior changed: Yes - the expansion key is a hash of the structure, not of the text.
- Does this need documentation: No
…ntity markers

### What problem does this solve?

Problem Summary: the simplified form `HBO SHOW STATISTICS` prints parses the scan token of the stored
canonical to drop the data state and the parts of the name which do not tell two entries apart. The
identity components a scan token now carries (`#ti:<table id>`, `#ra:<alias>`, `#pt:` for the partition
selection) were read as part of the table name, so the displayed row kept them
(`S{hbo_test.hbo_sp_r#ti:1792472580857}`) and the column qualifier was no longer recognised as the
table's own (`F{hbo_sp_r.b = 1}` instead of `F{b = 1}`) - the operators cannot be located by name any
more, and the LIKE filters a user writes on the simplified column stopped matching.

The simplified form now reads the scan from its descriptor, unescapes the table name, drops every
identity component and writes the partition selection back as its count (`S{hbo_test.hbo_sp_r,p1/2}`),
so the column qualifier is recognised again and the full form remains the one which tells two scans of
one table apart.

### Release note

The simplified struct info of `HBO SHOW STATISTICS` no longer carries the scan identity markers and
names its columns without their qualifier again.

### Check List (For Author)

- Test: Regression test
    - `query_p0/hbo/hbo_show_statistics_test` and `hbo_learned_injection_test` (which assert the
      simplified column and the LIKE filters over it) were run against a FE rebuilt from this branch and
      pass; the whole `query_p0/hbo` directory keeps its legacy-config baseline.
- Behavior changed: Yes - the simplified display of a scan is readable again.
- Does this need documentation: No
… sites see

### What problem does this solve?

Problem Summary: the expansion key of a join was derived from the relations of the join's sub trees
(the alias bindings and, for an outer join, the qualifiers of each side's output). Estimating a group
and printing the plan chosen for it see different sub trees for the same join, so the key the read side
computed did not equal the key EXPLAIN printed: a pinned `JOIN_EXPANSION` entry was stored under the
annotated fingerprint but never applied (the join order of the regression query did not move), and the
semi join entry was never reported as skipped either.

The key is now derived from the information the join owns: the join type, the equality pairs, the
residuals and, for an inner join, the table each qualifier of those conditions names (read from the
relations below the join, but emitted only for the qualifiers the conditions actually use). An outer
join keeps its per side qualifier list, which is what it had before the bindings were added. The
regression suite takes both the fingerprint and the struct from the EXPLAIN annotation instead of
spelling the condition text out, so it follows whatever the annotation prints.

### Release note

A manually pinned join expansion entry is applied again (its key is stable between the estimation of
the join and the annotation which prints it).

### Check List (For Author)

- Test: Regression test + Unit Test
    - `query_p0/hbo/hbo_join_expansion_inject_test`, `hbo_show_statistics_test`,
      `hbo_learned_injection_test` and `hbo_persist_internal_db_test` were run against a FE rebuilt from
      this branch and all four pass; `HboJoinConditionsTest` pins that two joins with the same operand
      text and swapped bindings keep different keys.
- Behavior changed: Yes - the expansion key of a join no longer depends on its sub trees.
- Does this need documentation: No
@englefly

Copy link
Copy Markdown
Contributor Author

/review

…ow-state check

### What problem does this solve?

Problem Summary: the drift suite checks the row-state tag of the scan token with a regular expression
which spells the token out (`S{internal.hbo_test.hbo_dr_t,v2,r1000}`). A scan token now carries its
identity markers (`#ti:<table id>`, `#ra:<alias>`) in front of the baseline, so that expression could
not match the token the new FE prints, and the suite failed before it reached its drift assertions - a
consequence of the identity change which the previous runs could not show, because the running FE was
older than the branch.

The expression is applied to the same normalized text the other assertions of the suite use (the row
count tag and the identity markers are normalized away), so it checks the row state again.

### Release note

None

### Check List (For Author)

- Test: Regression test
    - `query_p0/hbo/hbo_row_count_drift_test` was run against a FE rebuilt from this branch and passes;
      the whole `query_p0/hbo` directory then reports its legacy-config baseline (19 suites, 8 legacy
      suites fail because the environment does not support them).
- Behavior changed: No
- Does this need documentation: No
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants