From 186fbe6b61661a200e7532b2504008503b05884b Mon Sep 17 00:00:00 2001 From: himmel Date: Tue, 1 Sep 2026 06:48:17 +0000 Subject: [PATCH] add doc for lnnvl feature Add Chinese and English documentation for the Oracle compatible LNNVL function, following the two-document layout used for the other built-in functions: an implementation note under oracle_builtin_functions and a usage document under oracle_compatibility. The implementation notes cover the equivalence with the SQL standard IS NOT TRUE predicate, the SQL definition and the reason for each of its properties, why CALLED ON NULL INPUT is mandatory and what breaks if it is changed to STRICT, and function inlining. The usage documents cover the truth table, the partition property that distinguishes LNNVL from a plain NOT, the NOT IN over a NULL-containing subquery case, argument type checking, and the behavioural differences from Oracle, including that the accepted argument forms are a superset of Oracle's. Also fix a duplicated entry number in the English navigation, where dbtimezone was numbered 24 like the preceding entry; it is now 25, matching the Chinese navigation, and LNNVL follows as 26 in both. --- CN/modules/ROOT/nav.adoc | 256 ++++++++--------- .../oracle_builtin_functions/lnnvl.adoc | 131 +++++++++ .../oracle_compatibility/compat_lnnvl.adoc | 253 +++++++++++++++++ EN/modules/ROOT/nav.adoc | 4 +- .../oracle_builtin_functions/lnnvl.adoc | 145 ++++++++++ .../oracle_compatibility/compat_lnnvl.adoc | 267 ++++++++++++++++++ 6 files changed, 928 insertions(+), 128 deletions(-) create mode 100644 CN/modules/ROOT/pages/master/oracle_builtin_functions/lnnvl.adoc create mode 100644 CN/modules/ROOT/pages/master/oracle_compatibility/compat_lnnvl.adoc create mode 100644 EN/modules/ROOT/pages/master/oracle_builtin_functions/lnnvl.adoc create mode 100644 EN/modules/ROOT/pages/master/oracle_compatibility/compat_lnnvl.adoc diff --git a/CN/modules/ROOT/nav.adoc b/CN/modules/ROOT/nav.adoc index b62fe4a5..ce0d05d5 100644 --- a/CN/modules/ROOT/nav.adoc +++ b/CN/modules/ROOT/nav.adoc @@ -1,127 +1,129 @@ -* xref:master/about_ivorysql.adoc[关于IvorySQL] -** xref:master/welcome.adoc[欢迎] -** xref:master/release_notes.adoc[发行说明] -* 快速上手 -** xref:master/getting-started/quick_start.adoc[快速开始] -* 安装部署 -** xref:master/installation_guide.adoc[安装指南] -** xref:master/cluster_setup.adoc[集群搭建] -* Oracle兼容功能 -** xref:master/oracle_compatibility/compat_case_conversion.adoc[1、大小写转换] -** xref:master/oracle_compatibility/compat_like_operator.adoc[2、LIKE操作符] -** xref:master/oracle_compatibility/anonymous_block.adoc[3、匿名块] -** xref:master/oracle_compatibility/compat_function_procedure.adoc[4、函数与存储过程] -** xref:master/oracle_compatibility/builtin_types_functions.adoc[5、内置数据类型与内置函数] -** xref:master/oracle_compatibility/port_ip.adoc[6、端口与IP] -** xref:master/oracle_compatibility/xml_functions.adoc[7、XML函数] -** xref:master/oracle_compatibility/sequence.adoc[8、sequence] -** xref:master/oracle_compatibility/package.adoc[9、包] -** xref:master/oracle_compatibility/invisible_column.adoc[10、不可见列] -** xref:master/oracle_compatibility/compat_rowid.adoc[11、RowID] -** xref:master/oracle_compatibility/compat_out_parameter.adoc[12、OUT 参数] -** xref:master/oracle_compatibility/compat_type_rowtype.adoc[13、%TYPE、%ROWTYPE] -** xref:master/oracle_compatibility/compat_nls_parameter.adoc[14、NLS 参数] -** xref:master/oracle_compatibility/compat_force_view.adoc[15、Force View] -** xref:master/oracle_compatibility/compat_nested_function.adoc[16、嵌套子函数] -** 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/oracle_compatibility/with_function_procedure.adoc[21、WITH FUNCTION/PROCEDURE] -** xref:master/oracle_compatibility/compat_create_index_online.adoc[22、索引 ONLINE 参数] -** xref:master/oracle_compatibility/compat_stragg.adoc[23、STRAGG 函数] -** xref:master/oracle_compatibility/compat_alter_index_unusable.adoc[24、禁用索引] -** xref:master/oracle_compatibility/compat_dbtimezone.adoc[25、dbtimezone] -* 容器化与云服务 -** 容器化指南 -*** xref:master/containerization/k8s_deployment.adoc[K8S部署] -*** xref:master/containerization/operator_deployment.adoc[Operator部署] -*** xref:master/containerization/docker_podman_deployment.adoc[Docker & Podman部署] -*** xref:master/containerization/docker_swarm_compose_deployment.adoc[Docker Swarm & Docker Compose部署] -** 云服务平台指南 -*** xref:master/cloud_platform/ivorysql_cloud_installation.adoc[IvorySQL Cloud安装] -*** xref:master/cloud_platform/ivorysql_cloud_usage.adoc[IvorySQL Cloud使用] -* IvorySQL生态 -** xref:master/cpu_os_adaptation/cpu_architecture_adaptation.adoc[芯片架构适配] -** xref:master/cpu_os_adaptation/os_architecture_adaptation.adoc[操作系统适配] -** 生态组件适配 -*** xref:master/ecosystem_components/ecosystem_overview.adoc[概述] -*** xref:master/ecosystem_components/postgis.adoc[postgis] -*** xref:master/ecosystem_components/pgvector.adoc[pgvector] -*** xref:master/ecosystem_components/pgddl.adoc[pgddl(DDL Extractor)] -*** xref:master/ecosystem_components/pg_cron.adoc[pg_cron] -*** xref:master/ecosystem_components/pgsql_http.adoc[pgsql-http] -*** xref:master/ecosystem_components/plpgsql_check.adoc[plpgsql_check] -*** xref:master/ecosystem_components/pgroonga.adoc[pgroonga] -*** xref:master/ecosystem_components/pgaudit.adoc[pgaudit] -*** xref:master/ecosystem_components/pgrouting.adoc[pgrouting] -*** xref:master/ecosystem_components/system_stats.adoc[system_stats] -*** xref:master/ecosystem_components/wal2json.adoc[wal2json] -*** xref:master/ecosystem_components/pg_stat_monitor.adoc[pg_stat_monitor] -*** xref:master/ecosystem_components/pg_ai_query.adoc[pg_ai_query] -*** xref:master/ecosystem_components/pg_partman.adoc[pg_partman] -*** xref:master/ecosystem_components/pgbouncer.adoc[pgbouncer] -*** xref:master/ecosystem_components/age.adoc[age] -*** xref:master/ecosystem_components/pg_curl.adoc[pg_curl] -*** xref:master/ecosystem_components/pg_textsearch.adoc[pg_textsearch] -*** xref:master/ecosystem_components/pg_hint_plan.adoc[pg_hint_plan] -*** xref:master/ecosystem_components/redis_fdw.adoc[redis_fdw] -*** xref:master/ecosystem_components/pg_show_plans.adoc[pg_show_plans] -*** xref:master/ecosystem_components/pg_bulkload.adoc[pg_bulkload] -*** xref:master/ecosystem_components/pg_bigm.adoc[pg_bigm] -*** xref:master/ecosystem_components/pg_profile.adoc[pg_profile] -*** xref:master/ecosystem_components/pg_repack.adoc[pg_repack] -*** xref:master/ecosystem_components/pgdog.adoc[PgDog] -*** xref:master/ecosystem_components/pg_readonly.adoc[pg_readonly] -*** xref:master/ecosystem_components/zhparser.adoc[zhparser] -*** xref:master/ecosystem_components/pgbackrest.adoc[pgBackRest] -*** xref:master/ecosystem_components/set_user.adoc[set_user] -* 监控运维 -** xref:master/getting-started/daily_monitoring.adoc[日常监控] -** xref:master/getting-started/daily_maintenance.adoc[日常维护] -** xref:master/operation_guide.adoc[运维管理指南] -* 数据迁移 -** xref:master/migration_guide.adoc[迁移指南] -* IvorySQL开发者 -** xref:master/contribution/contribution_guide.adoc[贡献指南] -** xref:master/developer_guide.adoc[开发者指南] -** IvorySQL架构设计 -*** 查询处理 -**** xref:master/architecture/dual_parser.adoc[双parser] -*** 兼容框架 -**** xref:master/architecture/framework_design.adoc[框架设计] -**** xref:master/architecture/guc_framework.adoc[GUC框架] -**** xref:master/architecture/dual_mode_design.adoc[双模式设计] -**** xref:master/architecture/initdb_process.adoc[initdb过程] -*** 兼容特性 -**** xref:master/compatibility_features_design/like_operator.adoc[like] -**** xref:master/compatibility_features_design/rowid.adoc[RowID] -**** xref:master/compatibility_features_design/out_parameter.adoc[OUT 参数] -**** xref:master/compatibility_features_design/type_rowtype.adoc[%TYPE、%ROWTYPE] -**** xref:master/compatibility_features_design/nls_parameter.adoc[NLS 参数] -**** xref:master/compatibility_features_design/function_procedure.adoc[函数与存储过程] -**** xref:master/compatibility_features_design/nested_function.adoc[嵌套子函数] -**** xref:master/compatibility_features_design/force_view.adoc[Force View] -**** xref:master/compatibility_features_design/case_conversion.adoc[大小写转换] -**** 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/compatibility_features_design/with_function_procedure_impl.adoc[WITH FUNCTION/PROCEDURE] -**** xref:master/compatibility_features_design/create_index_online.adoc[索引 ONLINE 参数] -**** xref:master/compatibility_features_design/alter_index_unusable_impl.adoc[禁用索引] -*** 内置函数 -**** xref:master/oracle_builtin_functions/sys_context.adoc[sys_context] -**** xref:master/oracle_builtin_functions/userenv.adoc[userenv] -**** xref:master/oracle_builtin_functions/rawtohex.adoc[rawtohex] -**** xref:master/oracle_builtin_functions/stragg.adoc[stragg] -**** xref:master/oracle_builtin_functions/dbtimezone_impl.adoc[dbtimezone] -*** xref:master/gb18030.adoc[国标GB18030] -* 参考指南 -** xref:master/tools_reference.adoc[工具参考] -** xref:master/contribution/asciidoc_syntax_reference.adoc[asciidoc语法快速参考] -** xref:master/pg_reference/pg_parameters_reference.adoc[PG参数参考手册] -** xref:master/pg_reference/pg_functions_reference.adoc[PG函数参考手册] -* 常见问题解答 -** xref:master/contribution/faq.adoc[FAQ] - +* xref:master/about_ivorysql.adoc[关于IvorySQL] +** xref:master/welcome.adoc[欢迎] +** xref:master/release_notes.adoc[发行说明] +* 快速上手 +** xref:master/getting-started/quick_start.adoc[快速开始] +* 安装部署 +** xref:master/installation_guide.adoc[安装指南] +** xref:master/cluster_setup.adoc[集群搭建] +* Oracle兼容功能 +** xref:master/oracle_compatibility/compat_case_conversion.adoc[1、大小写转换] +** xref:master/oracle_compatibility/compat_like_operator.adoc[2、LIKE操作符] +** xref:master/oracle_compatibility/anonymous_block.adoc[3、匿名块] +** xref:master/oracle_compatibility/compat_function_procedure.adoc[4、函数与存储过程] +** xref:master/oracle_compatibility/builtin_types_functions.adoc[5、内置数据类型与内置函数] +** xref:master/oracle_compatibility/port_ip.adoc[6、端口与IP] +** xref:master/oracle_compatibility/xml_functions.adoc[7、XML函数] +** xref:master/oracle_compatibility/sequence.adoc[8、sequence] +** xref:master/oracle_compatibility/package.adoc[9、包] +** xref:master/oracle_compatibility/invisible_column.adoc[10、不可见列] +** xref:master/oracle_compatibility/compat_rowid.adoc[11、RowID] +** xref:master/oracle_compatibility/compat_out_parameter.adoc[12、OUT 参数] +** xref:master/oracle_compatibility/compat_type_rowtype.adoc[13、%TYPE、%ROWTYPE] +** xref:master/oracle_compatibility/compat_nls_parameter.adoc[14、NLS 参数] +** xref:master/oracle_compatibility/compat_force_view.adoc[15、Force View] +** xref:master/oracle_compatibility/compat_nested_function.adoc[16、嵌套子函数] +** 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/oracle_compatibility/with_function_procedure.adoc[21、WITH FUNCTION/PROCEDURE] +** xref:master/oracle_compatibility/compat_create_index_online.adoc[22、索引 ONLINE 参数] +** xref:master/oracle_compatibility/compat_stragg.adoc[23、STRAGG 函数] +** xref:master/oracle_compatibility/compat_alter_index_unusable.adoc[24、禁用索引] +** xref:master/oracle_compatibility/compat_dbtimezone.adoc[25、dbtimezone] +** xref:master/oracle_compatibility/compat_lnnvl.adoc[26、LNNVL 函数] +* 容器化与云服务 +** 容器化指南 +*** xref:master/containerization/k8s_deployment.adoc[K8S部署] +*** xref:master/containerization/operator_deployment.adoc[Operator部署] +*** xref:master/containerization/docker_podman_deployment.adoc[Docker & Podman部署] +*** xref:master/containerization/docker_swarm_compose_deployment.adoc[Docker Swarm & Docker Compose部署] +** 云服务平台指南 +*** xref:master/cloud_platform/ivorysql_cloud_installation.adoc[IvorySQL Cloud安装] +*** xref:master/cloud_platform/ivorysql_cloud_usage.adoc[IvorySQL Cloud使用] +* IvorySQL生态 +** xref:master/cpu_os_adaptation/cpu_architecture_adaptation.adoc[芯片架构适配] +** xref:master/cpu_os_adaptation/os_architecture_adaptation.adoc[操作系统适配] +** 生态组件适配 +*** xref:master/ecosystem_components/ecosystem_overview.adoc[概述] +*** xref:master/ecosystem_components/postgis.adoc[postgis] +*** xref:master/ecosystem_components/pgvector.adoc[pgvector] +*** xref:master/ecosystem_components/pgddl.adoc[pgddl(DDL Extractor)] +*** xref:master/ecosystem_components/pg_cron.adoc[pg_cron] +*** xref:master/ecosystem_components/pgsql_http.adoc[pgsql-http] +*** xref:master/ecosystem_components/plpgsql_check.adoc[plpgsql_check] +*** xref:master/ecosystem_components/pgroonga.adoc[pgroonga] +*** xref:master/ecosystem_components/pgaudit.adoc[pgaudit] +*** xref:master/ecosystem_components/pgrouting.adoc[pgrouting] +*** xref:master/ecosystem_components/system_stats.adoc[system_stats] +*** xref:master/ecosystem_components/wal2json.adoc[wal2json] +*** xref:master/ecosystem_components/pg_stat_monitor.adoc[pg_stat_monitor] +*** xref:master/ecosystem_components/pg_ai_query.adoc[pg_ai_query] +*** xref:master/ecosystem_components/pg_partman.adoc[pg_partman] +*** xref:master/ecosystem_components/pgbouncer.adoc[pgbouncer] +*** xref:master/ecosystem_components/age.adoc[age] +*** xref:master/ecosystem_components/pg_curl.adoc[pg_curl] +*** xref:master/ecosystem_components/pg_textsearch.adoc[pg_textsearch] +*** xref:master/ecosystem_components/pg_hint_plan.adoc[pg_hint_plan] +*** xref:master/ecosystem_components/redis_fdw.adoc[redis_fdw] +*** xref:master/ecosystem_components/pg_show_plans.adoc[pg_show_plans] +*** xref:master/ecosystem_components/pg_bulkload.adoc[pg_bulkload] +*** xref:master/ecosystem_components/pg_bigm.adoc[pg_bigm] +*** xref:master/ecosystem_components/pg_profile.adoc[pg_profile] +*** xref:master/ecosystem_components/pg_repack.adoc[pg_repack] +*** xref:master/ecosystem_components/pgdog.adoc[PgDog] +*** xref:master/ecosystem_components/pg_readonly.adoc[pg_readonly] +*** xref:master/ecosystem_components/zhparser.adoc[zhparser] +*** xref:master/ecosystem_components/pgbackrest.adoc[pgBackRest] +*** xref:master/ecosystem_components/set_user.adoc[set_user] +* 监控运维 +** xref:master/getting-started/daily_monitoring.adoc[日常监控] +** xref:master/getting-started/daily_maintenance.adoc[日常维护] +** xref:master/operation_guide.adoc[运维管理指南] +* 数据迁移 +** xref:master/migration_guide.adoc[迁移指南] +* IvorySQL开发者 +** xref:master/contribution/contribution_guide.adoc[贡献指南] +** xref:master/developer_guide.adoc[开发者指南] +** IvorySQL架构设计 +*** 查询处理 +**** xref:master/architecture/dual_parser.adoc[双parser] +*** 兼容框架 +**** xref:master/architecture/framework_design.adoc[框架设计] +**** xref:master/architecture/guc_framework.adoc[GUC框架] +**** xref:master/architecture/dual_mode_design.adoc[双模式设计] +**** xref:master/architecture/initdb_process.adoc[initdb过程] +*** 兼容特性 +**** xref:master/compatibility_features_design/like_operator.adoc[like] +**** xref:master/compatibility_features_design/rowid.adoc[RowID] +**** xref:master/compatibility_features_design/out_parameter.adoc[OUT 参数] +**** xref:master/compatibility_features_design/type_rowtype.adoc[%TYPE、%ROWTYPE] +**** xref:master/compatibility_features_design/nls_parameter.adoc[NLS 参数] +**** xref:master/compatibility_features_design/function_procedure.adoc[函数与存储过程] +**** xref:master/compatibility_features_design/nested_function.adoc[嵌套子函数] +**** xref:master/compatibility_features_design/force_view.adoc[Force View] +**** xref:master/compatibility_features_design/case_conversion.adoc[大小写转换] +**** 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/compatibility_features_design/with_function_procedure_impl.adoc[WITH FUNCTION/PROCEDURE] +**** xref:master/compatibility_features_design/create_index_online.adoc[索引 ONLINE 参数] +**** xref:master/compatibility_features_design/alter_index_unusable_impl.adoc[禁用索引] +*** 内置函数 +**** xref:master/oracle_builtin_functions/sys_context.adoc[sys_context] +**** xref:master/oracle_builtin_functions/userenv.adoc[userenv] +**** xref:master/oracle_builtin_functions/rawtohex.adoc[rawtohex] +**** xref:master/oracle_builtin_functions/stragg.adoc[stragg] +**** xref:master/oracle_builtin_functions/dbtimezone_impl.adoc[dbtimezone] +**** xref:master/oracle_builtin_functions/lnnvl.adoc[lnnvl] +*** xref:master/gb18030.adoc[国标GB18030] +* 参考指南 +** xref:master/tools_reference.adoc[工具参考] +** xref:master/contribution/asciidoc_syntax_reference.adoc[asciidoc语法快速参考] +** xref:master/pg_reference/pg_parameters_reference.adoc[PG参数参考手册] +** xref:master/pg_reference/pg_functions_reference.adoc[PG函数参考手册] +* 常见问题解答 +** xref:master/contribution/faq.adoc[FAQ] + diff --git a/CN/modules/ROOT/pages/master/oracle_builtin_functions/lnnvl.adoc b/CN/modules/ROOT/pages/master/oracle_builtin_functions/lnnvl.adoc new file mode 100644 index 00000000..58c51111 --- /dev/null +++ b/CN/modules/ROOT/pages/master/oracle_builtin_functions/lnnvl.adoc @@ -0,0 +1,131 @@ +:sectnums: +:sectnumlevels: 5 + +:imagesdir: ./_images + += LNNVL 函数的实现 + +== 目的 + +IvorySQL 在 `contrib/ivorysql_ora` 扩展中新增 `sys.lnnvl(boolean)` 函数, +实现与 Oracle 同名函数一致的条件取反行为:条件求值为 FALSE 或 UNKNOWN 时返回 TRUE, +求值为 TRUE 时返回 FALSE。 + +== 实现说明 + +=== 语义等价于 IS NOT TRUE + +LNNVL 的真值表与 SQL 标准的 `IS NOT TRUE` 谓词逐格相同: + +[cols="1,1,1"] +|=== +|条件求值结果 |`LNNVL(cond)` |`cond IS NOT TRUE` + +|TRUE +|FALSE +|FALSE + +|FALSE +|TRUE +|TRUE + +|UNKNOWN +|TRUE +|TRUE +|=== + +因此实现无需新增任何求值逻辑,函数体只是这一个谓词。 +PostgreSQL 中 `IS NOT TRUE` 的求值位于 +`src/backend/executor/execExprInterp.c` 的 `EEOP_BOOLTEST_IS_NOT_TRUE` 分支, +入参为 NULL 时直接返回 true 并清除结果的 null 标记, +因此 LNNVL 自身永不返回 NULL。 + +=== SQL 定义 + +函数定义位于 +`contrib/ivorysql_ora/src/builtin_functions/builtin_functions--1.0.sql`。 + +[source,sql] +---- +CREATE FUNCTION sys.lnnvl(pg_catalog.bool) +RETURNS pg_catalog.bool +AS $$SELECT $1 IS NOT TRUE$$ +LANGUAGE sql +CALLED ON NULL INPUT +PARALLEL SAFE +IMMUTABLE; +---- + +各项属性的选择理由: + +[cols="1,1,3"] +|=== +|属性 |取值 |理由 + +|所在模式 +|`sys` +|与其它 Oracle 兼容函数一致。`sys` 仅在 Oracle 兼容模式下进入隐式 `search_path`。 + +|参数类型 +|`pg_catalog.bool` +|显式限定模式,避免 Oracle 模式下 `sys` 位于 `search_path` 首位时类型名解析受影响。 + +|语言 +|`sql` +|可被 `inline_function()` 内联,见下节。C 函数不会被内联。 + +|NULL 处理 +|`CALLED ON NULL INPUT` +|必须项,见下节。 + +|易变性 +|`IMMUTABLE` +|纯逻辑运算,同一输入恒定输出;也是用于表达式索引等场景的前提。 + +|并行性 +|`PARALLEL SAFE` +|无副作用。若不声明,默认为 `PARALLEL UNSAFE`,会禁用相关查询的并行计划。 +|=== + +=== CALLED ON NULL INPUT 是必须项 + +`CALLED ON NULL INPUT` 是 SQL 的默认值,此处显式写出是为了防止被误改。 + +若声明为 `STRICT`,入参为 NULL 时函数体根本不会执行,直接返回 NULL, +而正确行为是返回 TRUE —— 恰好毁掉 LNNVL 存在的唯一理由,且不产生任何错误。 +风险来自代码风格的一致性压力:`builtin_functions--1.0.sql` 中多数邻近函数均声明为 +`STRICT`,按邻居样式统一即会踩中。 + +`STRICT` 还会连带废掉内联。声明为严格意味着承诺"NULL 进则 NULL 出", +而 `IS NOT TRUE` 本身并非严格谓词,优化器无法在保持语义的前提下将其展开, +只能保留函数调用: + +[source,text] +---- +-- CALLED ON NULL INPUT +Filter: (t.v IS NOT TRUE) + +-- STRICT +Filter: f_strict(t.v) +---- + +因此误写为 `STRICT` 是双重损失:语义错误且静默,同时性能退化为逐行函数调用。 + +=== 函数内联 + +函数体是可内联的简单 SQL 表达式,优化器的 `inline_function()` 会将其折叠到调用处: + +[source,sql] +---- +EXPLAIN (VERBOSE, COSTS OFF) SELECT id FROM t WHERE LNNVL(v >= 0.2); +---- + +[source,text] +---- + Seq Scan on public.t + Output: id + Filter: ((t.v >= 0.2) IS NOT TRUE) +---- + +计划中已不存在函数调用,与用户直接书写 `IS NOT TRUE` 的计划逐字相同, +因此不引入额外运行期开销。 diff --git a/CN/modules/ROOT/pages/master/oracle_compatibility/compat_lnnvl.adoc b/CN/modules/ROOT/pages/master/oracle_compatibility/compat_lnnvl.adoc new file mode 100644 index 00000000..88a815ba --- /dev/null +++ b/CN/modules/ROOT/pages/master/oracle_compatibility/compat_lnnvl.adoc @@ -0,0 +1,253 @@ +:sectnums: +:sectnumlevels: 5 + +:imagesdir: ./_images + += LNNVL 函数 + +== 目的 + +本文档说明 IvorySQL 中 `sys.lnnvl` 函数的功能,该函数以兼容 Oracle 的方式 +对条件取反,并且在条件求值结果为 UNKNOWN 时仍然返回 TRUE。 + +== 功能说明 + +* `sys.lnnvl(boolean)` 接受一个条件,条件求值为 FALSE 或 UNKNOWN 时返回 TRUE, + 求值为 TRUE 时返回 FALSE。 +* 函数**永不返回 NULL**,返回值必为 TRUE 或 FALSE 之一, + 因此可以直接用作 WHERE 条件而不会引入新的 UNKNOWN。 +* 主要用途是消除三值逻辑造成的"行丢失":直接书写 `NOT` 时, + 条件为 UNKNOWN 的行在原条件和取反条件下都会被丢弃; + 而条件与其 LNNVL 构成对整表的严格划分,两者的结果行数之和恒等于总行数。 + +真值表: + +[cols="1,1"] +|=== +|条件求值结果 |返回值 + +|TRUE +|FALSE + +|FALSE +|TRUE + +|UNKNOWN(条件中含 NULL) +|TRUE +|=== + +== 语法 + +[source,sql] +---- +sys.lnnvl(condition) +---- + +[cols="1,3"] +|=== +|参数 |说明 + +|`condition` +|任意布尔表达式。不接受非布尔类型,且不存在从整型的隐式转换。 +|=== + +返回类型:`boolean` + +== 测试用例 + +=== 测试环境准备 + +[source,sql] +---- +CREATE TABLE lnnvl_test (a INT, b INT); +INSERT INTO lnnvl_test VALUES (2, NULL); +---- + +=== 真值表验证 + +[source,sql] +---- +-- a = 2,b 为 NULL +SELECT LNNVL(a = 1) FROM lnnvl_test; -- 期望:t(条件为 FALSE) +SELECT LNNVL(a = 2) FROM lnnvl_test; -- 期望:f(条件为 TRUE) +SELECT LNNVL(a IS NULL) FROM lnnvl_test; -- 期望:t(条件为 FALSE) +SELECT LNNVL(b = 1) FROM lnnvl_test; -- 期望:t(条件为 UNKNOWN) +SELECT LNNVL(b IS NULL) FROM lnnvl_test; -- 期望:f(条件为 TRUE) +SELECT LNNVL(a = b) FROM lnnvl_test; -- 期望:t(条件为 UNKNOWN) +---- + +=== NULL 入参 + +[source,sql] +---- +-- NULL 入参必须返回 t,这是 LNNVL 的核心行为 +SELECT LNNVL(NULL); -- 期望:t +SELECT LNNVL(NULL::boolean); -- 期望:t +---- + +=== 划分性质 + +[source,sql] +---- +DROP TABLE lnnvl_test; +CREATE TABLE lnnvl_test (id INT, v NUMERIC); +INSERT INTO lnnvl_test VALUES (1,0.30),(2,0.10),(3,NULL),(4,0.25),(5,NULL); + +SELECT count(*) FROM lnnvl_test WHERE v >= 0.2; -- 期望:2 +SELECT count(*) FROM lnnvl_test WHERE LNNVL(v >= 0.2); -- 期望:3 +SELECT count(*) FROM lnnvl_test; -- 期望:5 + +-- 2 + 3 = 5,条件与其 LNNVL 覆盖全表,无行丢失 +-- 若改用 NOT,两者之和会小于总行数 +SELECT count(*) FROM lnnvl_test WHERE NOT (v >= 0.2); -- 期望:2,合计仅 4 +---- + +=== NOT IN 遇含 NULL 的子查询 + +[source,sql] +---- +CREATE TABLE lnnvl_bl (d INT); +INSERT INTO lnnvl_bl VALUES (1), (NULL); + +-- 黑名单含 NULL 时,NOT IN 返回空集 +SELECT count(*) FROM lnnvl_test WHERE id NOT IN (SELECT d FROM lnnvl_bl); +-- 期望:0 + +-- 改写为 LNNVL(... IN ...) 后得到符合直觉的结果 +SELECT count(*) FROM lnnvl_test WHERE LNNVL(id IN (SELECT d FROM lnnvl_bl)); +-- 期望:4 +---- + +`id NOT IN (1, NULL)` 展开为 `id <> 1 AND id <> NULL`, +其中 `id <> NULL` 恒为 UNKNOWN,导致整个条件不为 TRUE,因此所有行都被丢弃。 +`LNNVL(id IN (...))` 把 UNKNOWN 判定为"非真"从而保留这些行。 + +=== 参数类型检查 + +[source,sql] +---- +-- 参数必须是布尔类型,不存在从整型的隐式转换 +SELECT LNNVL(1); +-- 期望报错:function lnnvl(pg_catalog.int4) does not exist +---- + +=== 测试环境清理 + +[source,sql] +---- +DROP TABLE lnnvl_bl; +DROP TABLE lnnvl_test; +---- + +== 与 Oracle 的行为差异 + +Oracle 的语法只接受单个简单条件。IvorySQL 以普通函数实现, +参数在重载决议时已经是一个布尔表达式,产生它的 `AND`、`OR`、`NOT`、`BETWEEN` +对函数不可见,因此无法复现 Oracle 的形态限制,**接受的形态是 Oracle 的超集**。 + +方向性结论:能在 Oracle 上运行的 LNNVL 写法在 IvorySQL 上一定能运行; +反之不保证 —— 在 IvorySQL 上写通的复合条件形式搬回 Oracle 会报错。 + +[cols="2,1,1"] +|=== +|参数形态 |Oracle |IvorySQL + +|`LNNVL(1 = 1 AND 2 = 2)` +|拒绝 +|接受,返回 f + +|`LNNVL(1 = 1 OR 2 = 2)` +|拒绝 +|接受,返回 f + +|`LNNVL(1 BETWEEN 0 AND 5)` +|拒绝 +|接受,返回 f + +|`LNNVL(1 NOT BETWEEN 0 AND 5)` +|拒绝 +|接受,返回 t + +|`LNNVL(NOT (1 = 1))` +|拒绝(ORA-00936) +|接受,返回 t + +|`LNNVL(NOT EXISTS (SELECT ...))` +|拒绝(ORA-00936) +|接受,返回 t + +|`LNNVL(1 IN (1,2))` +|拒绝(ORA-13207) +|接受,返回 f + +|`LNNVL(1 NOT IN (1,2))` +|拒绝(ORA-13207) +|接受,返回 t +|=== + +其它差异: + +[cols="1,2,2"] +|=== +|场景 |Oracle |IvorySQL + +|错误码与错误文本 +|`ORA-` 编号 +|PostgreSQL 的 SQLSTATE 与英文描述 + +|`LNNVL(1)` +|依据其 BOOLEAN 类型转换规则处理 +|报函数不存在,不做隐式转换 +|=== + +需要表达复合条件时,按 Oracle 的做法拆成多个 LNNVL 调用再用 `AND` 或 `OR` 连接: + +[source,sql] +---- +SELECT LNNVL(1 = 1) OR LNNVL(2 = 3); -- 期望:f +SELECT LNNVL(1 = 1) AND LNNVL(2 = 3); -- 期望:f +SELECT NOT LNNVL(1 = 1); -- 期望:t +---- + +注意拆分时需遵循德摩根律:`LNNVL(A AND B)` 语义上对应 +`LNNVL(A) OR LNNVL(B)`,写成 `AND` 会改变结果。 + +== 与 NOT 的比较 + +[cols="1,2,2"] +|=== +|条件求值结果 |`NOT cond` |`LNNVL(cond)` + +|TRUE +|FALSE +|FALSE + +|FALSE +|TRUE +|TRUE + +|UNKNOWN +|UNKNOWN(行被丢弃) +|TRUE(行被保留) +|=== + +两者对"不确定"的处理方向相反:`NOT` 丢弃,LNNVL 保留。 +严格而言,`NOT` 在三值逻辑下的行为是正确的;LNNVL 做的是把 +"未知视为不满足原条件"这一假设显式化,业务上通常这才是所需的结果。 + +== 相关等价写法 + +`LNNVL(cond)` 等价于 SQL 标准谓词 `cond IS NOT TRUE`。在不要求 Oracle +兼容写法的场合可直接使用后者,两者的执行计划完全相同。 + +对于 `NOT IN` 遇 NULL 的场景,PostgreSQL 的惯用解法是 `NOT EXISTS`: + +[source,sql] +---- +SELECT count(*) FROM lnnvl_test t + WHERE NOT EXISTS (SELECT 1 FROM lnnvl_bl b WHERE b.d = t.id); +---- + +该写法与 `LNNVL(id IN (...))` 结果相同且更可移植; +LNNVL 的优势在于改写成本 —— 它是就地包一层, +而 `NOT EXISTS` 需要把子查询重构成相关子查询。 diff --git a/EN/modules/ROOT/nav.adoc b/EN/modules/ROOT/nav.adoc index e4221c78..e117747f 100644 --- a/EN/modules/ROOT/nav.adoc +++ b/EN/modules/ROOT/nav.adoc @@ -31,7 +31,8 @@ ** xref:master/oracle_compatibility/compat_create_index_online.adoc[22、ONLINE Parameter for CREATE INDEX] ** xref:master/oracle_compatibility/compat_stragg.adoc[23、STRAGG function] ** xref:master/oracle_compatibility/compat_alter_index_unusable_en.adoc[24、Alter Index Unusable] -** xref:master/oracle_compatibility/compat_dbtimezone_en.adoc[24、dbtimezone] +** xref:master/oracle_compatibility/compat_dbtimezone_en.adoc[25、dbtimezone] +** xref:master/oracle_compatibility/compat_lnnvl.adoc[26、LNNVL Function] * Containerization and Cloud Service ** Containerization *** xref:master/containerization/k8s_deployment.adoc[K8S deployment] @@ -116,6 +117,7 @@ *** xref:master/oracle_builtin_functions/rawtohex.adoc[rawtohex] *** xref:master/oracle_builtin_functions/stragg.adoc[stragg] *** xref:master/oracle_builtin_functions/dbtimezone_impl_en.adoc[dbtimezone] +*** xref:master/oracle_builtin_functions/lnnvl.adoc[lnnvl] ** xref:master/gb18030.adoc[GB18030 Character Set] * Reference ** xref:master/tools_reference.adoc[Tool Reference] diff --git a/EN/modules/ROOT/pages/master/oracle_builtin_functions/lnnvl.adoc b/EN/modules/ROOT/pages/master/oracle_builtin_functions/lnnvl.adoc new file mode 100644 index 00000000..a7fb3bd3 --- /dev/null +++ b/EN/modules/ROOT/pages/master/oracle_builtin_functions/lnnvl.adoc @@ -0,0 +1,145 @@ +:sectnums: +:sectnumlevels: 5 + +:imagesdir: ./_images + += Implementation of the LNNVL Function + +== Purpose + +IvorySQL adds the `sys.lnnvl(boolean)` function in the `contrib/ivorysql_ora` +extension, reproducing the behaviour of the Oracle function of the same name: +it returns TRUE when the condition evaluates to FALSE or UNKNOWN, and FALSE +when the condition evaluates to TRUE. + +== Implementation Notes + +=== Semantically Equivalent to IS NOT TRUE + +The truth table of LNNVL is cell-for-cell that of the SQL standard +`IS NOT TRUE` predicate: + +[cols="1,1,1"] +|=== +|Condition evaluates to |`LNNVL(cond)` |`cond IS NOT TRUE` + +|TRUE +|FALSE +|FALSE + +|FALSE +|TRUE +|TRUE + +|UNKNOWN +|TRUE +|TRUE +|=== + +No new evaluation logic is therefore required; the function body is just that +predicate. In PostgreSQL, `IS NOT TRUE` is evaluated by the +`EEOP_BOOLTEST_IS_NOT_TRUE` case in +`src/backend/executor/execExprInterp.c`, which returns true and clears the +result null flag when the argument is NULL. LNNVL consequently never returns +NULL. + +=== SQL Definition + +The function is defined in +`contrib/ivorysql_ora/src/builtin_functions/builtin_functions--1.0.sql`. + +[source,sql] +---- +CREATE FUNCTION sys.lnnvl(pg_catalog.bool) +RETURNS pg_catalog.bool +AS $$SELECT $1 IS NOT TRUE$$ +LANGUAGE sql +CALLED ON NULL INPUT +PARALLEL SAFE +IMMUTABLE; +---- + +Rationale for each property: + +[cols="1,1,3"] +|=== +|Property |Value |Rationale + +|Schema +|`sys` +|Consistent with the other Oracle compatibility functions. `sys` enters the +implicit `search_path` only in Oracle compatibility mode. + +|Argument type +|`pg_catalog.bool` +|Schema-qualified so that type name resolution is unaffected when `sys` is +first in the `search_path` in Oracle mode. + +|Language +|`sql` +|Can be folded by `inline_function()`, see below. A C function would not be +inlined. + +|NULL handling +|`CALLED ON NULL INPUT` +|Mandatory, see below. + +|Volatility +|`IMMUTABLE` +|Pure logic, same input always yields the same output; also a prerequisite for +use in expression indexes. + +|Parallelism +|`PARALLEL SAFE` +|No side effects. Without this clause the default is `PARALLEL UNSAFE`, which +disables parallel plans for any query using the function. +|=== + +=== CALLED ON NULL INPUT Is Mandatory + +`CALLED ON NULL INPUT` is the SQL default; it is spelled out here to keep it +from being changed by mistake. + +Declared `STRICT`, the body would not run at all for a NULL argument and the +result would be NULL rather than true, which removes the only reason the +function exists, and it would do so silently. The risk comes from the pull +towards stylistic consistency: most neighbouring functions in +`builtin_functions--1.0.sql` are declared `STRICT`, so tidying this one to +match its neighbours introduces the bug. + +`STRICT` also defeats inlining. Declaring a function strict promises that NULL +in yields NULL out, while `IS NOT TRUE` is not itself a strict predicate, so +the planner cannot expand it while preserving semantics and has to keep the +function call: + +[source,text] +---- +-- CALLED ON NULL INPUT +Filter: (t.v IS NOT TRUE) + +-- STRICT +Filter: f_strict(t.v) +---- + +Writing `STRICT` here therefore costs twice: the semantics are wrong and +silently so, and performance degrades to a per-row function call. + +=== Function Inlining + +The body is a simple, inlinable SQL expression, so `inline_function()` folds +it into the caller: + +[source,sql] +---- +EXPLAIN (VERBOSE, COSTS OFF) SELECT id FROM t WHERE LNNVL(v >= 0.2); +---- + +[source,text] +---- + Seq Scan on public.t + Output: id + Filter: ((t.v >= 0.2) IS NOT TRUE) +---- + +No function call remains in the plan, which is identical to the plan for +writing `IS NOT TRUE` by hand, so there is no additional runtime cost. diff --git a/EN/modules/ROOT/pages/master/oracle_compatibility/compat_lnnvl.adoc b/EN/modules/ROOT/pages/master/oracle_compatibility/compat_lnnvl.adoc new file mode 100644 index 00000000..e762a337 --- /dev/null +++ b/EN/modules/ROOT/pages/master/oracle_compatibility/compat_lnnvl.adoc @@ -0,0 +1,267 @@ +:sectnums: +:sectnumlevels: 5 + +:imagesdir: ./_images + += LNNVL Function + +== Purpose + +This document describes the `sys.lnnvl` function in IvorySQL, which negates a +condition in an Oracle compatible way and still returns TRUE when the condition +evaluates to UNKNOWN. + +== Functional Description + +* `sys.lnnvl(boolean)` takes a condition and returns TRUE when it evaluates to + FALSE or UNKNOWN, and FALSE when it evaluates to TRUE. +* The function **never returns NULL**. The result is always TRUE or FALSE, so it + can be used directly as a WHERE condition without introducing a new UNKNOWN. +* Its main use is to eliminate the row loss caused by three-valued logic: with a + plain `NOT`, rows whose condition is UNKNOWN are dropped both by the original + condition and by its negation, whereas a condition and its LNNVL partition the + table strictly, so the two row counts always add up to the total. + +Truth table: + +[cols="1,1"] +|=== +|Condition evaluates to |Return value + +|TRUE +|FALSE + +|FALSE +|TRUE + +|UNKNOWN (condition involves NULL) +|TRUE +|=== + +== Syntax + +[source,sql] +---- +sys.lnnvl(condition) +---- + +[cols="1,3"] +|=== +|Parameter |Description + +|`condition` +|Any boolean expression. Non-boolean types are not accepted and there is no +implicit cast from integer. +|=== + +Return type: `boolean` + +== Test Cases + +=== Test Environment Setup + +[source,sql] +---- +CREATE TABLE lnnvl_test (a INT, b INT); +INSERT INTO lnnvl_test VALUES (2, NULL); +---- + +=== Truth Table Verification + +[source,sql] +---- +-- a = 2, b is NULL +SELECT LNNVL(a = 1) FROM lnnvl_test; -- expected: t (condition is FALSE) +SELECT LNNVL(a = 2) FROM lnnvl_test; -- expected: f (condition is TRUE) +SELECT LNNVL(a IS NULL) FROM lnnvl_test; -- expected: t (condition is FALSE) +SELECT LNNVL(b = 1) FROM lnnvl_test; -- expected: t (condition is UNKNOWN) +SELECT LNNVL(b IS NULL) FROM lnnvl_test; -- expected: f (condition is TRUE) +SELECT LNNVL(a = b) FROM lnnvl_test; -- expected: t (condition is UNKNOWN) +---- + +=== NULL Argument + +[source,sql] +---- +-- A NULL argument must return t; this is the core behaviour of LNNVL +SELECT LNNVL(NULL); -- expected: t +SELECT LNNVL(NULL::boolean); -- expected: t +---- + +=== Partition Property + +[source,sql] +---- +DROP TABLE lnnvl_test; +CREATE TABLE lnnvl_test (id INT, v NUMERIC); +INSERT INTO lnnvl_test VALUES (1,0.30),(2,0.10),(3,NULL),(4,0.25),(5,NULL); + +SELECT count(*) FROM lnnvl_test WHERE v >= 0.2; -- expected: 2 +SELECT count(*) FROM lnnvl_test WHERE LNNVL(v >= 0.2); -- expected: 3 +SELECT count(*) FROM lnnvl_test; -- expected: 5 + +-- 2 + 3 = 5: the condition and its LNNVL cover the whole table, no row is lost +-- With NOT, the two counts add up to less than the total +SELECT count(*) FROM lnnvl_test WHERE NOT (v >= 0.2); -- expected: 2, total only 4 +---- + +=== NOT IN over a Subquery Containing NULL + +[source,sql] +---- +CREATE TABLE lnnvl_bl (d INT); +INSERT INTO lnnvl_bl VALUES (1), (NULL); + +-- With a NULL in the list, NOT IN matches nothing +SELECT count(*) FROM lnnvl_test WHERE id NOT IN (SELECT d FROM lnnvl_bl); +-- expected: 0 + +-- Rewritten with LNNVL(... IN ...) it gives the intuitive result +SELECT count(*) FROM lnnvl_test WHERE LNNVL(id IN (SELECT d FROM lnnvl_bl)); +-- expected: 4 +---- + +`id NOT IN (1, NULL)` expands to `id <> 1 AND id <> NULL`, where +`id <> NULL` is always UNKNOWN, so the whole condition is never TRUE and every +row is dropped. `LNNVL(id IN (...))` treats UNKNOWN as "not true" and keeps +those rows. + +=== Argument Type Checking + +[source,sql] +---- +-- The argument must be boolean; there is no implicit cast from integer +SELECT LNNVL(1); +-- expected error: function lnnvl(pg_catalog.int4) does not exist +---- + +=== Test Environment Cleanup + +[source,sql] +---- +DROP TABLE lnnvl_bl; +DROP TABLE lnnvl_test; +---- + +== Behavioral Differences from Oracle + +Oracle's grammar accepts only a single simple condition. IvorySQL implements +LNNVL as an ordinary function, and by the time overload resolution runs its +argument is already a boolean expression: the `AND`, `OR`, `NOT` or `BETWEEN` +that produced it is no longer visible to the function. The restrictions +therefore cannot be reproduced, and **the accepted forms are a superset of +Oracle's**. + +The direction is safe: any LNNVL usage that runs on Oracle also runs on +IvorySQL, but not the reverse -- a compound form written on IvorySQL will raise +an error when moved back to Oracle. + +[cols="2,1,1"] +|=== +|Argument form |Oracle |IvorySQL + +|`LNNVL(1 = 1 AND 2 = 2)` +|rejected +|accepted, returns f + +|`LNNVL(1 = 1 OR 2 = 2)` +|rejected +|accepted, returns f + +|`LNNVL(1 BETWEEN 0 AND 5)` +|rejected +|accepted, returns f + +|`LNNVL(1 NOT BETWEEN 0 AND 5)` +|rejected +|accepted, returns t + +|`LNNVL(NOT (1 = 1))` +|rejected (ORA-00936) +|accepted, returns t + +|`LNNVL(NOT EXISTS (SELECT ...))` +|rejected (ORA-00936) +|accepted, returns t + +|`LNNVL(1 IN (1,2))` +|rejected (ORA-13207) +|accepted, returns f + +|`LNNVL(1 NOT IN (1,2))` +|rejected (ORA-13207) +|accepted, returns t +|=== + +Other differences: + +[cols="1,2,2"] +|=== +|Aspect |Oracle |IvorySQL + +|Error codes and text +|`ORA-` numbers +|PostgreSQL SQLSTATE values and wording + +|`LNNVL(1)` +|Handled by its BOOLEAN type conversion rules +|Reports that no such function exists; no implicit cast +|=== + +To express a compound condition, follow the Oracle practice of splitting it +into several LNNVL calls combined with `AND` or `OR`: + +[source,sql] +---- +SELECT LNNVL(1 = 1) OR LNNVL(2 = 3); -- expected: f +SELECT LNNVL(1 = 1) AND LNNVL(2 = 3); -- expected: f +SELECT NOT LNNVL(1 = 1); -- expected: t +---- + +Note that De Morgan's laws apply when splitting: `LNNVL(A AND B)` corresponds to +`LNNVL(A) OR LNNVL(B)`; using `AND` changes the result. + +== Comparison with NOT + +[cols="1,2,2"] +|=== +|Condition evaluates to |`NOT cond` |`LNNVL(cond)` + +|TRUE +|FALSE +|FALSE + +|FALSE +|TRUE +|TRUE + +|UNKNOWN +|UNKNOWN (row dropped) +|TRUE (row kept) +|=== + +The two treat "unknown" in opposite directions: `NOT` discards it, LNNVL keeps +it. Strictly speaking the behaviour of `NOT` is correct under three-valued +logic; what LNNVL does is make the assumption "unknown means the original +condition is not satisfied" explicit, which is usually the intended result in +practice. + +== Equivalent Formulations + +`LNNVL(cond)` is equivalent to the SQL standard predicate +`cond IS NOT TRUE`. Where Oracle compatible syntax is not required, the latter +can be used directly; the execution plans are identical. + +For the NOT IN with NULL case, the idiomatic PostgreSQL solution is +`NOT EXISTS`: + +[source,sql] +---- +SELECT count(*) FROM lnnvl_test t + WHERE NOT EXISTS (SELECT 1 FROM lnnvl_bl b WHERE b.d = t.id); +---- + +That form gives the same result as `LNNVL(id IN (...))` and is more portable. +The advantage of LNNVL is the cost of rewriting: it wraps an existing condition +in place, whereas `NOT EXISTS` requires restructuring the subquery into a +correlated one.