Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions CREATE_FORCE_VIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
### **CREATE FORCE VIEW 功能详细设计文档**

#### 1. 引言

本设计的核心目标是在 OpenTenBase 中实现 CREATE FORCE VIEW 功能,以对齐 Oracle 数据库在该领域的行为。标准 CREATE VIEW 命令在解析阶段对所有依赖对象(表、函数等)进行严格的存在性校验,这在某些开发和部署场景下(如解耦的模块化部署、先部署应用逻辑后部署底层表)会造成不便。

CREATE FORCE VIEW 功能旨在解决此问题,允许在视图的依赖对象尚不存在时,也能成功创建视图的定义。

为实现此目标,我们确立了**“运行时延迟校验” (Run-Time Deferred Validation)** 的核心设计哲学。

此哲学将标准的**“编译时强校验” (Compile-Time Strong Validation)** 策略,转变为将存在性、权限和语义的最终校验,从 CREATE 命令的解析阶段,完全推迟到用户第一次 SELECT 该视图的**运行时(Run-Time)**。

#### 2. 核心实现机制

为在 OpenTenBase 复杂的内核中实现“延迟校验”,我们采取了一系列精确、协同且风险可控的修改。

- **文件:** src/backend/parser/gram.y, src/include/nodes/parsenodes.h
- **实现:**
1. 在 SQL 语法文件 gram.y 中,为 CreateViewStmt 规则增加了可选的 FORCE 关键字。
2. 在 ViewStmt 解析树节点结构体中,增加了一个 bool force 成员,用于在内核中传递 FORCE 模式的语义。

我们实现延迟校验的核心技巧,是在解析阶段创建一个**“占位符范围表条目 (Placeholder Range Table Entry - RTE)”**。

- **文件:** src/backend/parser/analyze.c, src/backend/parser/parse_relation.c
- **实现:**
1. **会话级标志:** 引入一个安全的、有明确生命周期管理的全局标志 creating_force_view,用于在解析期间广播“宽容模式”。
2. **修改 scanRTEForColumn**: 这是最关键的修改。在 creating_force_view 上下文中,当此函数遇到一个不存在的关系时,其行为被修改为:
- **不报错**: 不再抛出 relation does not exist 错误。
- **创建占位符**: 为该关系创建一个 relid 为 InvalidOid (0) 的 RTE。
- **记录元数据**: 动态地将用户请求的列名记录到 rte->eref->colnames 列表中,并向并行的 rte->coltypes, rte->coltypmods, rte->colcollations 列表中,分别追加 UNKNOWNOID, -1, 和 InvalidOid。

一个 relid=0 的“占位符 RTE”无法被内核的下游模块正确处理。我们通过 lldb 调试,定位了所有因此而失败的关键函数,并为它们增加了相同的、精确的“豁免”补丁。

- **核心补丁逻辑:** if (!OidIsValid(rte->relid)) continue; 或类似逻辑。
- **被修复的模块与文件:**
- **查询重写器 (src/backend/rewrite/rewriteHandler.c)**:
- AcquireRewriteLocks: 避免了对 relid=0 的关系进行加锁。
- fireRIRrules: 避免了在应用视图规则时打开 relid=0 的关系。
- **分布式计划器 (PGXC)**:
- pgxc_FQS_datanodes_for_rtr (src/backend/optimizer/util/pgxcship.c): 避免了在分析查询可下推性时,查询 relid=0 关系的元数据。
- contains_temp_tables (src/backend/pgxc/plan/planner.c): 避免了在检查临时表时,打开 relid=0 的关系。
- **查询反解析器 (src/backend/utils/adt/ruleutils.c)**:
- set_relation_column_names: 避免了在获取列别名时,打开 relid=0 的关系。
- get_from_clause_item: 避免了在将查询树转回 SQL 文本时,尝试为 relid=0 的关系生成其 catalog 名称。

#### 3. 关键设计决策与权衡

在开发和调试过程中,我们遇到了两个深层次的设计问题。在对 Oracle 实现进行研究并深入评估了多种方案的风险和复杂度后,我们做出了以下关键的工程决策。

- **问题描述:** 我们发现,解析器在处理未知列时,会将其类型**默认推断为 TEXT**,并且这个 TEXT 类型会被**永久地**写入 pg_attribute 系统目录。这导致后续 CREATE OR REPLACE VIEW 在面对底层表真实的、非 TEXT 类型(如 INTEGER)时,会因类型不匹配而失败。
- **方案权衡:**
- **理想方案 (高风险):** 效仿 Oracle,引入通用的对象状态模型(VALID/INVALID)和自动重新编译机制。此方案需要对 pg_class, pg_attribute 等核心目录进行修改,并重构查询重写器的核心逻辑,开发成本和系统性风险极高。
- **最终方案 (低风险):** **接受**此行为,并将其作为功能的一个明确约定。我们不修改 CREATE OR REPLACE VIEW 的核心类型保护机制,而是将类型兼容的责任交给用户。
- **最终理由:** 保证功能的稳定性和可预测性,优于追求一个可能引入巨大风险的“完美”方案。这是一个负责任的工程决策。
- **问题描述:** 我们通过 pg_depend 诊断发现,CREATE FORCE VIEW 命令本身**不会**创建依赖记录。这导致在视图被“最终化”之前,底层表可以被 DROP,造成数据不一致的风险。
- **方案权衡:**
- **理想方案 (高风险):** 在查询重写器中,当第一次成功访问视图时,动态地、自动地“回填”依赖记录。此方案需要在性能敏感的核心模块中增加修改系统目录的操作,面临复杂的事务和并发挑战。
- **最终方案 (零风险):** **依赖关系的建立,必须由用户通过一次后续的 CREATE OR REPLACE VIEW 命令来显式地“最终化”。**
- **最终理由:** 此方案完美地重用了 PostgreSQL 内核现有的、经过千锤百炼的 REPLACE 机制来管理依赖。它没有向内核引入任何新的、高风险的修改,并为用户提供了一个清晰、明确、且 100% 可靠的“视图激活”工作流程。

#### 4. 边界情况处理

- **文件:** src/backend/parser/parse_relation.c (expandRelAttrs)
- **实现:** 在 CREATE FORCE VIEW 命令的解析阶段,增加了对 SELECT * 的检查。如果 * 所引用的关系是一个不存在的“占位符 RTE”,命令将立即失败,并提示用户必须明确列出所有列名。
- **理由:** 当底层表的结构未知时,* 的含义是不明确的。在创建时就拒绝这种模棱两可的定义,是保证功能健壮性的必要措施。

#### 5. 总结

本设计成功地在 OpenTenBase 中实现了一个功能完备、行为健壮的 CREATE FORCE VIEW 功能。最终的实现方案,是在深入理解内核、充分评估风险后,在功能、成本和稳定性之间取得平衡的务实成果。它通过一个巧妙的“占位符 RTE”机制和一系列精准的“豁免”补丁,成功地实现了“运行时延迟校验”的核心哲学,并通过明确的设计边界和用户约定,保证了与系统其他部分的和谐共存。
188 changes: 188 additions & 0 deletions CREATE_FORCE_VIEW_TEST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# `CREATE FORCE VIEW` 功能测试计划与数据

## 1. 测试概述

本文档提供了用于完整验证 OpenTenBase `CREATE FORCE VIEW` 功能的最终验收测试计划。该计划包含了所有必要的测试用例、指令、测试数据以及精确的、与在最终代码版本下运行完全一致的预期输出。

### 1.1. 测试目的

本测试计划旨在全面验证 `CREATE FORCE VIEW` 功能的正确性与健壮性,具体包括:

* **核心功能:** 验证 `CREATE FORCE VIEW` 可以在依赖对象不存在时创建视图,并在依赖满足后自动变为可用。

* **视图转换:** 验证由 `CREATE FORCE VIEW` 创建的视图,可以被 `CREATE OR REPLACE VIEW` 正确地转换为一个标准的、不再具有 `FORCE` 属性的正常视图。

* **依赖管理:** 验证 `CREATE FORCE VIEW` 的依赖关系生命周期,确保其在被 `CREATE OR REPLACE VIEW` 命令“最终化”后,能够被 `DROP` (RESTRICT/CASCADE) 等命令正确地管理。

* **边界情况:** 验证 `CREATE FORCE VIEW` 对 `SELECT *` 等不明确定义的健壮性处理,以及在 `REPLACE` 过程中对原生列定义(类型、名称)兼容性规则的遵守。

### 1.2. 测试环境

* **数据库:** 已编译并安装了包含 `CREATE FORCE VIEW` 所有最终补丁的 OpenTenBase 版本。
* **集群状态:** 一个干净的、已启动的 OpenTenBase 集群。
* **工具:** `psql` 命令行客户端。
* **运行平台:** 所有编译、调试和测试工作均在一个 **Docker 容器**中完成。
* **执行用户:** 所有测试指令均由一个专门为此项目创建的、名为 `opentenbase` 的**非 root 普通用户**执行,以符合数据库的安全运行规范。

---

## 2. 独立测试用例 (Test Cases)

以下所有测试用例均被设计为完全独立的、自包含的。每一个测试用例都以清理环境的 `DROP` 语句开始,可以在任何时候独立运行,并预期得到与下面完全一致的输出。

### Part 1: 核心功能与生命周期

#### Test Case 1.1: 核心功能 - 创建、运行时失败、激活

* **目的:** (覆盖目标 1.1) 验证 `CREATE FORCE VIEW` 的基本流程:成功创建,运行时因依赖缺失而失败,依赖满足后自动激活并成功查询。
* **测试指令与数据:**
```sql
DROP VIEW IF EXISTS core_view;
DROP TABLE IF EXISTS base_table_1;
CREATE FORCE VIEW core_view AS SELECT col1 FROM base_table_1;
SELECT * FROM core_view;
CREATE TABLE base_table_1 (col1 TEXT, col2 INT) DISTRIBUTE BY REPLICATION;
INSERT INTO base_table_1 VALUES ('Core Functionality: Success!', 100);
SELECT * FROM core_view;
```
* **完整的预期输出:**
```
DROP VIEW
DROP TABLE
CREATE VIEW
ERROR: node:dn1, backend_pid:..., nodename:dn1,backend_pid:...,message:relation "base_table_1" does not exist
CREATE TABLE
INSERT 0 1
col1
------------------------------
Core Functionality: Success!
(1 row)
```

---

### Part 2: 与 `CREATE OR REPLACE VIEW` 的交互

#### Test Case 2.1: 替换视图 - 类型不匹配 (预期失败)

* **目的:** (覆盖目标 1.2 & 1.3 的限制) 验证 `CREATE OR REPLACE VIEW` 在列类型不匹配时,会如原生 PostgreSQL 一样报错,即使使用了 `FORCE` 关键字。
* **测试指令与数据:**
```sql
DROP VIEW IF EXISTS replace_type_view;
DROP TABLE IF EXISTS replace_type_table;
CREATE FORCE VIEW replace_type_view AS SELECT user_id FROM replace_type_table;
CREATE TABLE replace_type_table (user_id INT) DISTRIBUTE BY REPLICATION;
CREATE OR REPLACE FORCE VIEW replace_type_view AS SELECT user_id FROM replace_type_table;
```
* **完整的预期输出:**
```
DROP VIEW
DROP TABLE
CREATE VIEW
CREATE TABLE
ERROR: cannot change data type of view column "user_id" from text to integer
```

#### Test Case 2.2: 替换视图 - 名称不匹配 (预期失败)

* **目的:** (覆盖目标 1.2 & 1.3 的限制) 验证 `CREATE OR REPLACE VIEW` 在列名不匹配时,会如原生 PostgreSQL 一样报错,即使使用了 `FORCE` 关键字。
* **测试指令与数据:**
```sql
DROP VIEW IF EXISTS replace_name_view;
DROP TABLE IF EXISTS replace_name_table;
CREATE FORCE VIEW replace_name_view AS SELECT user_id FROM replace_name_table;
CREATE TABLE replace_name_table (user_id_new INT) DISTRIBUTE BY REPLICATION;
CREATE OR REPLACE FORCE VIEW replace_name_view AS SELECT user_id_new FROM replace_name_table;
```
* **完整的预期输出:**
```
DROP VIEW
DROP TABLE
CREATE VIEW
CREATE TABLE
ERROR: cannot change name of view column "user_id" to "user_id_new"
```

#### Test Case 2.3: 正确的 `REPLACE` 工作流程 (转换与 `FORCE` 属性移除)

* **目的:** (覆盖目标 1.2 & 1.3) 验证 `FORCE VIEW` 转换为 `NORMAL VIEW` 的正确工作流程,并验证其 `FORCE` 属性已被移除。
* **测试指令与数据:**
```sql
DROP VIEW IF EXISTS correct_replace_view;
DROP TABLE IF EXISTS base_table_correct, non_existent_table;
CREATE FORCE VIEW correct_replace_view AS SELECT user_id FROM base_table_correct;
CREATE TABLE base_table_correct (user_id TEXT) DISTRIBUTE BY REPLICATION;
INSERT INTO base_table_correct VALUES ('user_abc');
CREATE OR REPLACE VIEW correct_replace_view AS SELECT user_id FROM base_table_correct;
SELECT * FROM correct_replace_view;
DROP TABLE base_table_correct CASCADE;
CREATE OR REPLACE VIEW correct_replace_view AS SELECT user_id FROM non_existent_table;
```
* **完整的预期输出:**
```
DROP VIEW
DROP TABLE
CREATE VIEW
CREATE TABLE
INSERT 0 1
CREATE VIEW
user_id
----------
user_abc
(1 row)
NOTICE: drop cascades to view correct_replace_view
DROP TABLE
ERROR: relation "non_existent_table" does not exist
```

---

### Part 3: 依赖管理与边界情况

#### Test Case 3.1: 依赖关系与 `DROP` (最终化工作流)

* **目的:** (覆盖目标 1.4) 验证视图依赖关系的完整生命周期:只有在被 `REPLACE` 命令最终化之后,依赖关系才被正式建立。
* **测试指令与数据:**
```sql
DROP VIEW IF EXISTS dependency_view;
DROP TABLE IF EXISTS base_table_depend;
CREATE FORCE VIEW dependency_view AS SELECT val FROM base_table_depend;
CREATE TABLE base_table_depend (val TEXT) DISTRIBUTE BY REPLICATION;
DROP TABLE base_table_depend;
CREATE TABLE base_table_depend (val TEXT) DISTRIBUTE BY REPLICATION;
CREATE OR REPLACE VIEW dependency_view AS SELECT val FROM base_table_depend;
DROP TABLE base_table_depend;
DROP TABLE base_table_depend CASCADE;
SELECT * FROM dependency_view;
```
* **完整的预期输出:**
```
DROP VIEW
DROP TABLE
CREATE VIEW
CREATE TABLE
DROP TABLE
CREATE TABLE
CREATE VIEW
ERROR: cannot drop table base_table_depend because other objects depend on it
DETAIL: view dependency_view depends on table base_table_depend
HINT: Use DROP ... CASCADE to drop the dependent objects too.
NOTICE: drop cascades to view dependency_view
DROP TABLE
ERROR: relation "dependency_view" does not exist
```

#### Test Case 3.2: `SELECT *` 边界情况

* **目的:** 验证为 `SELECT *` 添加的健壮性检查,确保在创建时就拒绝此类不明确的 `FORCE VIEW`。
* **测试指令与数据:**
```sql
DROP VIEW IF EXISTS star_test_view;
CREATE FORCE VIEW star_test_view AS SELECT * FROM non_existent_star_table;
```
* **完整的预期输出:**
```
DROP VIEW
ERROR: CREATE FORCE VIEW does not support "*" for relations that do not exist
HINT: Please explicitly list the column names in the SELECT statement.
```