From bbde4df6a445b1ccac91dd3164f5c0576a3003ee Mon Sep 17 00:00:00 2001 From: JiaoShuntian Date: Mon, 21 Sep 2026 13:47:51 +0800 Subject: [PATCH] docs: add UTL_ENCODE documentation --- CN/modules/ROOT/nav.adoc | 2 + .../utl_encode.adoc | 245 ++++++++++++++++++ .../oracle_compatibility/utl_encode.adoc | 245 ++++++++++++++++++ EN/modules/ROOT/nav.adoc | 2 + .../utl_encode.adoc | 245 ++++++++++++++++++ .../oracle_compatibility/utl_encode.adoc | 245 ++++++++++++++++++ 6 files changed, 984 insertions(+) create mode 100644 CN/modules/ROOT/pages/master/compatibility_features_design/utl_encode.adoc create mode 100644 CN/modules/ROOT/pages/master/oracle_compatibility/utl_encode.adoc create mode 100644 EN/modules/ROOT/pages/master/compatibility_features_design/utl_encode.adoc create mode 100644 EN/modules/ROOT/pages/master/oracle_compatibility/utl_encode.adoc diff --git a/CN/modules/ROOT/nav.adoc b/CN/modules/ROOT/nav.adoc index 7ed31aff..2724e685 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/utl_encode.adoc[26、UTL_ENCODE] * 容器化与云服务 ** 容器化指南 *** xref:master/containerization/k8s_deployment.adoc[K8S部署] @@ -112,6 +113,7 @@ **** 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/compatibility_features_design/utl_encode.adoc[UTL_ENCODE] *** 内置函数 **** 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/utl_encode.adoc b/CN/modules/ROOT/pages/master/compatibility_features_design/utl_encode.adoc new file mode 100644 index 00000000..0fd5b3a3 --- /dev/null +++ b/CN/modules/ROOT/pages/master/compatibility_features_design/utl_encode.adoc @@ -0,0 +1,245 @@ +:sectnums: +:sectnumlevels: 5 + += UTL_ENCODE 包实现原理 + +== 概述 + +`UTL_ENCODE` 是 IvorySQL Oracle 兼容扩展(`ivorysql_ora`)中的内置包,提供与 Oracle 数据库兼容的 Base64 编码与解码功能。本文档描述 `BASE64_ENCODE` 和 `BASE64_DECODE` 两个函数的设计目标、实现原理及关键技术细节。 + +== 文件结构 + +[source,text] +---- +contrib/ivorysql_ora/ +├── src/builtin_packages/utl_encode/ +│ ├── utl_encode.c # C 函数实现 +│ └── utl_encode--1.0.sql # SQL 注册和 PL/iSQL 包声明 +├── sql/utl_encode.sql # 回归测试用例 +└── expected/utl_encode.out # 回归测试期望输出 +---- + +== Oracle 兼容目标 + +Oracle 的 `UTL_ENCODE` 包定义以下接口: + +[source,sql] +---- +-- 编码:将二进制 RAW 数据转换为 Base64 ASCII 字节序列 +UTL_ENCODE.BASE64_ENCODE(r IN RAW) RETURN RAW + +-- 解码:将 Base64 ASCII 字节序列还原为二进制 RAW 数据 +UTL_ENCODE.BASE64_DECODE(r IN RAW) RETURN RAW +---- + +在 IvorySQL 中,Oracle 的 `RAW` 类型映射为 PostgreSQL 的 `bytea`,因此两个函数的 C 签名均为 `bytea -> bytea`。 + +== BASE64_ENCODE 实现原理 + +=== 设计目标 + +Oracle `BASE64_ENCODE` 采用 RFC 1521(MIME)格式:每 64 个 Base64 字符后插入一个换行符(`\n`,即 LF),包括最后一行。 + +PostgreSQL 内置的 `encode(bytea, 'base64')` 遵循 RFC 2045,每 76 个字符换行,且末尾不保证有换行符。因此,需要自行实现换行逻辑。 + +=== 编码流程 + +[source,text] +---- +输入 bytea(src_len 字节) + │ + ▼ +pg_b64_encode() ← PostgreSQL 内部函数,输出纯 Base64(无换行) + │ + ▼ +raw_b64(b64_len 字节) ← 长度 = ceil(src_len / 3) × 4 + │ + ▼ +按 64 字符分块,每块追加 '\n' + │ + ▼ +输出 bytea(b64_len + num_lines 字节) +---- + +=== 输出长度计算 + +[cols="2,3",options="header"] +|=== +|参数 |公式 +|纯 Base64 长度 |`b64_len = pg_b64_enc_len(src_len) = ceil(src_len / 3) × 4` +|行数 |`num_lines = ceil(b64_len / 64)` +|最终输出长度 |`result_len = b64_len + num_lines` +|=== + +例如,编码 `Hello`(5 字节)时: + +* `b64_len` = 8(`SGVsbG8=`) +* `num_lines` = 1(8 ≤ 64) +* `result_len` = 8 + 1 = 9 字节(`SGVsbG8=\n`) + +边界情况如下: + +[cols="1,1,1,1",options="header"] +|=== +|输入字节数 |`b64_len` |行数 |输出字节数 +|48 |64 |1 |65 +|49 |68 |2 |70 +|96 |128 |2 |130 +|=== + +=== 关键代码 + +[source,c] +---- +/* 计算纯 Base64 长度,再计算行数 */ +b64_len = pg_b64_enc_len(src_len); +num_lines = (b64_len + 63) / 64; +result_len = b64_len + num_lines; + +/* 调用 PostgreSQL 内部编码函数(不含换行) */ +encoded_len = pg_b64_encode(src_data, src_len, raw_b64, b64_len); + +/* 按 64 字符切块,逐块写入并追加 LF */ +while (remaining > 0) +{ + chunk = (remaining >= 64) ? 64 : remaining; + memcpy(dst, p, chunk); + dst += chunk; + p += chunk; + remaining -= chunk; + *dst++ = '\n'; +} +---- + +=== 边界行为 + +[cols="2,3",options="header"] +|=== +|输入 |输出 +|`NULL` |`NULL`,由 SQL 层的 `STRICT` 修饰符处理 +|空 `bytea`(0 字节) |空 `bytea`(0 字节) +|任意非空二进制 |Base64 文本,每 64 个字符一行,末行也有 `\n` +|=== + +== BASE64_DECODE 实现原理 + +=== 设计目标 + +`BASE64_DECODE` 接受 `BASE64_ENCODE` 产生的带换行 Base64 字节序列,并将其还原为原始二进制数据。PostgreSQL 内部的 `pg_b64_decode()` 拒绝所有空白字符,而 Oracle 编码输出中包含 `\n`,因此必须在解码前剥离空白。 + +=== 解码流程 + +[source,text] +---- +输入 bytea(含 \n 的 Base64 字节序列) + │ + ▼ +剥离空白字符 +过滤 '\n'、'\r'、'\t' 和空格 + │ + ▼ +clean_buf(无空白的纯 Base64 字符) + │ + ▼ +clean_len == 0? ── 是 ──▶ 返回空 bytea + │ 否 + ▼ +pg_b64_decode() ← PostgreSQL 内部函数 + │ + ▼ +decoded_len < 0? ── 是 ──▶ ERROR: invalid base64 input + │ 否 + ▼ +输出 bytea(decoded_len 字节) +---- + +=== 空白剥离策略 + +接受的空白字符包括 `\n`(LF)、`\r`(CR)、`\t`(TAB)和空格。该设计同时兼容: + +* Oracle `BASE64_ENCODE` 输出的 `\n` 换行 +* Windows 风格的 `\r\n` 换行 +* 人工格式化时引入的 TAB 和空格 + +[source,c] +---- +for (i = 0; i < src_len; i++) +{ + unsigned char c = (unsigned char) src_data[i]; + + if (c != '\n' && c != '\r' && c != '\t' && c != ' ') + clean_buf[clean_len++] = src_data[i]; +} +---- + +=== 错误处理 + +[cols="2,3",options="header"] +|=== +|情形 |行为 +|`NULL` 输入 |返回 `NULL`,由 `STRICT` 修饰符处理 +|空 `bytea` 输入 |返回空 `bytea` +|纯空白输入(如 `\x0a0d200a`) |返回空 `bytea` +|无效 Base64 字符 |抛出 `ERROR: UTL_ENCODE.BASE64_DECODE: invalid base64 input`,错误码为 `ERRCODE_INVALID_PARAMETER_VALUE` +|=== + +== PL/iSQL 包封装 + +C 函数注册在 `sys` 模式中,再由 PL/iSQL 包封装,对外提供 Oracle 风格的调用接口: + +[source,sql] +---- +-- 在 sys 模式中注册 C 函数 +CREATE FUNCTION sys.utl_encode_base64_encode(bytea) RETURNS bytea + AS 'MODULE_PATHNAME', 'ivorysql_utl_encode_base64_encode' + LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION sys.utl_encode_base64_decode(bytea) RETURNS bytea + AS 'MODULE_PATHNAME', 'ivorysql_utl_encode_base64_decode' + LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +-- 对外提供接口的 PL/iSQL 包 +CREATE PACKAGE utl_encode AS + FUNCTION base64_encode(r IN RAW) RETURN RAW; + FUNCTION base64_decode(r IN RAW) RETURN RAW; +END utl_encode; + +CREATE PACKAGE BODY utl_encode AS + FUNCTION base64_encode(r IN RAW) RETURN RAW IS + BEGIN RETURN utl_encode_base64_encode(r); END; + + FUNCTION base64_decode(r IN RAW) RETURN RAW IS + BEGIN RETURN utl_encode_base64_decode(r); END; +END utl_encode; +---- + +调用链路为:`utl_encode.base64_encode(r)` → PL/iSQL 包体 → `sys.utl_encode_base64_encode(bytea)` → C 函数。 + +== 与 PostgreSQL 内置函数的差异 + +[cols="2,2,2",options="header"] +|=== +|特性 |PostgreSQL `encode(x, 'base64')` |Oracle `UTL_ENCODE.BASE64_ENCODE` +|换行标准 |RFC 2045(76 字符/行) |RFC 1521(64 字符/行) +|末行换行 |无 |有(`\n`) +|输入和输出类型 |`bytea` → `text` |`RAW` → `RAW`(均为 `bytea`) +|解码时空白处理 |`decode()` 接受换行 |`pg_b64_decode()` 不接受,需预处理 +|=== + +== 回归测试覆盖 + +测试文件为 `contrib/ivorysql_ora/sql/utl_encode.sql`。 + +[cols="2,3",options="header"] +|=== +|测试类型 |覆盖内容 +|NULL 边界 |NULL 输入返回 NULL +|空输入边界 |0 字节 `bytea` 返回 0 字节 `bytea` +|已知值验证 |`Hello` 编码结果为 `SGVsbG8=\n`(9 字节) +|行断边界 |48 字节 → 1 行 65 字节;49 字节 → 2 行 70 字节 +|大输入 |200 字节数据的多行编解码 +|往返一致性 |`decode(encode(x)) = x` +|CRLF 兼容 |正确剥离 `\r\n` 换行 +|纯空白输入 |`\x0a0d200a` 解码后返回空 `bytea` +|PL/iSQL 接口 |通过包调用验证端到端路径 +|=== diff --git a/CN/modules/ROOT/pages/master/oracle_compatibility/utl_encode.adoc b/CN/modules/ROOT/pages/master/oracle_compatibility/utl_encode.adoc new file mode 100644 index 00000000..79a90de5 --- /dev/null +++ b/CN/modules/ROOT/pages/master/oracle_compatibility/utl_encode.adoc @@ -0,0 +1,245 @@ +:sectnums: +:sectnumlevels: 5 + += UTL_ENCODE + +== 简介 + +`UTL_ENCODE` 是 IvorySQL Oracle 兼容模式下的内置包,提供与 Oracle 数据库行为一致的 Base64 编解码功能。它支持将任意二进制数据(`RAW` 类型)转换为可打印的 Base64 ASCII 字节序列,也可以将 Base64 字节序列还原为原始二进制数据。 + +== 函数参考 + +=== BASE64_ENCODE + +将二进制数据编码为 Base64 格式的 ASCII 字节序列。 + +==== 语法 + +[source,sql] +---- +UTL_ENCODE.BASE64_ENCODE(r IN RAW) RETURN RAW +---- + +==== 参数 + +[cols="1,1,3",options="header"] +|=== +|参数 |类型 |说明 +|`r` |`RAW` |待编码的二进制数据 +|=== + +==== 返回值 + +返回 Base64 编码后的 ASCII 字节序列,类型为 `RAW`。输出格式遵循 RFC 1521(MIME)规范:每 64 个字符后插入一个换行符(`\n`),最后一行末尾同样有换行符。 + +==== 特殊值行为 + +[cols="2,3",options="header"] +|=== +|输入 |返回值 +|`NULL` |`NULL` +|空 `RAW`(0 字节) |空 `RAW`(0 字节) +|=== + +=== BASE64_DECODE + +将 Base64 编码的 ASCII 字节序列还原为原始二进制数据。 + +==== 语法 + +[source,sql] +---- +UTL_ENCODE.BASE64_DECODE(r IN RAW) RETURN RAW +---- + +==== 参数 + +[cols="1,1,3",options="header"] +|=== +|参数 |类型 |说明 +|`r` |`RAW` |Base64 编码的字节序列 +|=== + +==== 返回值 + +返回解码后的原始二进制数据,类型为 `RAW`。 + +解码前会自动剥离输入中的空白字符(`\n`、`\r`、`\t`、空格),因此可直接接受 `BASE64_ENCODE` 的输出(含换行)。 + +==== 特殊值行为 + +[cols="2,3",options="header"] +|=== +|输入 |返回值 +|`NULL` |`NULL` +|空 `RAW`(0 字节) |空 `RAW`(0 字节) +|仅含空白字符(如 `\n`、`\r`、空格) |空 `RAW`(0 字节) +|含无效 Base64 字符 |报错:`invalid base64 input` +|=== + +== 使用示例 + +=== 编码字符串 + +将文本 `Hello` 对应的十六进制字节 `\x48656c6c6f` 进行 Base64 编码: + +[source,sql] +---- +SELECT utl_encode.base64_encode('\x48656c6c6f'); +---- + +输出为 `SGVsbG8=\n` 的 `RAW` 字节,共 9 字节: + +[source,text] +---- +\x534756736247383d0a +---- + +=== 解码 Base64 字节序列 + +将已知的 Base64 字节序列 `\x534756736247383d0a`(即 ASCII 的 `SGVsbG8=\n`)解码回原始数据: + +[source,sql] +---- +SELECT utl_encode.base64_decode('\x534756736247383d0a'); +---- + +[source,text] +---- +\x48656c6c6f +---- + +=== 编码后立即解码 + +[source,sql] +---- +SELECT utl_encode.base64_decode( + utl_encode.base64_encode('\x48656c6c6f') +) = '\x48656c6c6f'::bytea; +---- + +[source,text] +---- +t +---- + +=== 在 PL/iSQL 块中使用 + +[source,sql] +---- +DECLARE + v_src RAW(100) := '\x48656c6c6f'; + v_encoded RAW(200); + v_decoded RAW(200); +BEGIN + v_encoded := utl_encode.base64_encode(v_src); + DBMS_OUTPUT.PUT_LINE('编码长度: ' || pg_catalog.octet_length(v_encoded::bytea)); + + v_decoded := utl_encode.base64_decode(v_encoded); + DBMS_OUTPUT.PUT_LINE('往返一致: ' || CASE WHEN v_decoded = v_src THEN 'TRUE' ELSE 'FALSE' END); +END; +/ +---- + +[source,text] +---- +编码长度: 9 +往返一致: TRUE +---- + +=== 编码较大数据 + +编码 49 字节数据时,输出跨越两行(64 字符 + 换行 + 4 字符 + 换行 = 70 字节): + +[source,sql] +---- +SELECT octet_length( + utl_encode.base64_encode(pg_catalog.decode(repeat('00', 49), 'hex')) +); +---- + +[source,text] +---- +70 +---- + +查看分行内容: + +[source,sql] +---- +SELECT convert_from( + utl_encode.base64_encode(pg_catalog.decode(repeat('00', 49), 'hex')), + 'UTF8' +); +---- + +[source,text] +---- +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAA +---- + +第一行 64 个字符,第二行 4 个字符,每行末尾都有换行符。 + +=== NULL 输入返回 NULL + +[source,sql] +---- +SELECT utl_encode.base64_encode(NULL::bytea) IS NULL; -- t +SELECT utl_encode.base64_decode(NULL::bytea) IS NULL; -- t +---- + +=== 解码含 CRLF 换行的 Base64 文本 + +`BASE64_DECODE` 会自动剥离 `\r\n` 等空白字符,无需手动预处理: + +[source,sql] +---- +SELECT utl_encode.base64_decode( + pg_catalog.encode( + regexp_replace( + convert_from(utl_encode.base64_encode('\x48656c6c6f'), 'UTF8'), + E'\n', E'\r\n' + )::bytea, + 'escape' + )::bytea +) = '\x48656c6c6f'::bytea; +---- + +[source,text] +---- +t +---- + +=== 纯空白输入返回空 RAW + +[source,sql] +---- +-- \x0a0d200a = LF CR SP LF +SELECT octet_length(utl_encode.base64_decode('\x0a0d200a')); +---- + +[source,text] +---- +0 +---- + +== 输出格式 + +`BASE64_ENCODE` 的输出格式遵循 RFC 1521 MIME 规范,与 Oracle 数据库行为一致: + +[cols="1,1,1,1",options="header"] +|=== +|输入字节数 |纯 Base64 长度 |行数 |输出总字节数 +|1 |4 |1 |5 +|3 |4 |1 |5 +|48 |64 |1 |65 +|49 |68 |2 |70 +|96 |128 |2 |130 +|200 |268 |5 |273 +|=== + +[NOTE] +==== +PostgreSQL 的 `encode(data, 'base64')` 遵循 RFC 2045,每 76 个字符换行,末行无换行符,且返回 `text` 类型。`UTL_ENCODE.BASE64_ENCODE` 每 64 个字符换行,末行有换行符,返回 `RAW`(`bytea`)类型。 +==== diff --git a/EN/modules/ROOT/nav.adoc b/EN/modules/ROOT/nav.adoc index 594bda81..1d929060 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/utl_encode.adoc[25、UTL_ENCODE] * Containerization and Cloud Service ** Containerization *** xref:master/containerization/k8s_deployment.adoc[K8S deployment] @@ -112,6 +113,7 @@ *** xref:master/compatibility_features_design/with_function_procedure_impl_en.adoc[WITH FUNCTION/PROCEDURE] *** xref:master/compatibility_features_design/create_index_online.adoc[ONLINE Parameter for CREATE INDEX] *** xref:master/compatibility_features_design/alter_index_unusable_impl_en.adoc[Alter Index Unusable] +*** xref:master/compatibility_features_design/utl_encode.adoc[UTL_ENCODE] ** 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/utl_encode.adoc b/EN/modules/ROOT/pages/master/compatibility_features_design/utl_encode.adoc new file mode 100644 index 00000000..6d5619f7 --- /dev/null +++ b/EN/modules/ROOT/pages/master/compatibility_features_design/utl_encode.adoc @@ -0,0 +1,245 @@ +:sectnums: +:sectnumlevels: 5 + += UTL_ENCODE package implementation + +== Overview + +`UTL_ENCODE` is a built-in package in the IvorySQL Oracle compatibility extension (`ivorysql_ora`). It provides Base64 encoding and decoding compatible with Oracle Database. This document describes the design goals, implementation, and key technical details of `BASE64_ENCODE` and `BASE64_DECODE`. + +== File structure + +[source,text] +---- +contrib/ivorysql_ora/ +├── src/builtin_packages/utl_encode/ +│ ├── utl_encode.c # C function implementation +│ └── utl_encode--1.0.sql # SQL registration and PL/iSQL package declarations +├── sql/utl_encode.sql # Regression tests +└── expected/utl_encode.out # Expected regression test output +---- + +== Oracle compatibility goals + +The Oracle `UTL_ENCODE` package defines the following interfaces: + +[source,sql] +---- +-- Encode binary RAW data as Base64 ASCII bytes +UTL_ENCODE.BASE64_ENCODE(r IN RAW) RETURN RAW + +-- Decode Base64 ASCII bytes to binary RAW data +UTL_ENCODE.BASE64_DECODE(r IN RAW) RETURN RAW +---- + +IvorySQL maps Oracle's `RAW` type to PostgreSQL's `bytea` type. Both C functions therefore have a `bytea -> bytea` signature. + +== BASE64_ENCODE implementation + +=== Design goals + +Oracle `BASE64_ENCODE` uses the RFC 1521 MIME format. It inserts a newline (`\n`, or LF) after every 64 Base64 characters, including the final line. + +PostgreSQL's built-in `encode(bytea, 'base64')` follows RFC 2045, wraps lines after 76 characters, and does not guarantee a final newline. The Oracle-compatible format therefore requires separate line-wrapping logic. + +=== Encoding flow + +[source,text] +---- +Input bytea (src_len bytes) + │ + ▼ +pg_b64_encode() ← PostgreSQL internal function; produces Base64 without newlines + │ + ▼ +raw_b64 (b64_len bytes) ← length = ceil(src_len / 3) × 4 + │ + ▼ +Split into 64-character chunks and append '\n' to each chunk + │ + ▼ +Output bytea (b64_len + num_lines bytes) +---- + +=== Output length calculation + +[cols="2,3",options="header"] +|=== +|Value |Formula +|Base64 length |`b64_len = pg_b64_enc_len(src_len) = ceil(src_len / 3) × 4` +|Number of lines |`num_lines = ceil(b64_len / 64)` +|Final output length |`result_len = b64_len + num_lines` +|=== + +For example, encoding `Hello` (5 bytes) produces: + +* `b64_len` = 8 (`SGVsbG8=`) +* `num_lines` = 1 (8 ≤ 64) +* `result_len` = 8 + 1 = 9 bytes (`SGVsbG8=\n`) + +The following table shows the boundary cases: + +[cols="1,1,1,1",options="header"] +|=== +|Input bytes |`b64_len` |Lines |Output bytes +|48 |64 |1 |65 +|49 |68 |2 |70 +|96 |128 |2 |130 +|=== + +=== Key code + +[source,c] +---- +/* Calculate the Base64 length and number of lines. */ +b64_len = pg_b64_enc_len(src_len); +num_lines = (b64_len + 63) / 64; +result_len = b64_len + num_lines; + +/* Call PostgreSQL's internal encoder, which does not add newlines. */ +encoded_len = pg_b64_encode(src_data, src_len, raw_b64, b64_len); + +/* Write 64-character chunks and append LF to each chunk. */ +while (remaining > 0) +{ + chunk = (remaining >= 64) ? 64 : remaining; + memcpy(dst, p, chunk); + dst += chunk; + p += chunk; + remaining -= chunk; + *dst++ = '\n'; +} +---- + +=== Boundary behavior + +[cols="2,3",options="header"] +|=== +|Input |Output +|`NULL` |`NULL`, handled by the SQL-level `STRICT` modifier +|Empty `bytea` (0 bytes) |Empty `bytea` (0 bytes) +|Any nonempty binary value |Base64 text wrapped after every 64 characters, with `\n` after the final line +|=== + +== BASE64_DECODE implementation + +=== Design goals + +`BASE64_DECODE` accepts the newline-containing Base64 byte sequence produced by `BASE64_ENCODE` and restores the original binary data. PostgreSQL's internal `pg_b64_decode()` rejects all whitespace, while Oracle-compatible encoded output contains `\n`. The implementation must therefore remove whitespace before decoding. + +=== Decoding flow + +[source,text] +---- +Input bytea (Base64 bytes containing \n) + │ + ▼ +Strip whitespace +Remove '\n', '\r', '\t', and spaces + │ + ▼ +clean_buf (Base64 characters without whitespace) + │ + ▼ +clean_len == 0? ── yes ──▶ Return an empty bytea + │ no + ▼ +pg_b64_decode() ← PostgreSQL internal function + │ + ▼ +decoded_len < 0? ── yes ──▶ ERROR: invalid base64 input + │ no + ▼ +Output bytea (decoded_len bytes) +---- + +=== Whitespace removal + +The accepted whitespace characters are `\n` (LF), `\r` (CR), `\t` (TAB), and space. This supports: + +* `\n` line endings produced by Oracle `BASE64_ENCODE` +* Windows-style `\r\n` line endings +* TAB and space characters introduced by manual formatting + +[source,c] +---- +for (i = 0; i < src_len; i++) +{ + unsigned char c = (unsigned char) src_data[i]; + + if (c != '\n' && c != '\r' && c != '\t' && c != ' ') + clean_buf[clean_len++] = src_data[i]; +} +---- + +=== Error handling + +[cols="2,3",options="header"] +|=== +|Condition |Behavior +|`NULL` input |Returns `NULL`, handled by the `STRICT` modifier +|Empty `bytea` input |Returns an empty `bytea` +|Whitespace-only input, such as `\x0a0d200a` |Returns an empty `bytea` +|Invalid Base64 characters |Raises `ERROR: UTL_ENCODE.BASE64_DECODE: invalid base64 input` with `ERRCODE_INVALID_PARAMETER_VALUE` +|=== + +== PL/iSQL package wrapper + +The C functions are registered in the `sys` schema and wrapped in a PL/iSQL package that exposes an Oracle-style interface: + +[source,sql] +---- +-- Register C functions in the sys schema +CREATE FUNCTION sys.utl_encode_base64_encode(bytea) RETURNS bytea + AS 'MODULE_PATHNAME', 'ivorysql_utl_encode_base64_encode' + LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION sys.utl_encode_base64_decode(bytea) RETURNS bytea + AS 'MODULE_PATHNAME', 'ivorysql_utl_encode_base64_decode' + LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +-- PL/iSQL package that exposes the public interface +CREATE PACKAGE utl_encode AS + FUNCTION base64_encode(r IN RAW) RETURN RAW; + FUNCTION base64_decode(r IN RAW) RETURN RAW; +END utl_encode; + +CREATE PACKAGE BODY utl_encode AS + FUNCTION base64_encode(r IN RAW) RETURN RAW IS + BEGIN RETURN utl_encode_base64_encode(r); END; + + FUNCTION base64_decode(r IN RAW) RETURN RAW IS + BEGIN RETURN utl_encode_base64_decode(r); END; +END utl_encode; +---- + +The call path is `utl_encode.base64_encode(r)` → PL/iSQL package body → `sys.utl_encode_base64_encode(bytea)` → C function. + +== Differences from PostgreSQL built-in functions + +[cols="2,2,2",options="header"] +|=== +|Feature |PostgreSQL `encode(x, 'base64')` |Oracle `UTL_ENCODE.BASE64_ENCODE` +|Line-wrapping standard |RFC 2045 (76 characters per line) |RFC 1521 (64 characters per line) +|Final newline |No |Yes (`\n`) +|Input and output types |`bytea` → `text` |`RAW` → `RAW` (both map to `bytea`) +|Whitespace during decoding |`decode()` accepts newlines |`pg_b64_decode()` rejects whitespace, so preprocessing is required +|=== + +== Regression test coverage + +The tests are defined in `contrib/ivorysql_ora/sql/utl_encode.sql`. + +[cols="2,3",options="header"] +|=== +|Test category |Coverage +|NULL boundary |NULL input returns NULL +|Empty input boundary |A 0-byte `bytea` returns a 0-byte `bytea` +|Known value |`Hello` encodes as `SGVsbG8=\n` (9 bytes) +|Line boundary |48 bytes produce one 65-byte line; 49 bytes produce two lines and 70 bytes +|Large input |Multiline encoding and decoding of 200 bytes +|Round trip |`decode(encode(x)) = x` +|CRLF compatibility |`\r\n` line endings are removed correctly +|Whitespace-only input |Decoding `\x0a0d200a` returns an empty `bytea` +|PL/iSQL interface |Package calls verify the end-to-end path +|=== diff --git a/EN/modules/ROOT/pages/master/oracle_compatibility/utl_encode.adoc b/EN/modules/ROOT/pages/master/oracle_compatibility/utl_encode.adoc new file mode 100644 index 00000000..b96d4d2e --- /dev/null +++ b/EN/modules/ROOT/pages/master/oracle_compatibility/utl_encode.adoc @@ -0,0 +1,245 @@ +:sectnums: +:sectnumlevels: 5 + += UTL_ENCODE + +== Introduction + +`UTL_ENCODE` is a built-in package available in IvorySQL's Oracle-compatible mode. It provides Base64 encoding and decoding compatible with Oracle Database. The package converts arbitrary binary data (`RAW`) to printable Base64 ASCII bytes and converts Base64 bytes back to their original binary form. + +== Function reference + +=== BASE64_ENCODE + +Encodes binary data as a Base64 ASCII byte sequence. + +==== Syntax + +[source,sql] +---- +UTL_ENCODE.BASE64_ENCODE(r IN RAW) RETURN RAW +---- + +==== Parameter + +[cols="1,1,3",options="header"] +|=== +|Parameter |Type |Description +|`r` |`RAW` |Binary data to encode +|=== + +==== Return value + +Returns the Base64-encoded ASCII byte sequence as `RAW`. The output follows the RFC 1521 MIME format: a newline (`\n`) is inserted after every 64 characters, including at the end of the final line. + +==== Special values + +[cols="2,3",options="header"] +|=== +|Input |Return value +|`NULL` |`NULL` +|Empty `RAW` (0 bytes) |Empty `RAW` (0 bytes) +|=== + +=== BASE64_DECODE + +Decodes a Base64 ASCII byte sequence to its original binary data. + +==== Syntax + +[source,sql] +---- +UTL_ENCODE.BASE64_DECODE(r IN RAW) RETURN RAW +---- + +==== Parameter + +[cols="1,1,3",options="header"] +|=== +|Parameter |Type |Description +|`r` |`RAW` |Base64-encoded byte sequence +|=== + +==== Return value + +Returns the decoded binary data as `RAW`. + +Before decoding, the function removes whitespace characters (`\n`, `\r`, `\t`, and spaces). It therefore accepts newline-containing output from `BASE64_ENCODE` directly. + +==== Special values + +[cols="2,3",options="header"] +|=== +|Input |Return value +|`NULL` |`NULL` +|Empty `RAW` (0 bytes) |Empty `RAW` (0 bytes) +|Whitespace only, such as `\n`, `\r`, or spaces |Empty `RAW` (0 bytes) +|Invalid Base64 characters |Error: `invalid base64 input` +|=== + +== Examples + +=== Encoding a string + +Encode the hexadecimal bytes `\x48656c6c6f`, which represent `Hello`: + +[source,sql] +---- +SELECT utl_encode.base64_encode('\x48656c6c6f'); +---- + +The result is 9 `RAW` bytes containing `SGVsbG8=\n`: + +[source,text] +---- +\x534756736247383d0a +---- + +=== Decoding a Base64 byte sequence + +Decode `\x534756736247383d0a`, the ASCII bytes for `SGVsbG8=\n`: + +[source,sql] +---- +SELECT utl_encode.base64_decode('\x534756736247383d0a'); +---- + +[source,text] +---- +\x48656c6c6f +---- + +=== Encoding and immediately decoding + +[source,sql] +---- +SELECT utl_encode.base64_decode( + utl_encode.base64_encode('\x48656c6c6f') +) = '\x48656c6c6f'::bytea; +---- + +[source,text] +---- +t +---- + +=== Using the package in a PL/iSQL block + +[source,sql] +---- +DECLARE + v_src RAW(100) := '\x48656c6c6f'; + v_encoded RAW(200); + v_decoded RAW(200); +BEGIN + v_encoded := utl_encode.base64_encode(v_src); + DBMS_OUTPUT.PUT_LINE('Encoded length: ' || pg_catalog.octet_length(v_encoded::bytea)); + + v_decoded := utl_encode.base64_decode(v_encoded); + DBMS_OUTPUT.PUT_LINE('Round trip matches: ' || CASE WHEN v_decoded = v_src THEN 'TRUE' ELSE 'FALSE' END); +END; +/ +---- + +[source,text] +---- +Encoded length: 9 +Round trip matches: TRUE +---- + +=== Encoding larger data + +Encoding 49 bytes produces two lines (64 characters + newline + 4 characters + newline = 70 bytes): + +[source,sql] +---- +SELECT octet_length( + utl_encode.base64_encode(pg_catalog.decode(repeat('00', 49), 'hex')) +); +---- + +[source,text] +---- +70 +---- + +Display the line-wrapped result: + +[source,sql] +---- +SELECT convert_from( + utl_encode.base64_encode(pg_catalog.decode(repeat('00', 49), 'hex')), + 'UTF8' +); +---- + +[source,text] +---- +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAA +---- + +The first line has 64 characters and the second has 4. Both lines end with a newline. + +=== NULL input returns NULL + +[source,sql] +---- +SELECT utl_encode.base64_encode(NULL::bytea) IS NULL; -- t +SELECT utl_encode.base64_decode(NULL::bytea) IS NULL; -- t +---- + +=== Decoding Base64 text with CRLF line endings + +`BASE64_DECODE` automatically removes whitespace such as `\r\n`; no preprocessing is required: + +[source,sql] +---- +SELECT utl_encode.base64_decode( + pg_catalog.encode( + regexp_replace( + convert_from(utl_encode.base64_encode('\x48656c6c6f'), 'UTF8'), + E'\n', E'\r\n' + )::bytea, + 'escape' + )::bytea +) = '\x48656c6c6f'::bytea; +---- + +[source,text] +---- +t +---- + +=== Whitespace-only input returns an empty RAW + +[source,sql] +---- +-- \x0a0d200a = LF CR SP LF +SELECT octet_length(utl_encode.base64_decode('\x0a0d200a')); +---- + +[source,text] +---- +0 +---- + +== Output format + +`BASE64_ENCODE` follows the RFC 1521 MIME format and matches Oracle Database behavior: + +[cols="1,1,1,1",options="header"] +|=== +|Input bytes |Base64 length |Lines |Total output bytes +|1 |4 |1 |5 +|3 |4 |1 |5 +|48 |64 |1 |65 +|49 |68 |2 |70 +|96 |128 |2 |130 +|200 |268 |5 |273 +|=== + +[NOTE] +==== +PostgreSQL's `encode(data, 'base64')` follows RFC 2045, wraps lines after 76 characters, omits the final newline, and returns `text`. `UTL_ENCODE.BASE64_ENCODE` wraps lines after 64 characters, includes a final newline, and returns `RAW` (`bytea`). +====