# CTE 支持设计：适配 PG18 原生 CteScan

## 背景

ORCA 为 CTE 生成的 DXL 物理计划结构（Sequence + CTEProducer + CTEConsumer）无法直接映射到
PG18 的执行模型。当前翻译层遇到这三类节点时直接抛出异常，导致所有含 CTE 的查询回退到
`standard_planner`。

本文档描述将 ORCA DXL CTE 节点翻译为 **PG18 原生 CteScan** 的完整设计方案。放弃
CustomScan 路线，改为让翻译层将 ORCA 的计划结构"折叠"为 PG18 期望的形式：CTE 子计划进入
`PlannedStmt.subplans`，主树中以 `CteScan` 节点引用它。

---

## PG18 CTE 预处理与 ORCA 的关系

PG18 在 `subquery_planner()` 入口处调用 `SS_process_ctes()`（`subselect.c:880`），对每个
CTE 做**内联或物化**的二选一决策。ORCA 通过 `planner_hook` 在此之前接管规划，因此两套
逻辑是**平行独立**的，但有以下关键影响：

### ORCA 接管时 CTE 尚未被 PG 处理

`SS_process_ctes()` 从未执行，`query->cteList` 保留全部原始 CTE，所有 FROM 子句中的引用
仍为 `RTE_CTE`。ORCA 看到的是完整未展开的 WITH 子句。

### ORCA 自行决定内联还是物化

ORCA 内部有 `CXformInlineCTEConsumer`（及 `CXformInlineCTEConsumerUnderSelect`）变换，
会对满足条件的 CTE 做内联优化。若 ORCA 选择**内联**，DXL 计划中不出现
CTEProducer/Consumer，翻译层无需处理；若 ORCA 选择**物化**，才会生成
`Sequence + CTEProducer + CTEConsumer` 结构，翻译层需将其折叠为 PG18 的 CteScan。

**结论：翻译层只需处理 ORCA 选择物化的情况。**

### `ctematerialized` 字段应被 ORCA 尊重

`CommonTableExpr.ctematerialized` 在 parse 阶段由语法设置：

| 值 | 含义 |
|----|------|
| `CTEMaterializeDefault` | 用户未指定，由优化器自行决定 |
| `CTEMaterializeAlways` | 用户显式写了 `MATERIALIZED` |
| `CTEMaterializeNever` | 用户显式写了 `NOT MATERIALIZED` |

ORCA 的 `CXformInlineCTEConsumer` 应检查此字段：`CTEMaterializeAlways` 时禁止内联，
`CTEMaterializeNever` 时强制内联。若当前 ORCA 忽略此字段，则用户的显式 hint 会被忽略，
属于已知缺陷。

### `cterefcount` 在规划时已是最终值

`cterefcount` 在 parse analysis 阶段（`parse_relation.c:2359`）按 RTE_CTE 引用次数累加，
ORCA 翻译 `query->cteList` 时可直接使用此值辅助决策（例如：引用次数为 1 且无 volatile
函数时，倾向内联）。PG18 原生的内联条件是 `CTEMaterializeDefault && cterefcount == 1`，
ORCA 的 xform 启发式规则应与之对齐。

---

## PG18 原生 CTE 执行模型

### 计划树结构

```
PlannedStmt {
    planTree:   CteScan { ctePlanId=1, cteParam=0, scanrelid=N }
    rtable:     [ ..., RTE_CTE { ctename="cte", ctelevelsup=0 }, ... ]
    subplans:   [ <CTE 子计划 Plan*> ]          ← subplans[ctePlanId-1]
    paramExecTypes: [ InvalidOid ]              ← param 0 的类型占位
}
```

### 关键字段

| 字段 | 含义 |
|------|------|
| `CteScan.ctePlanId` | 1-based，指向 `PlannedStmt.subplans[ctePlanId-1]` |
| `CteScan.cteParam`  | PARAM_EXEC 槽编号，executor 用来共享 `CteScanState*` 指针（leader-follower 机制） |
| `CteScan.scan.scanrelid` | rtable 中对应 `RTE_CTE` 的 1-based 索引 |

### Executor 执行流程

1. `ExecInitCteScan`：
   - 通过 `ctePlanId-1` 在 `es_subplanstates` 中找到 CTE 子计划的 `PlanState`
   - 检查 `es_param_exec_vals[cteParam]`：若为 NULL，当前节点为 **leader**，创建 tuplestore，
     将自身指针写入 param 槽；否则为 **follower**，从 leader 的 `cte_table` 分配独立读指针
2. `CteScanNext`：按需从 CTE 子计划拉取行并追加到 tuplestore；多个 CteScan 共享同一
   tuplestore，各持独立读指针

---

## ORCA DXL 计划结构

ORCA 为 `WITH cte AS (...) SELECT ... FROM cte` 生成：

```
CDXLPhysicalSequence
  ├── [0] projlist
  ├── [1] CDXLPhysicalCTEProducer (cte_id=0)
  │         ├── [0] projlist
  │         └── [1] <CTE 子计划，如 SeqScan/Agg 等>
  └── [2] <主计划，内含 CDXLPhysicalCTEConsumer (cte_id=0)>
```

多个 CTE 时，Sequence 有多个 CTEProducer 子节点，最后一个子节点是主计划。

---

## 翻译策略

### 核心思路

将 ORCA 的"Sequence 驱动 Producer"结构**拆解**为 PG18 的"initplan + CteScan"结构：

1. **CDXLPhysicalCTEProducer** → 子计划加入 `PlannedStmt.subplans`，分配 PARAM_EXEC 槽
2. **CDXLPhysicalSequence** → 直接返回**最后一个子节点**（主计划）的翻译结果，Sequence 节点消失
3. **CDXLPhysicalCTEConsumer** → 生成 `CteScan` 节点 + `RTE_CTE` 条目

### 翻译顺序保证

`TranslateDXLSequence` 按 `ul = 1 .. arity-1` 顺序翻译子节点：
- `ul=1`：CTEProducer → 调用 `TranslateDXLCTEProducerToPlan`，此时分配 `param_id`，
  加入 `m_subplan_entries_list`，记录 `cte_id → (plan_id, param_id)` 映射
- `ul=arity-1`：主计划 → 翻译时遇到 CTEConsumer，查映射取 `param_id` 和 `plan_id`，
  生成 `CteScan`

Producer 必然先于 Consumer 翻译，映射在 Consumer 翻译时已存在。

---

## 数据结构变更

### `CContextDXLToPlStmt`

新增 CTE 映射，替换旧的 `SCTEConsumerInfo`：

```cpp
// cte_id → CTEPlanInfo，在 Producer 翻译时写入，Consumer 翻译时读取
struct SCTEPlanInfo {
    int  plan_id;   // 1-based，subplans 中的位置
    int  param_id;  // PARAM_EXEC 槽编号
    SCTEPlanInfo(int pid, int prmid) : plan_id(pid), param_id(prmid) {}
};

using HMUlCTEPlanInfo =
    CHashMap<ULONG, SCTEPlanInfo, gpos::HashValue<ULONG>, gpos::Equals<ULONG>,
             CleanupDelete<ULONG>, CleanupDelete<SCTEPlanInfo>>;

HMUlCTEPlanInfo *m_cte_plan_info;   // 替换 m_cte_consumer_info
```

新增两个方法（替换 `AddCTEConsumerInfo` / `GetCTEConsumerList`）：

```cpp
// Producer 翻译时调用：加入 subplans，分配 PARAM_EXEC 槽，记录映射
// 返回分配的 plan_id (1-based)
int RegisterCTEPlan(ULONG cte_id, Plan *cte_subplan);

// Consumer 翻译时调用：取回 plan_id 和 param_id
// 若未找到则 GPOS_ASSERT 失败（说明翻译顺序被破坏）
SCTEPlanInfo GetCTEPlanInfo(ULONG cte_id) const;
```

`RegisterCTEPlan` 实现逻辑：

```cpp
int CContextDXLToPlStmt::RegisterCTEPlan(ULONG cte_id, Plan *cte_subplan)
{
    // 加入 subplans 列表（1-based plan_id = 当前长度 + 1）
    AddSubplan(cte_subplan);
    int plan_id = list_length(m_subplan_entries_list);  // 已追加后的长度即 plan_id

    // 分配 PARAM_EXEC 槽（InvalidOid，与 PG 原生行为一致）
    int param_id = (int) GetNextParamId(InvalidOid);

    // 记录映射
    ULONG *key = GPOS_NEW(m_mp) ULONG(cte_id);
    SCTEPlanInfo *info = GPOS_NEW(m_mp) SCTEPlanInfo(plan_id, param_id);
    m_cte_plan_info->Insert(key, info);

    return plan_id;
}
```

### `CContextDXLToPlStmt.h`

- 删除 `SCTEConsumerInfo`、`HMUlCTEConsumerInfo`、`m_cte_consumer_info`
- 删除 `AddCTEConsumerInfo`、`GetCTEConsumerList`
- 新增 `SCTEPlanInfo`、`HMUlCTEPlanInfo`、`m_cte_plan_info`
- 新增 `RegisterCTEPlan`、`GetCTEPlanInfo`

---

## 翻译函数实现

### `TranslateDXLSequence` — 拆解 Sequence

```
旧行为：生成 Sequence { subplans = [child1, child2, ...] }
新行为：
  - 遍历 child[1 .. arity-2]（CTEProducer 们），各自翻译为子计划加入 subplans
  - 翻译 child[arity-1]（主计划）
  - 直接返回主计划的 Plan*，Sequence 节点不创建
```

伪代码：

```cpp
Plan *
CTranslatorDXLToPlStmt::TranslateDXLSequence(
    const CDXLNode *sequence_dxlnode, CDXLTranslateContext *output_context,
    CDXLTranslationContextArray *ctxt_translation_prev_siblings)
{
    ULONG arity = sequence_dxlnode->Arity();
    // child[0] = projlist, child[1..arity-2] = CTEProducers, child[arity-1] = main plan

    CDXLTranslateContext child_context(m_mp, false,
                                       output_context->GetColIdToParamIdMap());

    // 翻译所有 CTEProducer 子节点（加入 subplans，不进主树）
    for (ULONG ul = 1; ul < arity - 1; ul++)
    {
        CDXLNode *child_dxlnode = (*sequence_dxlnode)[ul];
        // 必须是 CTEProducer，否则 GPOS_ASSERT
        GPOS_ASSERT(EdxlopPhysicalCTEProducer ==
                    child_dxlnode->GetOperator()->GetDXLOperator());
        TranslateDXLOperatorToPlan(child_dxlnode, &child_context,
                                   ctxt_translation_prev_siblings);
        // 注意：CTEProducer 翻译函数负责调用 RegisterCTEPlan，返回值在此丢弃
    }

    // 翻译最后一个子节点（主计划）并直接返回
    CDXLNode *main_dxlnode = (*sequence_dxlnode)[arity - 1];
    Plan *main_plan = TranslateDXLOperatorToPlan(main_dxlnode, &child_context,
                                                  ctxt_translation_prev_siblings);

    // 将主计划的输出列映射传播到 output_context
    // （原 Sequence 的 projlist 由主计划的 targetlist 覆盖）
    CDXLNode *proj_list = (*sequence_dxlnode)[0];
    CDXLTranslationContextArray *child_contexts =
        GPOS_NEW(m_mp) CDXLTranslationContextArray(m_mp);
    child_contexts->Append(&child_context);
    main_plan->targetlist = TranslateDXLProjList(proj_list, nullptr,
                                                  child_contexts, output_context);
    SetParamIds(main_plan);
    child_contexts->Release();

    return main_plan;
}
```

**注意**：若 Sequence 只有一个子节点（无 CTEProducer，只有主计划），或者 arity == 2，
则 `ul=1..arity-2` 循环体不执行，直接翻译 `child[1]` 作为主计划返回。
Sequence 中只有 CTEProducer 被特殊处理；其他类型的非末尾子节点（如分区场景的 DynamicSeqScan
初始化）若将来出现，需扩展此函数。

---

### `TranslateDXLCTEProducerToPlan` — 生成子计划

```cpp
Plan *
CTranslatorDXLToPlStmt::TranslateDXLCTEProducerToSharedScan(
    const CDXLNode *cte_producer_dxlnode, CDXLTranslateContext *output_context,
    CDXLTranslationContextArray *ctxt_translation_prev_siblings)
{
    CDXLPhysicalCTEProducer *cte_prod_dxlop =
        CDXLPhysicalCTEProducer::Cast(cte_producer_dxlnode->GetOperator());
    ULONG cte_id = cte_prod_dxlop->Id();

    // 翻译 CTE 子计划（child[1]）
    CDXLNode *proj_list_dxlnode = (*cte_producer_dxlnode)[0];
    CDXLNode *child_dxlnode     = (*cte_producer_dxlnode)[1];

    CDXLTranslateContext child_context(m_mp, false,
                                       output_context->GetColIdToParamIdMap());
    Plan *child_plan = TranslateDXLOperatorToPlan(child_dxlnode, &child_context,
                                                   ctxt_translation_prev_siblings);

    CDXLTranslationContextArray *child_contexts =
        GPOS_NEW(m_mp) CDXLTranslationContextArray(m_mp);
    child_contexts->Append(&child_context);
    child_plan->targetlist = TranslateDXLProjList(proj_list_dxlnode, nullptr,
                                                   child_contexts, output_context);
    TranslatePlanCosts(cte_producer_dxlnode, child_plan);
    SetParamIds(child_plan);
    child_contexts->Release();

    // 注册到 subplans，记录 cte_id → (plan_id, param_id)
    m_dxl_to_plstmt_context->RegisterCTEPlan(cte_id, child_plan);

    // 翻译函数约定返回 Plan*，但 Sequence 翻译器不会使用此返回值
    // 返回 child_plan 仅为满足接口签名
    return child_plan;
}
```

---

### `TranslateDXLCTEConsumerToSharedScan` — 生成 CteScan

```cpp
Plan *
CTranslatorDXLToPlStmt::TranslateDXLCTEConsumerToSharedScan(
    const CDXLNode *cte_consumer_dxlnode, CDXLTranslateContext *output_context,
    CDXLTranslationContextArray * /*ctxt_translation_prev_siblings*/)
{
    CDXLPhysicalCTEConsumer *cte_consumer_dxlop =
        CDXLPhysicalCTEConsumer::Cast(cte_consumer_dxlnode->GetOperator());
    ULONG cte_id = cte_consumer_dxlop->Id();

    // 查找 Producer 已注册的信息
    CContextDXLToPlStmt::SCTEPlanInfo cte_info =
        m_dxl_to_plstmt_context->GetCTEPlanInfo(cte_id);

    // 在 rtable 中添加 RTE_CTE 条目
    RangeTblEntry *rte = makeNode(RangeTblEntry);
    rte->rtekind    = RTE_CTE;
    rte->ctename    = pstrdup("<orca_cte>");  // 仅用于 EXPLAIN，可填 CTE 名称
    rte->ctelevelsup = 0;
    rte->self_reference = false;
    rte->eref       = makeAlias("<orca_cte>", NIL);
    rte->lateral    = false;
    rte->inh        = false;
    rte->inFromCl   = true;
    m_dxl_to_plstmt_context->AddRTE(rte);
    Index scan_relid = list_length(m_dxl_to_plstmt_context->GetRTableEntriesList());

    // 构建 CteScan 节点
    CteScan *cte_scan = makeNode(CteScan);
    Plan *plan = &cte_scan->scan.plan;
    plan->plan_node_id = m_dxl_to_plstmt_context->GetNextPlanId();
    cte_scan->scan.scanrelid = scan_relid;
    cte_scan->ctePlanId      = cte_info.plan_id;   // 1-based subplan index
    cte_scan->cteParam       = cte_info.param_id;  // PARAM_EXEC slot

    TranslatePlanCosts(cte_consumer_dxlnode, plan);

    // 翻译投影列
    CDXLNode *proj_list_dxlnode = (*cte_consumer_dxlnode)[0];
    const ULONG num_cols = proj_list_dxlnode->Arity();
    plan->targetlist = NIL;

    for (ULONG ul = 0; ul < num_cols; ul++)
    {
        CDXLNode *proj_elem_dxlnode = (*proj_list_dxlnode)[ul];
        CDXLScalarProjElem *sc_proj_elem_dxlop =
            CDXLScalarProjElem::Cast(proj_elem_dxlnode->GetOperator());
        ULONG colid = sc_proj_elem_dxlop->Id();

        CDXLNode *sc_ident_dxlnode = (*proj_elem_dxlnode)[0];
        CDXLScalarIdent *sc_ident_dxlop =
            CDXLScalarIdent::Cast(sc_ident_dxlnode->GetOperator());
        OID type_oid  = CMDIdGPDB::CastMdid(sc_ident_dxlop->MdidType())->Oid();
        INT typmod    = sc_ident_dxlop->TypeModifier();
        OID collation = gpdb::TypeCollation(type_oid);

        // CteScan 从 scanslot 读取，使用 INDEX_VAR（scanrelid 为 CTE RTE）
        // 但 PG18 实际以 OUTER_VAR 引用子计划输出，对于 CteScan 而言
        // 使用 INDEX_VAR，attno 对应子计划输出列顺序（1-based）
        Var *var = gpdb::MakeVar(INDEX_VAR, (AttrNumber)(ul + 1),
                                  type_oid, typmod, collation, 0);
        char *resname = CTranslatorUtils::CreateMultiByteCharStringFromWCString(
            sc_proj_elem_dxlop->GetColumnName()->GetBuffer());
        TargetEntry *te = gpdb::MakeTargetEntry((Expr *) var,
                                                (AttrNumber)(ul + 1),
                                                resname, false);
        plan->targetlist = gpdb::LAppend(plan->targetlist, te);

        // 向 output_context 注册此列的映射（colid → TargetEntry）
        output_context->InsertMapping(colid, te);
    }

    plan->qual = NIL;
    SetParamIds(plan);

    return (Plan *) cte_scan;
}
```

**关于 INDEX_VAR vs OUTER_VAR**：PG18 `ExecInitCteScan` 调用
`ExecAssignScanProjectionInfoWithVarno(&css->ss, INDEX_VAR)`，scan tuple slot 按
`INDEX_VAR` 投影。targetlist 中的 Var 需使用 `INDEX_VAR` 且 `varattno` 对应子计划输出
列的位置（1-based），与 `ExecScan` 框架一致。

---

## `PlannedStmt` 组装

`TranslateDXLToPlan` 中已有：

```cpp
planned_stmt->subplans = m_dxl_to_plstmt_context->GetSubplanEntriesList();
planned_stmt->paramExecTypes = m_dxl_to_plstmt_context->GetParamTypes();
```

这两行无需修改——`RegisterCTEPlan` 调用 `AddSubplan` 和 `GetNextParamId`，自动维护这两个列表。

`planned_stmt->initPlan` 保持 NIL。PG18 的 `CteScan` 使用 `ctePlanId` 直接索引
`es_subplanstates`，不需要 initPlan 机制驱动（executor 在 `ExecInitNode` 阶段统一初始化
所有 subplans）。

---

## 多 CTE / 嵌套 CTE

### 多个独立 CTE

```sql
WITH a AS (...), b AS (...)
SELECT ... FROM a JOIN b ON ...
```

ORCA 生成：

```
Sequence
  ├── CTEProducer(cte_id=0)  → subplans[0], param_id=0
  ├── CTEProducer(cte_id=1)  → subplans[1], param_id=1
  └── HashJoin
        ├── CTEConsumer(0)   → CteScan(ctePlanId=1, cteParam=0)
        └── CTEConsumer(1)   → CteScan(ctePlanId=2, cteParam=1)
```

`TranslateDXLSequence` 遍历 `ul=1..arity-2`，依次翻译两个 CTEProducer，各自调用
`RegisterCTEPlan`，分配独立的 `plan_id` 和 `param_id`。

### 嵌套 CTE

```sql
WITH a AS (...), b AS (SELECT ... FROM a ...)
SELECT ... FROM b
```

ORCA 为嵌套 CTE 生成嵌套 Sequence：

```
Sequence(outer)
  ├── CTEProducer(cte_id=0)   ← a 的定义
  └── Sequence(inner)
        ├── CTEProducer(cte_id=1) ← b 的定义，内含 CTEConsumer(0)
        └── CTEConsumer(1)        ← 主查询引用 b
```

翻译时，outer Sequence 翻译 `CTEProducer(0)` 后，递归翻译 inner Sequence：
- inner Sequence 翻译 `CTEProducer(1)` 时遇到 `CTEConsumer(0)`，此时映射已存在，生成
  `CteScan(ctePlanId=1, cteParam=0)`，整个 CTEProducer(1) 的子计划含一个 CteScan
- inner Sequence 最后返回主计划（含 `CTEConsumer(1)` 翻译出的 `CteScan(ctePlanId=2, cteParam=1)`）

无需特殊处理嵌套情况，递归翻译天然正确。

### 同一 CTE 被多次引用

```sql
WITH cte AS (...) SELECT * FROM cte c1, cte c2
```

ORCA 生成一个 CTEProducer + 两个 CTEConsumer。`RegisterCTEPlan` 只调用一次（Producer），
两个 Consumer 调用 `GetCTEPlanInfo` 得到相同的 `plan_id` 和 `param_id`，生成两个
`CteScan` 节点。

PG18 executor 的 leader-follower 机制处理多个 CteScan：第一个初始化的成为 leader 并创建
tuplestore，第二个成为 follower 并分配独立读指针，共享同一 tuplestore，互不干扰。

---

## 递归 CTE

递归 CTE（`WITH RECURSIVE`）是独立问题，不在本次范围内。ORCA 不支持递归 CTE 优化（直接
回退到 standard_planner），此设计不改变这一行为。

---

## 需修改/新增的文件

| 文件 | 变更 |
|------|------|
| `include/gpopt/translate/CContextDXLToPlStmt.h` | 删除 `SCTEConsumerInfo`/`HMUlCTEConsumerInfo`/`m_cte_consumer_info`/`AddCTEConsumerInfo`/`GetCTEConsumerList`；新增 `SCTEPlanInfo`/`HMUlCTEPlanInfo`/`m_cte_plan_info`/`RegisterCTEPlan`/`GetCTEPlanInfo` |
| `gpopt/translate/CContextDXLToPlStmt.cpp` | 实现 `RegisterCTEPlan`、`GetCTEPlanInfo`；删除旧 CTE 方法 |
| `gpopt/translate/CTranslatorDXLToPlStmt.cpp` | 重写 `TranslateDXLSequence`（折叠为主计划）、`TranslateDXLCTEProducerToSharedScan`（生成子计划）、`TranslateDXLCTEConsumerToSharedScan`（生成 CteScan） |

不需要新增文件，不需要 CustomScan 注册，不需要修改 `pg_orca.cpp`。

---

## 边界情况与约束

| 场景 | 处理方式 |
|------|----------|
| Sequence 只有主计划（arity=2） | `ul=1..arity-2` 范围为空，直接翻译并返回 child[1] |
| Consumer 翻译时 cte_id 未找到 | `GetCTEPlanInfo` 中 `GPOS_ASSERT` 失败，说明 ORCA 生成了非预期的结构 |
| Sequence 的非末尾子节点不是 CTEProducer | `GPOS_ASSERT` 失败，目前 ORCA 不会生成此类结构 |
| EXPLAIN（不执行） | CteScan 是普通计划节点，EXPLAIN 按标准路径工作，无副作用 |
| Consumer Rescan | 由 `nodeCtescan.c` 原生处理：`tuplestore_select_read_pointer` + `tuplestore_rescan` |
| nParamExec | `GetNextParamId` 维护 `m_param_types_list`，`planned_stmt->paramExecTypes` 自动正确 |
| RTE_CTE 的 ctename | 填写实际 CTE 名称需要从 DXL metadata 获取；V1 可填占位符，不影响执行正确性，仅影响 EXPLAIN 显示 |
| 递归 CTE | 继续回退到 standard_planner，无变化 |

---

