feat: %TYPE/%ROWTYPE 锚定建成 [T] 引用边(detail/impact 可见,lineage 不污染) (#158) - #164
Conversation
- create_object_ref_edges 的 CreateProcedure/CreateFunction 分支:每 statement
新建 AnchorExtractor 实例 walk 例程 block,消费 anchors 建 AnchorsOn 边;
routine 内以 (object 小写, column, kind, site) 去重,覆盖签名锚定(Param/
ReturnType)与变量/嵌套锚定(Variable/NestedType)可能撞同列的场景。
- collect_package_object_ref_edges:新增 table_index/package_index 参数。
- PackageItem::Variable:锚到包节点(site=Variable)——包级变量属于包,
不属于某个例程;用包级 cursor 名集合守卫 %ROWTYPE。
- PackageItem::Cursor:收集包级 cursor 名,注入每个成员例程的
AnchorExtractor(新增 pub fn register_cursor_name),保证包级 cursor
也能守卫例程体内的 %ROWTYPE。
- PackageItem::Procedure/Function:补齐签名 Param/ReturnType 扁平串锚定
(原代码只做 body 内 ReferencesType/UsesSequence)+ body walk 新
AnchorExtractor 实例;同 routine 级去重。
- extractor.rs:抽出 anchor_from_pl_data_type 自由函数(object/column/kind
判定,不含守卫),AnchorExtractor::visit_pl_data_type 复用;新增
register_cursor_name 注入接口;补两句 doc caveat(declare-before-use 假设
+ 遮蔽同名表时保守跳过是有意行为)。
- AnchorKind/AnchorSite 补 derive(Hash)(去重 HashSet key 需要)。
- 新增 4 个 builder 单测 + 集成测试 tests/regress_issue_158_type_anchor_edges.rs
(issue #158 简化等价样例:RETURN/RESULT/变量三处锚定同列去重为 1 条,
第二张表仅通过变量锚定、无 DML,验证 inferred table* + AnchorsOn 独立于
TableAccess 共存)。
测试:
- cargo test --features full -- --skip test_path_mapping_applied --skip test_serve_
696 passed, 0 failed, 3 ignored (pre-existing)
- cargo build --features full: clean
- cargo clippy --features full -- -D warnings: clean
- cargo fmt --all -- --check: clean
…ified_key (#158) - 新增 GraphBuilder::collect_routine_anchor_edges:统一「params 扁平串解析→ return_type 解析→新 AnchorExtractor walk block」序列,消除 CreateProcedure/CreateFunction/包成员例程三处逐字重复(~35-40 行 x3)。 顶层调用传 pkg_cursor_names=&[],包成员传包级 cursor 名。 - 新增模块级 type alias AnchorDedupKey,消除 4 处重复的 6 行元组类型拼写 (anchor_dedup_key 返回类型 + 原 3 处局部 HashSet 声明,后者随重复代码 一起被消除)。 - 包级变量锚定分支改为直接调用既有 pkg_qualified_key(pkg_name) helper, 去掉内联重复实现的 schema.pkg_name 限定 key 拼接逻辑。 - fixture tests/regress/issue_158_type_anchor_edges/cases/anchor_edges.sql 顶部补 -- Issue #158 说明块,对齐 issue_140/issue_120 sibling fixture 惯例。 - 纯行为保持重构:全部既有 db88aba 测试(4 builder + 3 集成)作为安全网, 重构前后逐一确认无回归。 测试: - cargo test --test regress_issue_158_type_anchor_edges: 3 passed - cargo test --features full cursor_rowtype / should_keep_table_access_and_anchor / should_anchor_package_level: 全绿 - cargo test --features full -- --skip test_path_mapping_applied --skip test_serve_: 696 passed, 0 failed, 3 ignored(与重构前完全一致) - cargo build --features full: clean - cargo fmt --all -- --check: clean - cargo clippy --features full -- -D warnings: clean builder.rs: 8054 → 7986 行(净减 68 行,含新增 ~65 行 helper+doc)
局部 TYPE ... IS TABLE OF/RECORD(...) 的类型名,以及独立 plain RECORD
变量声明名,此前未登记进 var_names 守卫集合,导致锚定到这些本地标识符
的 %TYPE(如 v_list typ_list%TYPE、v2 rec2%TYPE)被误判为表锚,产生
伪造的 inferred table* 节点/AnchorsOn 边。
真实 AST 形态(ogsql-parser src/ast/plpgsql.rs):
- PlDeclaration::Type(PlTypeDecl) 四个变体(Record/TableOf/VarrayOf/
RefCursor)均有独立 name 字段,已有 pl_type_decl_name() helper 提取。
- plain RECORD 变量是独立的 PlDeclaration::Record(PlRecordDecl { name }),
非 PlDeclaration::Variable。
修复:AnchorExtractor::visit_pl_declaration 补两处登记 —— Record(r) 的
r.name、Type(t) 的 pl_type_decl_name(t) 均 insert 进 var_names,早于
push_anchor 的守卫检查生效。
新测试:
- should_skip_type_anchored_to_local_type_declaration
- should_skip_type_anchored_to_plain_record_variable
均先红(缺失守卫产生伪表锚)后绿。
回归:should_collect_*(5 个)/ should_skip_*(3 个既有)/
should_keep_table_rowtype_when_cursor_exists_elsewhere 全绿,无回归。
AnchorsOn 边去重键 (kind, column, site) 的 column 分量此前按原始大小写
比较:GraphBuilder::anchor_dedup_key(同 routine 内)与 GraphStore::dedup
的 anchors_on 专分支(跨阶段合并后)均未归一化,导致 emp.id%TYPE 与
emp.ID%TYPE(openGauss 对未加引号标识符的大小写折叠语义下是同一列)
被当作两条不同的边保留,产生重复。
修复:两处 column 分量统一 .map(|c| c.to_lowercase())。
新测试:should_dedupe_anchor_edges_case_insensitively_by_column
(graph::store::tests)——同 (proc, table) 两条 AnchorsOn,column
Some("id") / Some("ID"),先红(剩 2 条)后绿(剩 1 条)。
回归:should_keep_distinct_anchor_edges_through_dedup(不同列仍保留)+
should_roundtrip_anchors_on_edge_through_bincode_store 全绿,无回归。
同时在 docs/plans/2026-09-07-issue-158-type-anchor-edges.md 执行备注
末尾补记本次外部 review 修复。
* feat(lineage): 标量子查询作为 INSERT 值源时解析其首表达式 (fix #142 部分) * feat(lineage): 表锚定 %ROWTYPE 记录字段解析为表列 (fix #142 部分) * feat(lineage): SELECT * 游标 + %ROWTYPE 记录字段归因到游标表 (fix #142 部分) * feat(lineage): 整记录 INSERT VALUES r 按游标源列位置展开 (fix #142 部分) * test(lineage): 锁定游标 %ROWTYPE 记录字段写入的穿透行为 (fix #142) * docs: add issue #142 column-lineage penetration plan * fix(lineage): 标量子查询首表达式复用 classify_value_expr 分类 (review #153-1) * fix(lineage): 拦截通用 walker 递归子查询,杜绝 join 状态泄漏 (review #153-2) * style: cargo fmt (review #153-2 follow-up) * fix(lineage): FETCH 将 %ROWTYPE 记录锚定重绑到实际游标 (review #153-3) * fix(lineage): 整记录 SELECT* catch-all 不再猜测列名,避免重排错归因 (review #153-5) * test(lineage): 锁定记录字段子查询穿透并修正局限文档 (review #153-4 #153-6) * fix(lineage): 多列 SET = 子查询按位置对齐各目标列 (review 5136742683) * fix(lineage): 容器子查询先收集左操作数列再跳过嵌套 SELECT (review 5136742683)
…(对齐 table*) (#160) * feat(graph): 无 CREATE SEQUENCE 时为 seq.nextval 建 inferred seq* 节点与 UsesSequence 边 Node::Sequence 增加 explicit 标记与 Option<location>(对齐 Table/View 模式),推断节点由 builder 在 DDL 缺失时 or_insert 生成;schema 限定引用仅精确匹配 full key,不回退短名别名,避免跨 schema 误绑定。STORE_VERSION 8 -> 9。 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * test: issue #159 序列推测节点集成回归(store 落盘 / 增量 / 无重复边) Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * docs: add issue #159 inferred-sequence implementation plan Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(graph): 推测序列索引提升到构建上下文,CREATE SEQUENCE 原位升级 inferred_sequence_index 原为 create_object_ref_edges 局部缓存,跨 chunk(>100 SQL 文件)即失效:同一序列产生重复 seq* 节点,且后置 CREATE SEQUENCE 只查 sequence_index 造出兄弟 explicit 节点不升级。现提升为 GraphBuildContext 字段跨 chunk 共享;DDL 命中推测节点时原位改写 explicit/location(保留 NodeIndex 与既有边),升级遵守精确 key 纪律——限定 DDL 只升级限定推测节点,杜绝短名模糊匹配跨 schema 误绑定。 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(store): pick_richer_node 补 Sequence 臂,优先保留带 DDL 位置的节点 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * docs: add PR #160 inferred-sequence cross-chunk fix plan Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> --------- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
--files 仍只列出调用链源文件。加上 --related-ddl 后,把链上表/视图的 索引、同义词、触发器文件合并进同一段 FILES,不改变调用树。 Closes #161
- Comprehensive functional comparison across 5 dimensions (SQL parsing, graph model, query, export, deployment) - Performance benchmarks on shared ANSI SQL corpus (3 tiers) and codeweb-only PL/pgSQL corpus (2 tiers) - Results: codeweb 19x faster on large ANSI SQL parsing, 44x faster on JSON export, 5x less memory - Deep dive into flowScope column-level lineage implementation (architecture, accuracy analysis, edge types, limitations) - Scenario-based tool selection recommendations - Improvement suggestions for codeweb
| use ogsql_parser::ast::plpgsql::PlDataType; | ||
| match dt { | ||
| PlDataType::PercentType { table, column } => { | ||
| Some((table.clone(), Some(column.clone()), AnchorKind::PercentType)) |
There was a problem hiding this comment.
[bug] anchor_from_pl_data_type treats every PlDataType::PercentType as a table.column anchor, including the single-identifier form that ogsql-parser encodes as column: "". In v0.10.0 (parse_pl_data_type), v1%TYPE becomes PercentType { table: "v1", column: String::new() }. The flat parser already rejects that shape (PercentType requires ≥2 identifiers), but the AST path does not. Combined with a skip set that only knows previously declared local variables/cursors, this creates inferred table* nodes for common non-table anchors: v2 v1%TYPE at package level (builder only checks pkg_cursor_names), and v p_id%TYPE where p_id is a parameter (never inserted into var_names). Those fake tables then show up in detail/trace/impact as [T] edges.
Suggestion: Return None for PercentType when column is empty/whitespace, matching parse_anchor_from_type_string. Also seed the skip set with routine parameter names and package-level variable/type names (see Issue 2) so rec.field%TYPE against a record param/package var is still suppressed. Add tests for v p_id%TYPE and package-level v2 v1%TYPE.
There was a problem hiding this comment.
已修复 1a53283:anchor_from_pl_data_type 对 column.trim().is_empty() 的 PercentType 返回 None(对齐扁平解析器的 ≥2 段语义)。测试:should_reject_empty_column_percent_type_from_ast(空串/空白/正常表列/PercentRowType 四形态)。
|
|
||
| let mut anchor_extractor = AnchorExtractor::new(); | ||
| for cname in pkg_cursor_names { | ||
| anchor_extractor.register_cursor_name(cname); |
There was a problem hiding this comment.
[bug] Member-routine (and top-level) AnchorExtractor walks are only seeded with package cursor names. Parameter names, package-level variables, and package-level type names are not registered, even though collect_package_call_edges already collects pkg_var_names and PackageItem::Type for call extraction. So v p_emp.empno%TYPE (parameter is p_emp employees%ROWTYPE) and v pkg_rec.col%TYPE (package-level record) still have a non-empty column and pass Issue 1’s empty-column filter, then become inferred tables. Package-level variables have the same hole: collect_package_object_ref_edges only skips pkg_cursor_names (line 2104), not earlier package vars/types.
Suggestion: Add register_var_name (or a generic register_local_name) next to register_cursor_name. Before walk_pl_block, register every parameters[].name, every PackageItem::Variable name, and every pl_type_decl_name from PackageItem::Type. At package-variable site, skip if the object matches any package var/type/cursor name collected from the same pkg_items list.
There was a problem hiding this comment.
已修复 d1e249e:新增 AnchorExtractor::register_var_name;collect_routine_anchor_edges 内注册全部 parameters[].name,签名扩展 pkg_var_type_names: &[String](包级 Variable 名 + pl_type_decl_name,收集于锚定循环前);包级变量锚定点守卫扩为 pkg_cursor_names ∪ pkg_var_type_names;成员例程注入双集合。测试:should_skip_type_anchored_to_param_name(v p_emp.empno%TYPE)、should_skip_package_var_anchored_to_earlier_package_var、should_skip_type_anchored_to_package_level_record + e2e issue_158_param_name_anchor_suppressed_end_to_end。
| ), | ||
| PackageItem::Raw(_) | ||
| | PackageItem::Variable(_) | ||
| | PackageItem::Type(_) |
There was a problem hiding this comment.
[bug] PackageItem::Type(_) is continued, so package-level TYPE t IS TABLE OF x.col%TYPE / VARRAY OF / RECORD (f t.col%TYPE) never produce AnchorsOn edges. Issue #158 lists nested TYPE/record-field anchors in scope, and the routine-body visitor already handles the same PlTypeDecl variants. In real PL/SQL those nested types almost always live in the package spec/body, not in a routine DECLARE. Package-level variables in the same loop are already turned into package-node anchors, so this is an inconsistent hole in the same declare section, not a documented non-goal (D3 is cursor RETURN only).
Suggestion: Walk PackageItem::Type with the same PlTypeDecl match used in AnchorExtractor (TableOf/VarrayOf/Record fields), apply the package-level cursor/var/type skip set, and attach surviving edges to the package node with site=NestedType. Add a builder test for TYPE t_list IS TABLE OF some_table.col%TYPE inside PACKAGE BODY.
There was a problem hiding this comment.
已修复 e165644:PackageItem::Type 不再 continue——提取共享入口 pub fn anchor_targets_in_pl_type_decl(复用 anchor_from_pl_data_type,TableOf{elem_type,index_by}/VarrayOf/Record fields 全覆盖),AnchorExtractor 的 Type 臂同步改用该入口(无重复 match);包节点锚定 site=NestedType,守卫含本 TYPE 名(TYPE t_bad IS TABLE OF t_list.col%TYPE 被抑制)。测试:should_anchor_package_level_nested_type + e2e issue_158_package_nested_type_anchor_end_to_end。
| /// passes its package's cursor names so `rec pkg_cursor%ROWTYPE` inside | ||
| /// the body is guarded the same way a routine-local cursor would be. | ||
| /// Shared across the three call sites (top-level procedure, top-level | ||
| /// function, package member) that previously duplicated this sequence. |
There was a problem hiding this comment.
[suggestion] The doc comment on collect_routine_anchor_edges narrates refactor history (“that previously duplicated this sequence”) rather than a current invariant. Same class of leakage as the long “Caveats” / “Task N” commentary elsewhere in the diff.
Suggestion: Keep one sentence on why pkg_cursor_names is injected; drop the “previously duplicated” / task-history wording.
There was a problem hiding this comment.
已修复 c47fa7e:doc 改为陈述当前不变量(为何注入包级名集),移除 "previously duplicated" 历史叙事;另在 5eaf87f 补充遮蔽 caveat 明确覆盖 register_var_name 注入的参数名(参数与真表同名的保守跳过是文档化行为)。
…y tags (#154) (#163) * fix(lineage): hint column-level needs an existing table in missing-table note (#154) Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * test(graph): pin jsp/jspsql node-key tags under the jsp feature (#154) Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> --------- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
| ) { | ||
| let mut anchor_seen: HashSet<AnchorDedupKey> = HashSet::new(); | ||
|
|
||
| for param in parameters { |
There was a problem hiding this comment.
[bug] Previous issue 2 (fake table anchors from non-table names) is only fixed for the body walk. collect_routine_anchor_edges still turns every signature %TYPE/%ROWTYPE string into an AnchorsOn edge with no cursor/var/TYPE/parameter skip. pkg_cursor_names / pkg_var_type_names are injected only after this loop (and only into AnchorExtractor), so they never apply here. Concrete false positives: PROCEDURE p(p_rec c%ROWTYPE) when c is a package cursor; FUNCTION f(p_id emp_rec.empno%TYPE) when emp_rec is a package RECORD type; PROCEDURE p(p_id v_emp.empno%TYPE) when v_emp is a package-level %ROWTYPE variable. Each creates an inferred table* named c / emp_rec / v_emp. The new tests (should_skip_type_anchored_to_param_name, issue_158_param_name_anchor_suppressed_end_to_end) only cover a body variable v p_emp.empno%TYPE, not a parameter/RETURN whose type string itself points at a local name.
Suggestion: Before add_anchor_edge, drop a parsed signature anchor whose lowercased object is in pkg_cursor_names, pkg_var_type_names, or the current routine's parameter names (excluding the parameter currently being declared, so employees employees%ROWTYPE as a param still anchors the table). Add a builder test whose parameter type is a package cursor/TYPE/variable name.
There was a problem hiding this comment.
已修复 e70728a:collect_routine_anchor_edges 参数/RETURN 锚定在建边前检查 pkg_cursor_names ∪ pkg_var_type_names ∪ 参数名集(小写全串对比);排除当前参数自身名——PROCEDURE p(employees employees%ROWTYPE) 惯用法仍锚表(should_still_anchor_param_named_after_table 锁定);RETURN 无 self 排除(保守)。测试:should_skip_signature_anchor_to_package_cursor / should_skip_signature_anchor_to_package_type_and_variable。
| // variables below, and are injected into every member routine's | ||
| // AnchorExtractor so `rec pkg_cursor%ROWTYPE` inside a routine body | ||
| // is guarded the same way a routine-local cursor would be (#158). | ||
| let pkg_cursor_names: Vec<String> = pkg_items |
There was a problem hiding this comment.
[bug] The new skip sets (pkg_cursor_names, pkg_var_type_names) are built only from the current statement's pkg_items. create_sql_edges already knows spec and body are independent statements and therefore indexes SPEC items (spec_items_by_pkg) so a BODY can inherit public Variable/Type/Cursor names (see the comment at builder.rs:1624–1636 and the BODY call at 1652–1662). create_object_ref_edges / collect_package_object_ref_edges do not. A SPEC-only CURSOR c, TYPE rec_t IS RECORD (...), or package variable used from a BODY member (rec c%ROWTYPE, v rec_t.col%TYPE) is therefore unguarded and becomes an inferred table*. The new tests keep TYPE/cursor/var and the member routine in the same BODY, so they never exercise this split.
Suggestion: Mirror the call-edge path: index CreatePackage items by pkg_qualified_key, pass them as inherited_items when walking CreatePackageBody, and union them into pkg_cursor_names / pkg_var_type_names (and into any signature skip from Issue 1). Cover with a two-statement SPEC+BODY fixture.
There was a problem hiding this comment.
已修复 0697f37 + 73b50c6:collect_package_object_ref_edges 新增 inherited_items 参数,create_object_ref_edges 预扫描构建 spec_items_by_pkg(镜像调用边 :1629 模式,后提取为 build_spec_items_index 共享 helper),BODY 调用传 SPEC items;pkg_cursor_names/pkg_var_type_names 收集 .chain(inherited)。测试:should_inherit_spec_names_for_body_anchor_guards(SPEC+BODY 双语句)+ e2e issue_158_spec_body_inherited_guards_end_to_end(伪表 c/rec_t/v_emp 缺席 + 对照组锚定)。
| self.cursor_names.insert(c.name.to_lowercase()); | ||
| } | ||
| PlDeclaration::Variable(v) => { | ||
| self.var_names.insert(v.name.to_lowercase()); |
There was a problem hiding this comment.
[bug] The skip set includes the identifier currently being declared, so the standard PL/SQL idiom emp emp%ROWTYPE / emp emp.empno%TYPE never produces an AnchorsOn edge. visit_pl_declaration inserts v.name (and the TYPE name at 1241) before reading the %TYPE/%ROWTYPE target; package-level code has the same hole because pkg_var_type_names is the full item list including self (builder.rs:2115–2131). Cursor %ROWTYPE does not need this: CURSOR c; rec c%ROWTYPE is already filtered by cursor_names. Empty-column v2 v1%TYPE is already rejected by anchor_from_pl_data_type. The extractor caveat that “shadowing means the name resolves to the local declaration” is wrong at the declaration site — Oracle’s own examples use employees employees%ROWTYPE, and %ROWTYPE looks up a table/view/cursor, not the variable whose type is being written. Same-name later siblings (v employees%ROWTYPE then a package var named employees) are also over-skipped by the full-set package guard; declare-before-use (previous names only, insert-after-visit) is enough for v_id v_emp.empno%TYPE.
Suggestion: Register the current variable/TYPE name after visiting its PlDataType / nested fields. For package-level items, skip only names declared earlier in pkg_items (plus inherited SPEC names from Issue 2), never the item’s own name. Add a builder test for emp emp%ROWTYPE asserting an AnchorsOn to table emp, and keep the existing v_emp → v_id v_emp.empno%TYPE guard test.
There was a problem hiding this comment.
已修复 6b07c1d:extractor 改 insert-after-visit(Variable/Type 名在自身类型访问后注册)——emp emp%ROWTYPE 惯用法正向测试 should_anchor_variable_self_named_after_table;包级循环改增量 declared_earlier 集(种子 = SPEC 继承名,永不含 self)+ should_anchor_package_var_self_named_after_table;成员例程注入维持全集(包体整体可见);cursor 名维持全集(如 review 所述不需要)。doc caveat 同步改写(声明点自名解析为表)。回归:v2 v1%TYPE/t_bad→t_list/v_id v_emp.empno%TYPE 守卫全部保持。
| continue; | ||
| }; | ||
|
|
||
| Self::collect_routine_anchor_edges( |
There was a problem hiding this comment.
[bug] collect_routine_anchor_edges is invoked for every package member that already has a proc_idx, including SPEC signatures whose block is None. Node creation (create_package_nodes) skips body-less SPEC items, but create_sql_nodes finishes the whole file (SPEC then BODY) before create_object_ref_edges runs, so by the time SPEC is walked for anchors the BODY has already inserted the shared RoutineId node. SPEC then emits Param/ReturnType AnchorsOn edges; BODY emits the same edges again. anchor_seen is local to a single call, and neither GraphBuilder::build nor project::analyze runs store.dedup(), so JSON/detail consumers see two identical anchors_on edges on the standard Oracle pattern (CREATE PACKAGE … PROCEDURE p(t t%ROWTYPE); + matching BODY). Round-2 tests never declare the procedure in both SPEC and BODY, so they miss it.
Suggestion: Dedup signature anchors across SPEC and BODY for the same proc_idx (a pass-level (proc_idx, AnchorDedupKey) set, or collect signatures only from SPEC when a matching SPEC exists and let BODY contribute DECLARE-site anchors only). Add a SPEC+BODY fixture that asserts exactly one AnchorsOn from p to the table.
There was a problem hiding this comment.
已修复 002366b:anchor_seen 上移为调用方持有 HashSet<(NodeIndex, AnchorDedupKey)>(proc 维度入键),collect_package_object_ref_edges 一个集贯穿 SPEC+BODY——签名相同折叠、不同保留(不偏向 SPEC)。测试:should_dedupe_signature_anchors_across_spec_and_body(SPEC+BODY 双声明恰好 1 条 Param 边 + BODY 独有 Variable 边保留)。
| } | ||
| self.var_names.insert(pl_type_decl_name(t).to_lowercase()); | ||
| } | ||
| _ => {} |
There was a problem hiding this comment.
[suggestion] AnchorExtractor::visit_pl_declaration falls through NestedProcedure / NestedFunction to _ => {} and returns Continue. The default walker then recurses into the nested block on the same extractor: nested-routine parameters are never register_var_name’d (they are not PlDeclarations), so PROCEDURE inner(p_emp VARCHAR2) IS v p_emp.empno%TYPE; still mints inferred table* p_emp — the same hole round 1 closed for top-level params. Nested locals/cursors also leak into the outer skip sets. CallExtractor in this same file already save/restores scope and injects nested parameters via begin_routine_scope.
Suggestion: Mirror CallExtractor: on NestedProcedure/NestedFunction, save cursor_names/var_names, register_var_name each nested parameter, walk the nested block, then restore. Cover with should_skip_type_anchored_to_nested_proc_param.
There was a problem hiding this comment.
已修复 6370485:镜像 CallExtractor——NestedProcedure/NestedFunction 臂 mem::take 保存 cursor/var 集 → 注册嵌套参数 → 手动 walk 嵌套块 → restore → SkipChildren 防默认递归双走。测试:should_skip_type_anchored_to_nested_proc_param(伪造 p_emp 被守卫)+ should_not_leak_nested_routine_locals_into_outer_scope(行为化:嵌套后用嵌套专属名锚定,正向断言泄漏未发生;变异自证 restore 移除即 Red)。
| } | ||
| if tag == "anchors_on" { | ||
| // `AnchorsOn` edges on the same (proc, table) pair are not | ||
| // interchangeable duplicates: distinct params/vars can each |
There was a problem hiding this comment.
[suggestion] GraphStore::dedup() now keeps distinct AnchorsOn edges that share (src, dst) but differ in (kind, column, site). GraphStore::merge still keys seen_edges on (src_key, dst_key, edge_type_tag) only ("anchors_on"), so p1 emp.id%TYPE + p2 emp.name%TYPE in one input store collapse to a single edge when merged. That reintroduces the exact invariant should_keep_distinct_anchor_edges_through_dedup locked for dedup().
Suggestion: Apply the same (kind, lowercased column, site) key inside merge’s per-store edge loop (and when combining with edges already in the accumulator). A merge-path unit test parallel to the dedup one would lock it.
There was a problem hiding this comment.
已修复 d667927 + e74f41f:anchors_on 边在 merge 用扩展键(kind/小写 column/site,贯穿全部 store 的 seen_anchor_keys,天然覆盖累加器已存在边),非锚定边维持 per-store 原语义。中途曾把全局 seen 泛化到全部边类型、破坏 TableAccess 跨 store 模式并集——review 捕获后回退并加回归锁:should_union_table_access_modes_across_stores_on_merge(Read+Write → Read|Write)+ should_keep_distinct_anchor_edges_through_merge / should_dedupe_identical_anchor_edge_across_stores。
| // variable/TYPE, or another parameter's name. The *current* | ||
| // parameter's own name is excluded from the "other param" check so | ||
| // the Oracle self-naming idiom (`p(employees employees%ROWTYPE)`) | ||
| // still anchors to the real table (PR #164 review round 2 issue 1). |
There was a problem hiding this comment.
[suggestion] Several new comments narrate review history rather than the current invariant: “PR #164 review round 2 issue 1” (here), “PR #164 review round 2” on the SPEC-inheritance skip sets, “PR #164 review round 2 issue 3” on declared_earlier. c47fa7e already stripped that style once; the follow-up commits put it back.
Suggestion: Keep the self-naming / SPEC-inheritance / earlier-only rules; drop the PR-round citations.
There was a problem hiding this comment.
已修复 f4de1c9:全仓 grep 清理 "PR #164 review round N" 叙事(builder/extractor/store 23 处),保留规则语义;bc07b6d 顺带修正两处陈旧前瞻描述与 doc rewrap。
| // `NestedFunction` arms. Nested RETURN-type anchoring is not | ||
| // done — a nested routine has no independent graph node. | ||
| PlDeclaration::NestedProcedure(p) => { | ||
| let saved_cursors = std::mem::take(&mut self.cursor_names); |
There was a problem hiding this comment.
[bug] AnchorExtractor’s new NestedProcedure / NestedFunction arms save scope with std::mem::take, which clears cursor_names / var_names for the nested walk and only restores them afterward. That correctly stops nested locals from leaking outward (the previous finding, covered by should_not_leak_nested_routine_locals_into_outer_scope) and nested parameters are now registered (covered by should_skip_type_anchored_to_nested_proc_param). It also wipes names the nested routine is supposed to see. PL/SQL nested subprograms inherit the enclosing DECLARE / parameter list, and collect_routine_anchor_edges injects package-level cursor/var/TYPE names plus the outer routine’s parameters onto this same extractor before walk_pl_block. After take(), a nested body like rec c%ROWTYPE (outer/package cursor c) or v p_emp.empno%TYPE (outer param p_emp) is unguarded and still mints inferred table* c / table* p_emp attached to the outer node. CallExtractor uses the same take() because local_vars is a call-vs-identifier set that starts fresh per routine; copying that mechanic here is the wrong scope rule for %TYPE/%ROWTYPE guards. The struct comment only claims leak-out isolation; the implementation is stronger than that invariant.
Suggestion: Clone-and-restore instead of take-and-restore (saved = self.cursor_names.clone(); … self.cursor_names = saved;, same for var_names), then register nested parameters (so they can shadow). Add a test that an outer/package cursor or outer param visible in a nested body does not become a table anchor (CURSOR c IS …; PROCEDURE inner IS rec c%ROWTYPE;). Same change on the NestedFunction arm at line 1279.
There was a problem hiding this comment.
Fixed in d0353f7: both NestedProcedure/NestedFunction arms now clone() (not take()) cursor_names/var_names before registering the nested routine's own parameters and walking — outer/package cursor and parameter names stay guarded inside the nested body (lexical inheritance), while the save/restore-to-snapshot afterward is unchanged, so nested-local declarations still don't leak outward. Added two RED\u2192GREEN regression tests: should_skip_nested_body_anchor_using_outer_cursor (outer CURSOR c + nested rec c%ROWTYPE) and should_skip_nested_function_body_anchor_using_outer_param (nested-within-nested: a nested routine's own parameter guarding a further-nested body's %TYPE reference \u2014 the literal top-level-signature-parameter variant doesn't exercise this code path since extract_anchors()'s test harness never registers a CREATE FUNCTION's own signature parameters, only graph::builder::collect_routine_anchor_edges does downstream). Verified both new tests fail against the old take() code (fabricated table* c / table* p_emp anchors) before the fix, and pass after. Existing guards (should_skip_type_anchored_to_nested_proc_param, should_not_leak_nested_routine_locals_into_outer_scope) and the full regress_issue_158_type_anchor_edges suite (10/10) remain green.
| /// every other edge type (in particular `TableAccess`) must stay on the | ||
| /// per-store generic key or `merge_duplicate_table_access_edges`'s | ||
| /// cross-store `AccessMode` union never gets a second edge to union | ||
| /// (regression fixed in commit following d667927: a global generic key |
There was a problem hiding this comment.
[suggestion] anchor_merge_key’s doc-comment still narrates the review/commit history (“regression fixed in commit following d667927: a global generic key silently dropped…”) rather than stating the current invariant. The useful part is already there: AnchorsOn uses a cross-store (src, dst, kind, column, site) set; every other edge type stays on the per-store (src, dst, tag) key so merge_duplicate_table_access_edges can union modes. The commit-hash sentence is the same class of history-narration the previous pass asked to drop from builder.rs (those sites are clean now). The test at should_union_table_access_modes_across_stores_on_merge also leads with “commit d667927”.
Suggestion: Keep the invariant (separate cross-store AnchorsOn key vs per-store generic key, because TableAccess union needs both stores’ edges present). Drop the commit hash and the “what we broke last time” recap from production comments; the regression test name/body is enough history.
There was a problem hiding this comment.
Fixed in 90bde2a: both doc comments dropped the commit-hash / "what we broke last time" narration while keeping the invariant statement. anchor_merge_key's doc now states: AnchorsOn uses a merge-spanning (src, dst, kind, lowercased column, site) key so identical anchors collapse across stores while distinct columns survive, and every other edge type keeps the per-store (src, dst, tag) key so merge_duplicate_table_access_edges can still union TableAccess modes across stores. should_union_table_access_modes_across_stores_on_merge's doc now reads "Regression guard (#158): a whole-merge generic key would drop the second store's TableAccess edge before mode union; per-store keys + the dedicated AnchorsOn key above keep both behaviors" \u2014 no commit hash. grep -rn "d667927" src/ \u2192 0 hits.
* docs(plans): #165-169 列级分析查询面实施计划(Momus 两轮审核通过) 覆盖 #165 columns 查询面 / #166 文档 / #167 PL 谓词 / #168 记录字段跨表键 / #169 transform 白名单;决策 D1-D6 全部锁定(方案A 合并、RecordField 变体、独立 predicates 命令、合并版本 bump、双变体白名单、store 侧表方案)。 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * feat(parser): 函数包裹列字面量过滤 transform 白名单 + WHERE/JOIN %ROWTYPE 记录字段跨表等值键 (fix #169, fix #168) - HardFilter 增加 transform(FilterTransform,serde "fn");白名单 {substr,substring,nvl,trim,upper,lower},FunctionCall+SpecialFunction 双变体;六个比较操作符统一支持 - transform 序列化采用 is_human_readable() 分支手写实现(bincode 固定布局 / JSON 省略 None) - JoinConditionSource 新增 RecordField 变体;column_source 记录字段解析抽取为 record_field_source/resolve_record_field 复用;等值一侧为已解析记录字段时产出跨表 JoinCondition - 诊断类型补 Hash derive(方案A 并集合并前置) Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * feat(parser): PL IF/CASE 条件解析为表列谓词(branch-aware 提取器) (fix #167) - PredicateExtractor 走整个 PlBlock 保留分支结构(extract_body_sql 摊平前收集) - 置信度规则:记录字段/裸列→high;SELECT INTO 主表变量→medium;维表变量→low + param_table_hint;函数/动态 SQL→low 保留 origin,绝不静默 high - 条件转换复用 #169 column_transform_of 与 #168 记录字段解析;PredicateClause.transform 沿用 HardFilter 的 is_human_readable 序列化模式 - 过程内 SELECT INTO 变量源追踪;SELECT INTO 目标/变量数不匹配时 parse_log 告警;游标 WHERE HardFilter 不混入谓词列表 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * feat(store): procedure_predicates 侧表与版本链 v10→v12 (#167) - v10: merge_table_access_edges 诊断字段并集(#165 方案A)+ transform 预留(#169) - v11: procedure_predicates 侧表(HashMap<NodeKey, Vec<PlPredicate>>,serde default,merge 时并集) - v12: PredicateClause.transform(bincode 布局变更) - 版本拒绝基线测试不变;版本戳字面量测试同步至 12 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * feat(builder): TableAccess 诊断字段并集合并 + 过程谓词收集(归一化存储键) (fix #165) - merge_table_access_edges 对 join_conditions/hard_filters/enum_mappings/select_into/insert_columns/update_columns/column_refs 做保序 HashSet 并集,alias_map 首见优先;column_mappings/read_tables 语义不变(方案A,修复同过程多语句同表时诊断字段「保留第一条」丢失) - 过程构建期走整个 PlBlock 收集谓词,procedure_predicates 经 RoutineId::normalized() 以与 NodeKey::from_node 一致的小写键写入(修复大小写不匹配查找 miss) - project/mod.rs 接线 ctx.procedure_predicates → GraphStore Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * feat(graph): 按过程/包聚合 ColumnAnalysis 查询后端 (fix #165) column_analysis_of_routine/column_analysis_of_package 扫入边+出边逐字段去重聚合,--table 过滤;AggregatedColumnAnalysis 字段与 ColumnAnalysis 1:1(schema_version 1),供 CLI/MCP/HTTP 三面共享,不另造 schema Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * feat(cli): codeweb columns / predicates 子命令 (fix #165, fix #167) - columns --procedure|--package [--table] --format json:旧 store(<v10) 软提示重跑 analyze;unknown 过程非零退出不静默 - predicates --procedure --format json:旧 store(<v12) 软提示;输出 {schema_version, procedure, predicates} - regress_columns / regress_predicates 端到端回归(含大写过程名大小写无关解析、记录字段跨表键、param_table_hint) Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * feat(serve,mcp): columns/lineage MCP 工具与 HTTP 端点,共享 lineage 目标解析 (fix #165) - MCP 新增 codeweb_column_analysis / codeweb_lineage(与 CLI JSON 字段 1:1,空图守卫与错误措辞对齐既有工具);工具清单测试 6→8 - HTTP 新增 GET /api/v1/columns、GET /api/v1/lineage(400 非法输入 / 404 未命中,同 serde 结构直出) - lineage 目标解析抽取为 graph::lineage::parse_lineage_target 纯函数(6 个单测),cmd_lineage 行为不变 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * docs: 补齐 lineage/columns/predicates 用户与开发者文档 (fix #166) - README 中英 CLI/HTTP/MCP 三表补齐(示例与 --help 逐字一致) - user-guide §6 新增 lineage/columns/predicates 小节;DeveloperGuide 增加 ColumnAnalysis 字段表(含 transform/RecordField)与 mock 造数消费场景 - getting-started(_zh) 增加可照跑的列级血缘示例;serve-api-guide 补 /columns 与 /lineage 端点(真实 JSON 输出) Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(predicates): ELSIF/简单 CASE 采集 + 函数包裹操作数断链修复 (PR#170 review F1/F2) - If 臂遍历 elsifs 逐条产出谓词;简单 CASE(expression: Some)合成 expression = WHEN 值比较,不再把裸字面量降级为 Low - condition_operand 移除 expr_name(expr)? 提前断链:记录字段 → var_sources(expr_name 或 transform 目标列名)→ sole-table fallback 顺序保持;裸列 substr 与 SELECT INTO 变量 substr 均正确解析 - Derived 臂携带 transform(删除硬编码 None);PL 变量的 transform 提取打通 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(cli,serve,mcp): 谓词身份对齐 columns、歧义显式失败、空谓词语义化 (PR#170 review F3/F4/F5) - predicates JSON 的 procedure 取 RoutineId 裸名并新增 package 字段,与 columns 可 join(此前包内过程为 pkg.prc) - columns/predicates/MCP/HTTP 四处新查询面 fail_on_multiple=true:多匹配非零/400/error JSON 显式失败,消灭静默取首个(MCP 区分 Empty 与 Ambiguous 文案;HTTP 歧义 400、未命中 404) - resolved 无分支过程返回 predicates: [] exit 0,非零保留给未解析/歧义 - cmd_lineage 改用共享 parse_lineage_target(行为不变,regress_lineage 套件守护);文档同步 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * refactor: 精简谓词/提取器注释,去除 issue 叙事(PR#170 review F6) 仅注释:保留非显性约束(bincode 固定字段数等)并压缩篇幅,删除控制流复述与评审/issue 编号叙事 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * docs(plans): PR#170 评审修复计划(Momus 审核通过) 六项评审发现的核实记录与 F1-F6 修复方案 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(cli): columns --package 歧义子串显式失败,对齐 --procedure (PR#170 review follow-up) package 臂 resolve_single_node 补上 fail_on_multiple=true(此前仍静默取首个匹配,Ambiguous 臂不可达);新增 columns_ambiguous_package_fails_explicitly 防回归 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> --------- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
| /// reference, not self-declaration, so it resolves to the local, not the | ||
| /// table, at the point of use. | ||
| /// - A nested routine (`PlDeclaration::NestedProcedure`/`NestedFunction`) | ||
| /// gets its own isolated scope: its parameters are injected into |
There was a problem hiding this comment.
[suggestion] After the take→clone fix, nested-routine scope is lexical inheritance (outer/package names remain visible; nested params shadow on top; restore only prevents leak-out). The struct-level caveat still says a nested routine "gets its own isolated scope". That was true of mem::take and is now false; it contradicts the arm comment and the new tests. A later change that "restores isolation" by switching back to take would reintroduce the round-4 bug.
Suggestion: Rewrite the bullet to the current invariant: nested bodies inherit the enclosing (and injected package) guard sets, nested parameters are registered on top, and the pre-nesting snapshot is restored so nested locals do not leak out.
| // nested locals outward and never registering the nested | ||
| // parameters as guarded names. We prevent this by returning | ||
| // SkipChildren and manually walking the nested block with a | ||
| // save/restore barrier: *clone* (not take) the guard sets so |
There was a problem hiding this comment.
[suggestion] The NestedProcedure/NestedFunction comment still narrates the review fix (*clone* (not take)) and claims the arms "Mirror CallExtractor", but CallExtractor still uses mem::take + begin_routine_scope (a fresh scope). The follow-up that de-historicized anchor_merge_key introduced the same class of history-narration here, and the CallExtractor parallel is now inaccurate.
Suggestion: State the current contract only: clone the guard sets, register nested params, walk, restore, return SkipChildren. Drop the take/clone post-mortem and the CallExtractor claim (or note that CallExtractor intentionally uses a fresh call-resolution scope).
|
|
||
| /// Issue #158: a routine parameter's flat | ||
| /// `%ROWTYPE` signature anchor bypasses the guard entirely — the | ||
| /// signature loop in `collect_routine_anchor_edges` never consults |
There was a problem hiding this comment.
[suggestion] should_skip_signature_anchor_to_package_cursor asserts that a package cursor does guard a signature %ROWTYPE (and collect_routine_anchor_edges now consults pkg_cursor_set / pkg_var_type_set / other param names). The test comment still speaks in the present tense as if the signature loop "bypasses the guard entirely" and "never consults" those sets — the old hole, not the current invariant.
Suggestion: Rephrase as the locked behavior (signature anchors must consult the same cursor/var/param guards as the body walk), not as a description of a bypass that the test then contradicts.
概要
把 PL/SQL
%TYPE/ 表级%ROWTYPE编译期 schema 锚定建成AnchorsOn引用边(EdgeCategory::Reference,CLI 标签[T]):detail/trace/impact可见,lineage/conflicts/--summarize-tables/ community 自动排除。Closes #158。
实现要点
Edge::AnchorsOn { kind, column, site, location }:追加在 Edge 枚举末尾(bincode 序号稳定);kind: PercentType | PercentRowType、site: ReturnType | Param | Variable | NestedType。8 处穷尽 match 补臂(category / edge_type_tag / edge_weight / JSON / NDJSON / DOT / Mermaid / location_line)。AnchorExtractorvisitor:结构化 AST(变量/嵌套 TYPE/record 全是PlDataType)+ 扁平签名串兜底解析(容忍par_sys_purchase. purchase_days% type式 token 空格与大小写混乱);守卫:cursor 名与局部声明名(变量/TYPE/RECORD)命中即跳过,防伪表锚。create_object_ref_edges/collect_package_object_ref_edges走table_index既有解析路径(无 DDL → inferredtable*);包级变量锚到 Package 节点,包级 cursor 名注入成员例程;routine 内(object, column, kind, site)去重。edge_label_for聚合:同对节点平行边标签合并为[R,T](修复原先只取首边的显示缺口;petgraph LIFO 迭代经.rev()还原创建顺序)。STORE_VERSION8→9:新变体改变 bincode 布局;旧 store 拒载并提示重建,analyze fast-path 自愈(peek_version门禁复用 fix(analyze): store 版本校验缺失导致升级二进制后 stale 缓存死循环 #157 机制)。anchors_on专分支按(kind, column小写, site)保留不同锚定组合(同表多列%TYPE不被折叠)。验收对照(issue §验收)
detailCALLEES 同表[R]+[T]共存 →issue_158_dml_and_anchor_edges_coexist_on_read_table[T]可见(inferredtable*)→issue_158_type_only_reference_produces_inferred_table_with_anchor_onlyissue_158_lineage_ignores_anchor_edges/issue_158_conflicts_ignore_anchor_edgesimpact从纯锚定表可达函数 →issue_158_impact_reaches_via_anchor_edgecursor%ROWTYPE不误建表边 →issue_158_no_cursor_rowtype_anchor_edges_present等should_reject_store_with_stale_version另含 33+ 单元测试(解析/抽取/建边/序列化),关键路径均做变异验证(注入实现错误 → 测试失败)。
披露
collect_routine_anchor_edges沿用本文件既有惯例加了#[allow(clippy::too_many_arguments)](builder.rs/main.rs 已有 8 处同类)。line暂取 routine 起始行(AST 无 span)。RETURN t%ROWTYPE锚定、CGEF 回导白名单扩展anchors_on、ogsql-parser 将签名类型结构化为PlDataType。门禁(rebase 到 origin/main 后实测)
🤖 Generated with Claude Code