From 1273bed11635f60ccb5b5f974bfd530a0af130ee Mon Sep 17 00:00:00 2001 From: Steven Niu Date: Sat, 9 May 2026 15:40:57 +0800 Subject: [PATCH] read only view --- CN/modules/ROOT/nav.adoc | 2 + .../read_only_view.adoc | 235 ++++++++++++++++++ .../compat_read_only_view.adoc | 168 +++++++++++++ EN/modules/ROOT/nav.adoc | 2 + .../read_only_view.adoc | 235 ++++++++++++++++++ .../compat_read_only_view.adoc | 168 +++++++++++++ 6 files changed, 810 insertions(+) create mode 100644 CN/modules/ROOT/pages/master/compatibility_features_design/read_only_view.adoc create mode 100644 CN/modules/ROOT/pages/master/oracle_compatibility/compat_read_only_view.adoc create mode 100644 EN/modules/ROOT/pages/master/compatibility_features_design/read_only_view.adoc create mode 100644 EN/modules/ROOT/pages/master/oracle_compatibility/compat_read_only_view.adoc diff --git a/CN/modules/ROOT/nav.adoc b/CN/modules/ROOT/nav.adoc index 50611c5..fb9c4a4 100644 --- a/CN/modules/ROOT/nav.adoc +++ b/CN/modules/ROOT/nav.adoc @@ -26,6 +26,7 @@ ** xref:master/oracle_compatibility/compat_sys_guid.adoc[17、sys_guid 函数] ** xref:master/oracle_compatibility/compat_empty_string_to_null.adoc[18、空字符串转null] ** xref:master/oracle_compatibility/compat_call_into.adoc[19、CALL INTO] +** xref:master/oracle_compatibility/compat_read_only_view.adoc[20、视图只读] * 容器化与云服务 ** 容器化指南 *** xref:master/containerization/k8s_deployment.adoc[K8S部署] @@ -90,6 +91,7 @@ **** xref:master/compatibility_features_design/sys_guid_function.adoc[sys_guid 函数] **** xref:master/compatibility_features_design/empty_string_to_null.adoc[空字符串转null] **** xref:master/compatibility_features_design/call_into.adoc[CALL INTO] +**** xref:master/compatibility_features_design/read_only_view.adoc[视图只读] *** 内置函数 **** xref:master/oracle_builtin_functions/sys_context.adoc[sys_context] **** xref:master/oracle_builtin_functions/userenv.adoc[userenv] diff --git a/CN/modules/ROOT/pages/master/compatibility_features_design/read_only_view.adoc b/CN/modules/ROOT/pages/master/compatibility_features_design/read_only_view.adoc new file mode 100644 index 0000000..5a4ebac --- /dev/null +++ b/CN/modules/ROOT/pages/master/compatibility_features_design/read_only_view.adoc @@ -0,0 +1,235 @@ +:sectnums: +: sectnumlevels: 5 + +:imagesdir: ./_images + += 视图只读属性 + +== 目的 + +Oracle 数据库支持在创建视图时使用 `WITH READ ONLY` 子句,将视图设置为只读状态,防止对视图执行任何数据修改操作(INSERT、UPDATE、DELETE、MERGE)。为了兼容 Oracle 的这一特性,IvorySQL 实现了视图只读属性功能。当视图被标记为只读后,任何试图修改视图数据的 DML 操作都会被拒绝,从而保证 Oracle 应用程序在 IvorySQL 上的行为一致性。 + +== 实现说明 + +=== 语法与解析 + +==== 语法规则扩展 + +在 `ora_gram.y` 文件中添加了 `READ_ONLY_OPTION` 枚举值,并扩展了 `opt_check_option` 语法规则: + +[source,yacc] +---- +opt_check_option: + WITH CHECK OPTION { $$ = CASCADED_CHECK_OPTION; } + | WITH CASCADED CHECK OPTION { $$ = CASCADED_CHECK_OPTION; } + | WITH LOCAL CHECK OPTION { $$ = LOCAL_CHECK_OPTION; } + | WITH READ ONLY { $$ = READ_ONLY_OPTION; } /* 新增 */ + | /* EMPTY */ { $$ = NO_CHECK_OPTION; } + ; +---- + +==== ViewStmt 结构体扩展 + +在 `parsenodes.h` 文件中,`ViewCheckOption` 枚举新增 `READ_ONLY_OPTION` 选项,`ViewStmt` 结构体新增 `readOnly` 字段: + +[source,c] +---- +typedef enum ViewCheckOption { + NO_CHECK_OPTION, + LOCAL_CHECK_OPTION, + CASCADED_CHECK_OPTION, + READ_ONLY_OPTION, /* WITH READ ONLY (Oracle compat) */ +} ViewCheckOption; + +typedef struct ViewStmt { + // ... 其他字段 + bool readOnly; /* WITH READ ONLY (Oracle compat) */ +} ViewStmt; +---- + +解析时设置 `readOnly` 标志: + +[source,c] +---- +n->readOnly = ($10 == READ_ONLY_OPTION); +n->withCheckOption = n->readOnly ? NO_CHECK_OPTION : $10; +---- + +需要注意的是,`WITH READ ONLY` 与 `WITH CHECK OPTION` 是互斥的,不能同时使用。 + +=== 关系选项系统 + +==== read_only 关系选项 + +在 `reloptions.c` 文件中定义了 `read_only` 视图关系选项: + +[source,c] +---- +/* reloptions.c */ +{ + {"read_only", + "Prevents INSERT, UPDATE, and DELETE on this view (Oracle compatibility)", + RELOPT_KIND_VIEW, + AccessExclusiveLock}, + false +}, + +/* view_reloptions() 函数中 */ +{"read_only", RELOPT_TYPE_BOOL, + offsetof(ViewOptions, read_only)}, +---- + +==== ViewOptions 结构体扩展 + +在 `rel.h` 文件中,`ViewOptions` 结构体新增 `read_only` 字段: + +[source,c] +---- +typedef struct ViewOptions { + // ... 其他字段 + bool read_only; /* WITH READ ONLY (Oracle compat) */ +} ViewOptions; +---- + +==== RelationIsReadOnlyView 宏 + +通过 `RelationIsReadOnlyView` 宏可以快速判断视图是否为只读: + +[source,c] +---- +#define RelationIsReadOnlyView(relation) \ + (AssertMacro(relation->rd_rel->relkind == RELKIND_VIEW), \ + (relation)->rd_options && \ + ((ViewOptions *) (relation)->rd_options)->read_only) +---- + +=== 视图创建处理 + +在 `view.c` 文件的 `DefineView` 函数中处理 `WITH READ ONLY` 选项: + +[source,c] +---- +/* DefineView() 中处理 WITH READ ONLY */ +if (stmt->readOnly) +{ + /* 设置 read_only 关系选项 */ + options = transformViewOptions(RelationGetRelid(newRelationView), + stmt->options, true); + setRelOptions(newRelationView, options, true, AccessExclusiveLock); +} +---- + +同时在 `compile_force_view_internal` 函数中也支持 `WITH READ ONLY`,确保 FORCE VIEW 可以正常使用只读属性。 + +=== DML 执行拦截 + +==== 执行层拦截 + +在 `execMain.c` 文件的 `CheckValidResultRel()` 函数中,对只读视图的 DML 操作进行拦截: + +[source,c] +---- +/* execMain.c */ +if (RelationIsReadOnlyView(resultRel)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot modify view \"%s\"", + RelationGetRelationName(resultRel)), + errhint("The view is defined as read-only."))); +---- + +当执行 INSERT、UPDATE、DELETE 或 MERGE 语句时,如果目标视图被标记为只读,将抛出错误。 + +==== 重写层拦截 + +在 `rewriteHandler.c` 文件的 `rewriteTargetView()` 函数中也添加了相同的检查: + +[source,c] +---- +/* rewriteHandler.c */ +if (RelationIsReadOnlyView(view)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot modify view \"%s\"", + RelationGetRelationName(view)), + errhint("The view is defined as read-only."))); +---- + +这确保了在查询重写层面也能阻止对只读视图的修改操作。 + +=== pg_dump 支持 + +在 `pg_dump.c` 文件中添加了对 `WITH READ ONLY` 属性的导出和恢复支持: + +[source,c] +---- +/* pg_dump.h - TableInfo 结构体新增字段 */ +typedef struct _tableInfo { + // ... 其他字段 + bool readOnly; /* WITH READ ONLY (Oracle compat) */ + // ... 其他字段 +} _tableInfo; + +/* pg_dump.c - 提取 read_only 选项 */ +if (strcmp(options[i].defname, "read_only") == 0) + info->readOnly = defGetBoolean(options[i].def); +---- + +导出时,`dumpTableSchema()` 和 `dumpRule()` 函数会在适当位置输出 `WITH READ ONLY` 子句,确保只读属性在数据库迁移过程中得以保留。 + +== 使用示例 + +=== 创建只读视图 + +[source,sql] +---- +-- 创建带 WITH READ ONLY 的视图 +CREATE VIEW emp_view AS +SELECT * FROM employees +WITH READ ONLY; + +-- 在只读视图上执行 DML 将被拒绝 +INSERT INTO emp_view VALUES (1, 'John', 5000); +-- ERROR: cannot modify view "emp_view" +-- HINT: The view is defined as read-only. + +UPDATE emp_view SET salary = 6000 WHERE id = 1; +-- ERROR: cannot modify view "emp_view" +-- HINT: The view is defined as read-only. + +DELETE FROM emp_view WHERE id = 1; +-- ERROR: cannot modify view "emp_view" +-- HINT: The view is defined as read-only. +---- + +=== 创建或替换只读视图 + +[source,sql] +---- +-- 使用 OR REPLACE 保留只读属性 +CREATE OR REPLACE VIEW emp_view AS +SELECT id, name, department FROM employees +WITH READ ONLY; +---- + +=== FORCE VIEW 与只读属性结合 + +[source,sql] +---- +-- 创建 FORCE VIEW 并设置为只读 +CREATE OR REPLACE FORCE VIEW emp_force_view AS +SELECT * FROM employees +WITH READ ONLY; +---- + +=== 与 WITH CHECK OPTION 的互斥性 + +[source,sql] +---- +-- 错误: WITH READ ONLY 和 WITH CHECK OPTION 不能同时使用 +CREATE VIEW emp_view AS +SELECT * FROM employees +WITH CHECK OPTION +WITH READ ONLY; +-- ERROR: READ ONLY and CHECK OPTION are mutually exclusive +---- diff --git a/CN/modules/ROOT/pages/master/oracle_compatibility/compat_read_only_view.adoc b/CN/modules/ROOT/pages/master/oracle_compatibility/compat_read_only_view.adoc new file mode 100644 index 0000000..90602ea --- /dev/null +++ b/CN/modules/ROOT/pages/master/oracle_compatibility/compat_read_only_view.adoc @@ -0,0 +1,168 @@ +:sectnums: +: sectnumlevels: 5 + +:imagesdir: ./_images + += Read Only View + +== 目的 + +本文档解释 IvorySQL 中 `WITH READ ONLY` 视图的功能,实现只读视图功能以保持与 Oracle 行为一致。 + +== 功能说明 + +- `WITH READ ONLY`:创建视图时指定此选项,可防止对视图进行 INSERT、UPDATE、DELETE 和 MERGE 操作。 +- 存储方式:`read_only=true` 存储在视图的 `reloptions` 中。 +- 互斥性:`WITH READ ONLY` 和 `WITH CHECK OPTION` 互斥,不能同时指定。 +- Force View 支持:可以与 `FORCE VIEW` 一起使用,只读属性会在视图成功编译后生效。 +- CREATE OR REPLACE 行为:如果使用 `CREATE OR REPLACE VIEW` 重新创建视图时未指定 `WITH READ ONLY`,则会清除只读属性。 + +== 测试用例 + +=== 创建只读视图 + +[source,sql] +---- +-- 创建基础表 +CREATE TABLE t_ro (a int, b text); +INSERT INTO t_ro VALUES (1, 'hello'), (2, 'world'); + +-- 创建只读视图,SELECT 成功 +CREATE VIEW ro_view AS SELECT * FROM t_ro WITH READ ONLY; +SELECT * FROM ro_view ORDER BY a; +-- 期望输出: +-- a | b +-- ---+------- +-- 1 | hello +-- 2 | world +---- + +=== 验证 DML 被阻止 + +[source,sql] +---- +-- INSERT 被阻止 +INSERT INTO ro_view VALUES (3, 'fail'); +-- 期望输出:ERROR: cannot modify view "ro_view" +-- HINT: The view is defined as read-only. + +-- UPDATE 被阻止 +UPDATE ro_view SET b = 'fail' WHERE a = 1; +-- 期望输出:ERROR: cannot modify view "ro_view" +-- HINT: The view is defined as read-only. + +-- DELETE 被阻止 +DELETE FROM ro_view WHERE a = 1; +-- 期望输出:ERROR: cannot modify view "ro_view" +-- HINT: The view is defined as read-only. +---- + +=== MERGE 命令被阻止 + +[source,sql] +---- +-- MERGE with INSERT action +MERGE INTO ro_view USING (SELECT 4 AS a, 'merge_ins' AS b) AS src +ON (ro_view.a = src.a) +WHEN NOT MATCHED THEN INSERT VALUES (src.a, src.b); +-- 期望输出:ERROR: cannot modify view "ro_view" +-- HINT: The view is defined as read-only. + +-- MERGE with UPDATE action +MERGE INTO ro_view USING (SELECT 1 AS a, 'merge_upd' AS b) AS src +ON (ro_view.a = src.a) +WHEN MATCHED THEN UPDATE SET b = src.b; +-- 期望输出:ERROR: cannot modify view "ro_view" +-- HINT: The view is defined as read-only. +---- + +=== CREATE OR REPLACE 行为 + +[source,sql] +---- +-- 重新创建视图时保留 WITH READ ONLY:DML 仍然被阻止 +CREATE OR REPLACE VIEW ro_view AS SELECT a, b FROM t_ro WITH READ ONLY; +INSERT INTO ro_view VALUES (3, 'fail'); +-- 期望输出:ERROR: cannot modify view "ro_view" +-- HINT: The view is defined as read-only. + +-- 重新创建视图时不指定 WITH READ ONLY:只读属性被清除,视图变为可更新 +CREATE OR REPLACE VIEW ro_view AS SELECT a, b FROM t_ro; +INSERT INTO ro_view VALUES (3, 'now_writable'); +SELECT * FROM ro_view ORDER BY a; +-- 期望输出: +-- a | b +-- ---+-------------- +-- 1 | hello +-- 2 | world +-- 3 | now_writable +---- + +=== FORCE VIEW 与 WITH READ ONLY + +[source,sql] +---- +-- 创建时基础表不存在,视图为占位符 +CREATE FORCE VIEW force_ro_view AS SELECT * FROM nonexistent_for_ro WITH READ ONLY; +-- 期望输出:WARNING: View created with compilation errors + +-- 创建基础表 +CREATE TABLE nonexistent_for_ro (a int, b text); + +-- 显式编译视图 +ALTER VIEW force_ro_view COMPILE; + +-- 编译成功后 DML 被阻止 +INSERT INTO force_ro_view VALUES (1, 'fail'); +-- 期望输出:ERROR: cannot modify view "force_ro_view" +-- HINT: The view is defined as read-only. +---- + +=== 递归视图与 WITH READ ONLY + +[source,sql] +---- +CREATE RECURSIVE VIEW ro_recursive_view (a) AS + SELECT 1 + UNION ALL + SELECT a + 1 FROM ro_recursive_view WHERE a < 3 +WITH READ ONLY; + +SELECT * FROM ro_recursive_view ORDER BY a; +-- 期望输出: +-- a +-- --- +-- 1 +-- 2 +-- 3 + +INSERT INTO ro_recursive_view VALUES (99); +-- 期望输出:ERROR: cannot modify view "ro_recursive_view" +-- HINT: The view is defined as read-only. +---- + +=== 验证 reloptions 存储 + +[source,sql] +---- +SELECT relname, reloptions +FROM pg_class +WHERE relname IN ('ro_view', 'ro_recursive_view', 'force_ro_view') +ORDER BY relname; +-- 期望输出: +-- relname | reloptions +-- -------------------+------------------ +-- force_ro_view | {read_only=true} +-- ro_recursive_view | {read_only=true} +-- ro_view | +---- + +=== 清理 + +[source,sql] +---- +DROP VIEW IF EXISTS ro_view; +DROP VIEW IF EXISTS force_ro_view; +DROP TABLE t_ro; +DROP TABLE IF EXISTS nonexistent_for_ro; +---- diff --git a/EN/modules/ROOT/nav.adoc b/EN/modules/ROOT/nav.adoc index 6379a08..34a3ce3 100644 --- a/EN/modules/ROOT/nav.adoc +++ b/EN/modules/ROOT/nav.adoc @@ -26,6 +26,7 @@ ** xref:master/oracle_compatibility/compat_sys_guid.adoc[17、sys_guid Function] ** xref:master/oracle_compatibility/compat_empty_string_to_null.adoc[18、Empty String to NULL] ** xref:master/oracle_compatibility/compat_call_into.adoc[19、CALL INTO] +** xref:master/oracle_compatibility/compat_read_only_view.adoc[20、Read Only View] * Containerization and Cloud Service ** Containerization *** xref:master/containerization/k8s_deployment.adoc[K8S deployment] @@ -90,6 +91,7 @@ *** xref:master/compatibility_features_design/sys_guid_function.adoc[sys_guid Function] *** xref:master/compatibility_features_design/empty_string_to_null.adoc[Empty String to NULL] *** xref:master/compatibility_features_design/call_into.adoc[CALL INTO] +*** xref:master/compatibility_features_design/read_only_view.adoc[Read Only View] ** Built-in Functions *** xref:master/oracle_builtin_functions/sys_context.adoc[sys_context] *** xref:master/oracle_builtin_functions/userenv.adoc[userenv] diff --git a/EN/modules/ROOT/pages/master/compatibility_features_design/read_only_view.adoc b/EN/modules/ROOT/pages/master/compatibility_features_design/read_only_view.adoc new file mode 100644 index 0000000..f569a14 --- /dev/null +++ b/EN/modules/ROOT/pages/master/compatibility_features_design/read_only_view.adoc @@ -0,0 +1,235 @@ +:sectnums: +: sectnumlevels: 5 + +:imagesdir: ./_images + += Read-Only View + +== Purpose + +Oracle database supports using the `WITH READ ONLY` clause when creating views to set them as read-only, preventing any data modification operations (INSERT, UPDATE, DELETE, MERGE) on the view. To provide Oracle compatibility, IvorySQL implements the read-only view feature. Once a view is marked as read-only, any DML operations attempting to modify the view's data will be rejected, ensuring behavioral consistency for Oracle applications running on IvorySQL. + +== Implementation Details + +=== Syntax and Parsing + +==== Syntax Rule Extension + +The `READ_ONLY_OPTION` enum value was added in the `ora_gram.y` file, and the `opt_check_option` syntax rule was extended: + +[source,yacc] +---- +opt_check_option: + WITH CHECK OPTION { $$ = CASCADED_CHECK_OPTION; } + | WITH CASCADED CHECK OPTION { $$ = CASCADED_CHECK_OPTION; } + | WITH LOCAL CHECK OPTION { $$ = LOCAL_CHECK_OPTION; } + | WITH READ ONLY { $$ = READ_ONLY_OPTION; } /* New */ + | /* EMPTY */ { $$ = NO_CHECK_OPTION; } + ; +---- + +==== ViewStmt Structure Extension + +In the `parsenodes.h` file, the `ViewCheckOption` enum was extended with `READ_ONLY_OPTION`, and the `ViewStmt` structure was extended with a `readOnly` field: + +[source,c] +---- +typedef enum ViewCheckOption { + NO_CHECK_OPTION, + LOCAL_CHECK_OPTION, + CASCADED_CHECK_OPTION, + READ_ONLY_OPTION, /* WITH READ ONLY (Oracle compat) */ +} ViewCheckOption; + +typedef struct ViewStmt { + // ... other fields + bool readOnly; /* WITH READ ONLY (Oracle compat) */ +} ViewStmt; +---- + +The `readOnly` flag is set during parsing: + +[source,c] +---- +n->readOnly = ($10 == READ_ONLY_OPTION); +n->withCheckOption = n->readOnly ? NO_CHECK_OPTION : $10; +---- + +Note that `WITH READ ONLY` and `WITH CHECK OPTION` are mutually exclusive and cannot be used together. + +=== Relation Options System + +==== read_only Relation Option + +The `read_only` view relation option was defined in the `reloptions.c` file: + +[source,c] +---- +/* reloptions.c */ +{ + {"read_only", + "Prevents INSERT, UPDATE, and DELETE on this view (Oracle compatibility)", + RELOPT_KIND_VIEW, + AccessExclusiveLock}, + false +}, + +/* in view_reloptions() function */ +{"read_only", RELOPT_TYPE_BOOL, + offsetof(ViewOptions, read_only)}, +---- + +==== ViewOptions Structure Extension + +In the `rel.h` file, the `ViewOptions` structure was extended with a `read_only` field: + +[source,c] +---- +typedef struct ViewOptions { + // ... other fields + bool read_only; /* WITH READ ONLY (Oracle compat) */ +} ViewOptions; +---- + +==== RelationIsReadOnlyView Macro + +The `RelationIsReadOnlyView` macro allows quick determination of whether a view is read-only: + +[source,c] +---- +#define RelationIsReadOnlyView(relation) \ + (AssertMacro(relation->rd_rel->relkind == RELKIND_VIEW), \ + (relation)->rd_options && \ + ((ViewOptions *) (relation)->rd_options)->read_only) +---- + +=== View Creation Processing + +The `WITH READ ONLY` option is handled in the `DefineView` function in the `view.c` file: + +[source,c] +---- +/* Handle WITH READ ONLY in DefineView() */ +if (stmt->readOnly) +{ + /* Set read_only relation option */ + options = transformViewOptions(RelationGetRelid(newRelationView), + stmt->options, true); + setRelOptions(newRelationView, options, true, AccessExclusiveLock); +} +---- + +`WITH READ ONLY` is also supported in the `compile_force_view_internal` function to ensure FORCE VIEW can properly use the read-only attribute. + +=== DML Execution Interception + +==== Execution Layer Interception + +In the `CheckValidResultRel()` function in `execMain.c`, DML operations on read-only views are intercepted: + +[source,c] +---- +/* execMain.c */ +if (RelationIsReadOnlyView(resultRel)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot modify view \"%s\"", + RelationGetRelationName(resultRel)), + errhint("The view is defined as read-only."))); +---- + +When executing INSERT, UPDATE, DELETE, or MERGE statements, an error is thrown if the target view is marked as read-only. + +==== Rewrite Layer Interception + +The same check was added in the `rewriteTargetView()` function in `rewriteHandler.c`: + +[source,c] +---- +/* rewriteHandler.c */ +if (RelationIsReadOnlyView(view)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot modify view \"%s\"", + RelationGetRelationName(view)), + errhint("The view is defined as read-only."))); +---- + +This ensures that modifications to read-only views are also blocked at the query rewrite layer. + +=== pg_dump Support + +Support for exporting and restoring the `WITH READ ONLY` attribute was added in `pg_dump.c`: + +[source,c] +---- +/* pg_dump.h - TableInfo structure new field */ +typedef struct _tableInfo { + // ... other fields + bool readOnly; /* WITH READ ONLY (Oracle compat) */ + // ... other fields +} _tableInfo; + +/* pg_dump.c - extract read_only option */ +if (strcmp(options[i].defname, "read_only") == 0) + info->readOnly = defGetBoolean(options[i].def); +---- + +During export, the `dumpTableSchema()` and `dumpRule()` functions output the `WITH READ ONLY` clause in the appropriate location, ensuring the read-only attribute is preserved during database migration. + +== Usage Examples + +=== Creating a Read-Only View + +[source,sql] +---- +-- Create a view with WITH READ ONLY +CREATE VIEW emp_view AS +SELECT * FROM employees +WITH READ ONLY; + +-- DML on read-only view will be rejected +INSERT INTO emp_view VALUES (1, 'John', 5000); +-- ERROR: cannot modify view "emp_view" +-- HINT: The view is defined as read-only. + +UPDATE emp_view SET salary = 6000 WHERE id = 1; +-- ERROR: cannot modify view "emp_view" +-- HINT: The view is defined as read-only. + +DELETE FROM emp_view WHERE id = 1; +-- ERROR: cannot modify view "emp_view" +-- HINT: The view is defined as read-only. +---- + +=== Create or Replace Read-Only View + +[source,sql] +---- +-- Use OR REPLACE to preserve read-only attribute +CREATE OR REPLACE VIEW emp_view AS +SELECT id, name, department FROM employees +WITH READ ONLY; +---- + +=== FORCE VIEW with Read-Only Attribute + +[source,sql] +---- +-- Create FORCE VIEW and set as read-only +CREATE OR REPLACE FORCE VIEW emp_force_view AS +SELECT * FROM employees +WITH READ ONLY; +---- + +=== Mutual Exclusivity with WITH CHECK OPTION + +[source,sql] +---- +-- Error: WITH READ ONLY and WITH CHECK OPTION cannot be used together +CREATE VIEW emp_view AS +SELECT * FROM employees +WITH CHECK OPTION +WITH READ ONLY; +-- ERROR: READ ONLY and CHECK OPTION are mutually exclusive +---- diff --git a/EN/modules/ROOT/pages/master/oracle_compatibility/compat_read_only_view.adoc b/EN/modules/ROOT/pages/master/oracle_compatibility/compat_read_only_view.adoc new file mode 100644 index 0000000..7415a6b --- /dev/null +++ b/EN/modules/ROOT/pages/master/oracle_compatibility/compat_read_only_view.adoc @@ -0,0 +1,168 @@ +:sectnums: +: sectnumlevels: 5 + +:imagesdir: ./_images + += Read Only View + +== Purpose + +This document describes the purpose of `WITH READ ONLY` views in IvorySQL, implementing read-only view functionality to maintain compatibility with Oracle behavior. + +== Functionality Description + +- `WITH READ ONLY`: When specified during view creation, this option prevents INSERT, UPDATE, DELETE, and MERGE operations on the view. +- Storage: `read_only=true` is stored in the view's `reloptions`. +- Mutually Exclusive: `WITH READ ONLY` and `WITH CHECK OPTION` are mutually exclusive and cannot be specified together. +- Force View Support: Can be used with `FORCE VIEW`; the read-only attribute takes effect after the view compiles successfully. +- CREATE OR REPLACE Behavior: If a view is recreated using `CREATE OR REPLACE VIEW` without specifying `WITH READ ONLY`, the read-only attribute is cleared. + +== Test Cases + +=== Creating a Read Only View + +[source,sql] +---- +-- Create base table +CREATE TABLE t_ro (a int, b text); +INSERT INTO t_ro VALUES (1, 'hello'), (2, 'world'); + +-- Create read-only view, SELECT succeeds +CREATE VIEW ro_view AS SELECT * FROM t_ro WITH READ ONLY; +SELECT * FROM ro_view ORDER BY a; +-- Expected output: +-- a | b +-- ---+------- +-- 1 | hello +-- 2 | world +---- + +=== Verify DML is Blocked + +[source,sql] +---- +-- INSERT is blocked +INSERT INTO ro_view VALUES (3, 'fail'); +-- Expected output: ERROR: cannot modify view "ro_view" +-- HINT: The view is defined as read-only. + +-- UPDATE is blocked +UPDATE ro_view SET b = 'fail' WHERE a = 1; +-- Expected output: ERROR: cannot modify view "ro_view" +-- HINT: The view is defined as read-only. + +-- DELETE is blocked +DELETE FROM ro_view WHERE a = 1; +-- Expected output: ERROR: cannot modify view "ro_view" +-- HINT: The view is defined as read-only. +---- + +=== MERGE Command is Blocked + +[source,sql] +---- +-- MERGE with INSERT action +MERGE INTO ro_view USING (SELECT 4 AS a, 'merge_ins' AS b) AS src +ON (ro_view.a = src.a) +WHEN NOT MATCHED THEN INSERT VALUES (src.a, src.b); +-- Expected output: ERROR: cannot modify view "ro_view" +-- HINT: The view is defined as read-only. + +-- MERGE with UPDATE action +MERGE INTO ro_view USING (SELECT 1 AS a, 'merge_upd' AS b) AS src +ON (ro_view.a = src.a) +WHEN MATCHED THEN UPDATE SET b = src.b; +-- Expected output: ERROR: cannot modify view "ro_view" +-- HINT: The view is defined as read-only. +---- + +=== CREATE OR REPLACE Behavior + +[source,sql] +---- +-- Recreating view with WITH READ ONLY: DML is still blocked +CREATE OR REPLACE VIEW ro_view AS SELECT a, b FROM t_ro WITH READ ONLY; +INSERT INTO ro_view VALUES (3, 'fail'); +-- Expected output: ERROR: cannot modify view "ro_view" +-- HINT: The view is defined as read-only. + +-- Recreating view without WITH READ ONLY: read-only attribute is cleared, view becomes updatable +CREATE OR REPLACE VIEW ro_view AS SELECT a, b FROM t_ro; +INSERT INTO ro_view VALUES (3, 'now_writable'); +SELECT * FROM ro_view ORDER BY a; +-- Expected output: +-- a | b +-- ---+-------------- +-- 1 | hello +-- 2 | world +-- 3 | now_writable +---- + +=== FORCE VIEW with WITH READ ONLY + +[source,sql] +---- +-- Base table does not exist at creation time, view is a placeholder +CREATE FORCE VIEW force_ro_view AS SELECT * FROM nonexistent_for_ro WITH READ ONLY; +-- Expected output: WARNING: View created with compilation errors + +-- Create base table +CREATE TABLE nonexistent_for_ro (a int, b text); + +-- Explicitly compile view +ALTER VIEW force_ro_view COMPILE; + +-- After successful compilation, DML is blocked +INSERT INTO force_ro_view VALUES (1, 'fail'); +-- Expected output: ERROR: cannot modify view "force_ro_view" +-- HINT: The view is defined as read-only. +---- + +=== Recursive View with WITH READ ONLY + +[source,sql] +---- +CREATE RECURSIVE VIEW ro_recursive_view (a) AS + SELECT 1 + UNION ALL + SELECT a + 1 FROM ro_recursive_view WHERE a < 3 +WITH READ ONLY; + +SELECT * FROM ro_recursive_view ORDER BY a; +-- Expected output: +-- a +-- --- +-- 1 +-- 2 +-- 3 + +INSERT INTO ro_recursive_view VALUES (99); +-- Expected output: ERROR: cannot modify view "ro_recursive_view" +-- HINT: The view is defined as read-only. +---- + +=== Verify reloptions Storage + +[source,sql] +---- +SELECT relname, reloptions +FROM pg_class +WHERE relname IN ('ro_view', 'ro_recursive_view', 'force_ro_view') +ORDER BY relname; +-- Expected output: +-- relname | reloptions +-- -------------------+------------------ +-- force_ro_view | {read_only=true} +-- ro_recursive_view | {read_only=true} +-- ro_view | +---- + +=== Cleanup + +[source,sql] +---- +DROP VIEW IF EXISTS ro_view; +DROP VIEW IF EXISTS force_ro_view; +DROP TABLE t_ro; +DROP TABLE IF EXISTS nonexistent_for_ro; +----