diff --git a/CN/modules/ROOT/nav.adoc b/CN/modules/ROOT/nav.adoc index 7ed31aff..2ca59ab1 100644 --- a/CN/modules/ROOT/nav.adoc +++ b/CN/modules/ROOT/nav.adoc @@ -32,6 +32,7 @@ ** 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/nanvl.adoc[26、NANVL 函数] * 容器化与云服务 ** 容器化指南 *** xref:master/containerization/k8s_deployment.adoc[K8S部署] @@ -119,6 +120,7 @@ **** xref:master/oracle_builtin_functions/stragg.adoc[stragg] **** xref:master/oracle_builtin_functions/dbtimezone_impl.adoc[dbtimezone] **** xref:master/oracle_builtin_functions/vsize.adoc[vsize] +**** xref:master/oracle_builtin_functions/nanvl.adoc[nanvl] *** xref:master/gb18030.adoc[国标GB18030] * 参考指南 ** xref:master/tools_reference.adoc[工具参考] diff --git a/CN/modules/ROOT/pages/master/oracle_builtin_functions/nanvl.adoc b/CN/modules/ROOT/pages/master/oracle_builtin_functions/nanvl.adoc new file mode 100644 index 00000000..d3f191e3 --- /dev/null +++ b/CN/modules/ROOT/pages/master/oracle_builtin_functions/nanvl.adoc @@ -0,0 +1,122 @@ +:sectnums: +:sectnumlevels: 5 + += NANVL 函数设计 + +== 背景 + +=== Oracle 语义 + +Oracle 提供 `NANVL(n, m)` 函数,用于处理浮点数中的 NaN(Not a Number)值: + +* 若 `n` 为 NaN,则返回替代值 `m`。 +* 若 `n` 不是 NaN,则返回 `n`。 +* 适用于 `BINARY_FLOAT` 和 `BINARY_DOUBLE` 类型。 + +=== 目的 + +NaN 是 IEEE 754 浮点标准定义的特殊值,在数据导入、科学计算和 ETL 场景中较为常见。若不处理 NaN,可能导致: + +* 聚合运算结果变为 NaN +* 比较运算行为异常(NaN ≠ NaN) +* 索引和约束检查失效 + +`NANVL` 以声明方式将 NaN 替换为有效数值(如 0、-1 或均值),是将 Oracle 应用迁移到 IvorySQL 时需要的兼容函数。 + +== 架构设计 + +=== 设计选择 + +`NANVL` 使用 C 函数实现。该函数不需要特殊语法规则,可以作为普通函数调用由解析器识别。通过 `CREATE FUNCTION` 注册为普通 SQL 函数后,PostgreSQL 的标准函数查找机制可以自动解析重载。 + +=== 代码组织 + +[source,text] +---- +contrib/ivorysql_ora/ +├── src/builtin_functions/ +│ ├── builtin_functions--1.0.sql -- SQL 注册 +│ └── numeric_datatype_functions.c -- C 函数实现 +├── sql/ +│ └── ora_nanvl.sql -- 回归测试 SQL +├── expected/ +│ └── ora_nanvl.out -- 期望输出 +└── Makefile -- 在 ORA_REGRESS 列表中添加 ora_nanvl +---- + +=== 函数属性 + +[cols="2,1,4",options="header"] +|=== +|属性 |值 |原因 +|`IMMUTABLE` |是 |纯计算函数,相同输入始终返回相同输出 +|`PARALLEL SAFE` |是 |无副作用,可以安全地用于并行查询计划 +|=== + +== 实现细节 + +=== binary_float 和 binary_double 实现 + +[source,c] +---- +Datum +binary_float_nanvl(PG_FUNCTION_ARGS) +{ + float4 arg1; + + if (PG_ARGISNULL(0)) + PG_RETURN_NULL(); + + arg1 = PG_GETARG_FLOAT4(0); + + if (!isnan(arg1)) + PG_RETURN_FLOAT4(arg1); + + if (PG_ARGISNULL(1)) + PG_RETURN_NULL(); + + PG_RETURN_FLOAT4(PG_GETARG_FLOAT4(1)); +} +---- + +* `BINARY_FLOAT` 底层为 `float4`(4 字节 IEEE 754 单精度),`BINARY_DOUBLE` 底层为 `float8`(8 字节双精度)。`binary_double_nanvl` 的结构相同,仅将 `float4` 和 `FLOAT4` 替换为 `float8` 和 `FLOAT8`。 +* 使用标准 C 库 `` 中的 `isnan()` 检测 NaN。 + +=== number 类型实现 + +[source,c] +---- +Datum +number_nanvl(PG_FUNCTION_ARGS) +{ + Numeric arg1; + + if (PG_ARGISNULL(0)) + PG_RETURN_NULL(); + + arg1 = PG_GETARG_NUMERIC(0); + + if (!numeric_is_nan(arg1)) + PG_RETURN_NUMERIC(arg1); + + if (PG_ARGISNULL(1)) + PG_RETURN_NULL(); + + PG_RETURN_NUMERIC(PG_GETARG_NUMERIC(1)); +} +---- + +* Oracle 的 `NUMBER` 类型映射到 PostgreSQL 的 `Numeric`。 +* PostgreSQL 的 `Numeric` 可以表示 NaN;Oracle 的 `NUMBER` 不支持 NaN。 +* 复用内核函数 `numeric_is_nan()`,该函数声明在 `utils/numeric.h` 中,定义在 `utils/adt/numeric.c` 中。 +* `numeric_is_nan()` 内部使用 `NUMERIC_IS_NAN()` 宏。 + +=== NULL 处理 + +`NANVL` 的语义仅依赖第一个参数 `n`: + +. 若 `n` 为 NULL,直接返回 NULL,不读取 `m`。 +. 若 `n` 不是 NULL 且不是 NaN,直接返回 `n`。此时 `m` 不参与运算,即使 `m` 为 NULL 也不影响结果。 +. 只有 `n` 为 NaN 时才使用 `m` 作为返回值;此时若 `m` 为 NULL,结果才是 NULL。 + +因此,三个重载都不使用 `STRICT`,而是在函数体中使用 `PG_ARGISNULL()` 显式处理上述情况:先判断 `n` 是否为 NULL,再判断其是否为 NaN,仅在需要返回 `m` 时检查 `m` 是否为 NULL。`numeric`、`float4` 和 `float8` 三种类型的判空逻辑一致,仅 NaN 检测方式不同。 diff --git a/CN/modules/ROOT/pages/master/oracle_compatibility/nanvl.adoc b/CN/modules/ROOT/pages/master/oracle_compatibility/nanvl.adoc new file mode 100644 index 00000000..050bc01f --- /dev/null +++ b/CN/modules/ROOT/pages/master/oracle_compatibility/nanvl.adoc @@ -0,0 +1,177 @@ +:sectnums: +:sectnumlevels: 5 + += NANVL 函数 + +== 概述 + +`NANVL` 是 Oracle 兼容函数,用于替换浮点数中的 NaN(Not a Number)值。当输入为 NaN 时返回指定的替代值,否则返回输入值本身。 + +== 语法 + +[source,sql] +---- +NANVL(n, m) +---- + +=== 参数 + +[cols="1,4",options="header"] +|=== +|参数 |说明 +|`n` |待检测的数值表达式(`binary_float`、`binary_double`) +|`m` |替代值,类型需与 `n` 兼容 +|=== + +=== 返回值 + +* 若 `n` 为 NaN,返回 `m`。 +* 若 `n` 不是 NaN,返回 `n`。 + +== 使用示例 + +=== 基本用法 + +[source,sql] +---- +-- 正常值:返回原值 +SELECT NANVL(CAST(1.5 AS BINARY_FLOAT), CAST(99.0 AS BINARY_FLOAT)); +-- 结果:1.5 + +-- NaN 值:返回替代值 +SELECT NANVL(CAST('NaN' AS BINARY_FLOAT), CAST(99.0 AS BINARY_FLOAT)); +-- 结果:99 + +-- 负数正常值:返回原值 +SELECT NANVL(CAST(-3.14 AS BINARY_DOUBLE), CAST(0.0 AS BINARY_DOUBLE)); +-- 结果:-3.14 +---- + +=== 数据清洗 + +将表中的 NaN 值替换为 0: + +[source,sql] +---- +CREATE TABLE measurements ( + id int, + temperature binary_double, + pressure binary_double +); + +INSERT INTO measurements VALUES + (1, 25.5, 1013.25), + (2, 'NaN', 1015.0), -- 传感器故障,温度 NaN + (3, 26.1, 'NaN'), -- 传感器故障,气压 NaN + (4, 'NaN', 'NaN'); -- 两个传感器都故障 + +-- 将 NaN 替换为 0 +SELECT id, + NANVL(temperature, 0.0) AS temp_clean, + NANVL(pressure, 0.0) AS press_clean +FROM measurements +ORDER BY id; + +-- 结果: +-- id | temp_clean | press_clean +-- ----+------------+------------- +-- 1 | 25.5 | 1013.25 +-- 2 | 0 | 1015.0 +-- 3 | 26.1 | 0 +-- 4 | 0 | 0 +---- + +=== 使用表达式作为替代值 + +替代值可以是任意表达式: + +[source,sql] +---- +-- 用列的平均值替代 NaN +SELECT NANVL(temperature, + (SELECT AVG(temperature) FROM measurements)) +FROM measurements; + +-- 用计算结果替代 NaN +SELECT NANVL('NaN'::binary_float, 1.0 + 2.0::binary_float); +-- 结果:3 +---- + +=== 配合聚合使用 + +[source,sql] +---- +-- 计算温度时,先将 NaN 替换为 0 再求平均 +SELECT AVG(NANVL(temperature, 0.0)) AS avg_temp +FROM measurements; +---- + +=== 在 WHERE 子句中使用 + +[source,sql] +---- +-- 筛选出“原始值为 NaN 或小于阈值”的行 +SELECT * +FROM measurements +WHERE NANVL(temperature, -999.0) < 0; +---- + +== 边界行为 + +=== NULL 处理 + +`NANVL` 根据 `n` 决定返回值。当 `n` 不是 NaN 时直接返回 `n`,`m` 的值不影响结果。只有 `n` 为 NaN 时才会使用 `m`;此时若 `m` 为 NULL,则返回 NULL。 + +[source,sql] +---- +SELECT NANVL(CAST(NULL AS BINARY_FLOAT), CAST(99.0 AS BINARY_FLOAT)); +-- n 为 NULL,直接返回 NULL + +SELECT NANVL(CAST(1.5 AS BINARY_FLOAT), CAST(NULL AS BINARY_FLOAT)); +-- 返回 1.5;n 不是 NaN,m 是否为 NULL 不影响结果 + +SELECT NANVL(CAST(NULL AS BINARY_FLOAT), CAST(NULL AS BINARY_FLOAT)); +-- n 为 NULL,返回 NULL + +SELECT NANVL(CAST('NaN' AS BINARY_FLOAT), CAST(NULL AS BINARY_FLOAT)); +-- n 是 NaN,返回 m;m 为 NULL,因此返回 NULL +---- + +=== Infinity 不被替换 + +Infinity(无穷大)不是 NaN,不会被替换: + +[source,sql] +---- +SELECT NANVL(CAST('Infinity' AS BINARY_FLOAT), CAST(99.0 AS BINARY_FLOAT)); +-- 结果:Inf + +SELECT NANVL(CAST('-Infinity' AS BINARY_DOUBLE), CAST(0.0 AS BINARY_DOUBLE)); +-- 结果:-Inf +---- + +=== 负零不被替换 + +负零(`-0.0`)是有效的浮点数值,不是 NaN: + +[source,sql] +---- +SELECT NANVL(CAST(-0.0 AS BINARY_FLOAT), CAST(99.0 AS BINARY_FLOAT)); +-- 结果:0 + +SELECT NANVL(CAST(-0.0 AS NUMBER), CAST(99.0 AS NUMBER)); +-- 结果:0.0 +---- + +=== NaN 作为替代值 + +替代值本身也可以是 NaN: + +[source,sql] +---- +SELECT NANVL( + CAST('NaN' AS BINARY_FLOAT), + CAST('NaN' AS BINARY_FLOAT) +); +-- 结果:NaN +---- diff --git a/EN/modules/ROOT/nav.adoc b/EN/modules/ROOT/nav.adoc index 594bda81..d1d49188 100644 --- a/EN/modules/ROOT/nav.adoc +++ b/EN/modules/ROOT/nav.adoc @@ -32,6 +32,7 @@ ** 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/nanvl.adoc[25、NANVL function] * Containerization and Cloud Service ** Containerization *** xref:master/containerization/k8s_deployment.adoc[K8S deployment] @@ -119,6 +120,7 @@ *** xref:master/oracle_builtin_functions/stragg.adoc[stragg] *** xref:master/oracle_builtin_functions/dbtimezone_impl_en.adoc[dbtimezone] *** xref:master/oracle_builtin_functions/vsize_en.adoc[vsize] +*** xref:master/oracle_builtin_functions/nanvl.adoc[nanvl] ** 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/nanvl.adoc b/EN/modules/ROOT/pages/master/oracle_builtin_functions/nanvl.adoc new file mode 100644 index 00000000..7e86a7ec --- /dev/null +++ b/EN/modules/ROOT/pages/master/oracle_builtin_functions/nanvl.adoc @@ -0,0 +1,122 @@ +:sectnums: +:sectnumlevels: 5 + += NANVL function design + +== Background + +=== Oracle semantics + +Oracle provides the `NANVL(n, m)` function for handling NaN (Not a Number) floating-point values: + +* If `n` is NaN, the function returns the substitute `m`. +* If `n` is not NaN, the function returns `n`. +* The function supports the `BINARY_FLOAT` and `BINARY_DOUBLE` types. + +=== Purpose + +NaN is a special value defined by the IEEE 754 floating-point standard. It commonly occurs during data import, scientific computing, and ETL processing. Leaving NaN values untreated can cause: + +* Aggregate results to become NaN +* Unexpected comparison behavior (NaN ≠ NaN) +* Index and constraint checks to fail + +`NANVL` provides a declarative way to replace NaN with a valid numeric value, such as zero, -1, or an average. It is needed for migrating Oracle applications to IvorySQL. + +== Architecture + +=== Design choice + +`NANVL` is implemented as a C function. It requires no special syntax and can be parsed as an ordinary function call. After it is registered as a regular SQL function with `CREATE FUNCTION`, PostgreSQL's standard function lookup resolves its overloads automatically. + +=== Code organization + +[source,text] +---- +contrib/ivorysql_ora/ +├── src/builtin_functions/ +│ ├── builtin_functions--1.0.sql -- SQL registration +│ └── numeric_datatype_functions.c -- C function implementation +├── sql/ +│ └── ora_nanvl.sql -- Regression test SQL +├── expected/ +│ └── ora_nanvl.out -- Expected output +└── Makefile -- Add ora_nanvl to the ORA_REGRESS list +---- + +=== Function attributes + +[cols="2,1,4",options="header"] +|=== +|Attribute |Value |Reason +|`IMMUTABLE` |Yes |A pure calculation always returns the same result for the same inputs +|`PARALLEL SAFE` |Yes |The function has no side effects and is safe for parallel query plans +|=== + +== Implementation details + +=== binary_float and binary_double implementation + +[source,c] +---- +Datum +binary_float_nanvl(PG_FUNCTION_ARGS) +{ + float4 arg1; + + if (PG_ARGISNULL(0)) + PG_RETURN_NULL(); + + arg1 = PG_GETARG_FLOAT4(0); + + if (!isnan(arg1)) + PG_RETURN_FLOAT4(arg1); + + if (PG_ARGISNULL(1)) + PG_RETURN_NULL(); + + PG_RETURN_FLOAT4(PG_GETARG_FLOAT4(1)); +} +---- + +* `BINARY_FLOAT` is represented by `float4` (4-byte IEEE 754 single precision), while `BINARY_DOUBLE` is represented by `float8` (8-byte double precision). `binary_double_nanvl` has the same structure, with `float4` and `FLOAT4` replaced by `float8` and `FLOAT8`. +* NaN is detected with `isnan()` from the standard C library header ``. + +=== number implementation + +[source,c] +---- +Datum +number_nanvl(PG_FUNCTION_ARGS) +{ + Numeric arg1; + + if (PG_ARGISNULL(0)) + PG_RETURN_NULL(); + + arg1 = PG_GETARG_NUMERIC(0); + + if (!numeric_is_nan(arg1)) + PG_RETURN_NUMERIC(arg1); + + if (PG_ARGISNULL(1)) + PG_RETURN_NULL(); + + PG_RETURN_NUMERIC(PG_GETARG_NUMERIC(1)); +} +---- + +* Oracle's `NUMBER` type maps to PostgreSQL's `Numeric` type. +* PostgreSQL `Numeric` can represent NaN, while Oracle `NUMBER` does not support NaN. +* The implementation reuses the core `numeric_is_nan()` function, declared in `utils/numeric.h` and defined in `utils/adt/numeric.c`. +* `numeric_is_nan()` uses the `NUMERIC_IS_NAN()` macro internally. + +=== NULL handling + +The semantics of `NANVL` depend initially on the first argument, `n`: + +. If `n` is NULL, return NULL without reading `m`. +. If `n` is neither NULL nor NaN, return `n`. In this case, `m` does not participate in the operation, and a NULL `m` does not affect the result. +. Only when `n` is NaN is `m` used as the return value. The result is NULL only if `m` is NULL in this case. + +The three overloads therefore omit `STRICT` and use `PG_ARGISNULL()` in the function body to handle these cases explicitly. Each function first checks whether `n` is NULL, then whether it is NaN, and checks whether `m` is NULL only when returning `m`. The `numeric`, `float4`, and `float8` overloads use the same NULL-handling logic and differ only in how they detect NaN. diff --git a/EN/modules/ROOT/pages/master/oracle_compatibility/nanvl.adoc b/EN/modules/ROOT/pages/master/oracle_compatibility/nanvl.adoc new file mode 100644 index 00000000..235c4637 --- /dev/null +++ b/EN/modules/ROOT/pages/master/oracle_compatibility/nanvl.adoc @@ -0,0 +1,177 @@ +:sectnums: +:sectnumlevels: 5 + += NANVL function + +== Overview + +`NANVL` is an Oracle-compatible function that replaces a NaN (Not a Number) floating-point value. It returns the specified substitute when the input is NaN and returns the input value otherwise. + +== Syntax + +[source,sql] +---- +NANVL(n, m) +---- + +=== Parameters + +[cols="1,4",options="header"] +|=== +|Parameter |Description +|`n` |Numeric expression to inspect (`binary_float` or `binary_double`) +|`m` |Substitute value whose type must be compatible with `n` +|=== + +=== Return value + +* If `n` is NaN, the function returns `m`. +* If `n` is not NaN, the function returns `n`. + +== Examples + +=== Basic usage + +[source,sql] +---- +-- A regular value is returned unchanged +SELECT NANVL(CAST(1.5 AS BINARY_FLOAT), CAST(99.0 AS BINARY_FLOAT)); +-- Result: 1.5 + +-- A NaN value is replaced +SELECT NANVL(CAST('NaN' AS BINARY_FLOAT), CAST(99.0 AS BINARY_FLOAT)); +-- Result: 99 + +-- A regular negative value is returned unchanged +SELECT NANVL(CAST(-3.14 AS BINARY_DOUBLE), CAST(0.0 AS BINARY_DOUBLE)); +-- Result: -3.14 +---- + +=== Data cleansing + +Replace NaN values in a table with zero: + +[source,sql] +---- +CREATE TABLE measurements ( + id int, + temperature binary_double, + pressure binary_double +); + +INSERT INTO measurements VALUES + (1, 25.5, 1013.25), + (2, 'NaN', 1015.0), -- Temperature sensor failure + (3, 26.1, 'NaN'), -- Pressure sensor failure + (4, 'NaN', 'NaN'); -- Both sensors failed + +-- Replace NaN with zero +SELECT id, + NANVL(temperature, 0.0) AS temp_clean, + NANVL(pressure, 0.0) AS press_clean +FROM measurements +ORDER BY id; + +-- Result: +-- id | temp_clean | press_clean +-- ----+------------+------------- +-- 1 | 25.5 | 1013.25 +-- 2 | 0 | 1015.0 +-- 3 | 26.1 | 0 +-- 4 | 0 | 0 +---- + +=== Using an expression as the substitute + +The substitute can be any expression: + +[source,sql] +---- +-- Replace NaN with the column average +SELECT NANVL(temperature, + (SELECT AVG(temperature) FROM measurements)) +FROM measurements; + +-- Replace NaN with a calculated value +SELECT NANVL('NaN'::binary_float, 1.0 + 2.0::binary_float); +-- Result: 3 +---- + +=== Using NANVL with an aggregate + +[source,sql] +---- +-- Replace NaN with zero before calculating the average temperature +SELECT AVG(NANVL(temperature, 0.0)) AS avg_temp +FROM measurements; +---- + +=== Using NANVL in a WHERE clause + +[source,sql] +---- +-- Select rows whose original value is NaN or below the threshold +SELECT * +FROM measurements +WHERE NANVL(temperature, -999.0) < 0; +---- + +== Boundary behavior + +=== NULL handling + +`NANVL` determines its return value from `n`. If `n` is not NaN, the function returns `n`, and the value of `m` does not affect the result. The function uses `m` only when `n` is NaN; if `m` is NULL in that case, the function returns NULL. + +[source,sql] +---- +SELECT NANVL(CAST(NULL AS BINARY_FLOAT), CAST(99.0 AS BINARY_FLOAT)); +-- n is NULL, so the function returns NULL immediately + +SELECT NANVL(CAST(1.5 AS BINARY_FLOAT), CAST(NULL AS BINARY_FLOAT)); +-- Returns 1.5; because n is not NaN, a NULL m does not affect the result + +SELECT NANVL(CAST(NULL AS BINARY_FLOAT), CAST(NULL AS BINARY_FLOAT)); +-- n is NULL, so the function returns NULL + +SELECT NANVL(CAST('NaN' AS BINARY_FLOAT), CAST(NULL AS BINARY_FLOAT)); +-- n is NaN, so the function returns m; m is NULL, so the result is NULL +---- + +=== Infinity is not replaced + +Infinity is not NaN and is therefore not replaced: + +[source,sql] +---- +SELECT NANVL(CAST('Infinity' AS BINARY_FLOAT), CAST(99.0 AS BINARY_FLOAT)); +-- Result: Inf + +SELECT NANVL(CAST('-Infinity' AS BINARY_DOUBLE), CAST(0.0 AS BINARY_DOUBLE)); +-- Result: -Inf +---- + +=== Negative zero is not replaced + +Negative zero (`-0.0`) is a valid floating-point value, not NaN: + +[source,sql] +---- +SELECT NANVL(CAST(-0.0 AS BINARY_FLOAT), CAST(99.0 AS BINARY_FLOAT)); +-- Result: 0 + +SELECT NANVL(CAST(-0.0 AS NUMBER), CAST(99.0 AS NUMBER)); +-- Result: 0.0 +---- + +=== Using NaN as the substitute + +The substitute value can itself be NaN: + +[source,sql] +---- +SELECT NANVL( + CAST('NaN' AS BINARY_FLOAT), + CAST('NaN' AS BINARY_FLOAT) +); +-- Result: NaN +----