Skip to content

Commit 15ca271

Browse files
committed
docs: add UTL_ENCODE documentation
1 parent 4c54d8e commit 15ca271

6 files changed

Lines changed: 1034 additions & 0 deletions

File tree

CN/modules/ROOT/nav.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
** xref:master/oracle_compatibility/compat_stragg.adoc[23、STRAGG 函数]
3333
** xref:master/oracle_compatibility/compat_alter_index_unusable.adoc[24、禁用索引]
3434
** xref:master/oracle_compatibility/compat_dbtimezone.adoc[25、dbtimezone]
35+
** xref:master/oracle_compatibility/utl_encode.adoc[26、UTL_ENCODE]
3536
* 容器化与云服务
3637
** 容器化指南
3738
*** xref:master/containerization/k8s_deployment.adoc[K8S部署]
@@ -112,6 +113,7 @@
112113
**** xref:master/compatibility_features_design/with_function_procedure_impl.adoc[WITH FUNCTION/PROCEDURE]
113114
**** xref:master/compatibility_features_design/create_index_online.adoc[索引 ONLINE 参数]
114115
**** xref:master/compatibility_features_design/alter_index_unusable_impl.adoc[禁用索引]
116+
**** xref:master/compatibility_features_design/utl_encode.adoc[UTL_ENCODE]
115117
*** 内置函数
116118
**** xref:master/oracle_builtin_functions/sys_context.adoc[sys_context]
117119
**** xref:master/oracle_builtin_functions/userenv.adoc[userenv]
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
:sectnums:
2+
:sectnumlevels: 5
3+
4+
= UTL_ENCODE 包实现原理
5+
6+
== 概述
7+
8+
`UTL_ENCODE` 是 IvorySQL Oracle 兼容扩展(`ivorysql_ora`)中的内置包,提供与 Oracle 数据库兼容的 Base64 编码与解码功能。本文档描述 `BASE64_ENCODE` 和 `BASE64_DECODE` 两个函数的设计目标、实现原理及关键技术细节。
9+
10+
== 文件结构
11+
12+
[source,text]
13+
----
14+
contrib/ivorysql_ora/
15+
├── src/builtin_packages/utl_encode/
16+
│ ├── utl_encode.c # C 函数实现
17+
│ └── utl_encode--1.0.sql # SQL 注册和 PL/iSQL 包声明
18+
├── sql/utl_encode.sql # 回归测试用例
19+
└── expected/utl_encode.out # 回归测试期望输出
20+
----
21+
22+
== Oracle 兼容目标
23+
24+
Oracle 的 `UTL_ENCODE` 包定义以下接口:
25+
26+
[source,sql]
27+
----
28+
-- 编码:将二进制 RAW 数据转换为 Base64 ASCII 字节序列
29+
UTL_ENCODE.BASE64_ENCODE(r IN RAW) RETURN RAW
30+
31+
-- 解码:将 Base64 ASCII 字节序列还原为二进制 RAW 数据
32+
UTL_ENCODE.BASE64_DECODE(r IN RAW) RETURN RAW
33+
----
34+
35+
在 IvorySQL 中,Oracle 的 `RAW` 类型映射为 PostgreSQL 的 `bytea`,因此两个函数的 C 签名均为 `bytea -> bytea`。
36+
37+
== BASE64_ENCODE 实现原理
38+
39+
=== 设计目标
40+
41+
Oracle `BASE64_ENCODE` 采用 RFC 1521(MIME)格式:每 64 个 Base64 字符后插入一个换行符(`\n`,即 LF),包括最后一行。
42+
43+
PostgreSQL 内置的 `encode(bytea, 'base64')` 遵循 RFC 2045,每 76 个字符换行,且末尾不保证有换行符。因此,需要自行实现换行逻辑。
44+
45+
=== 编码流程
46+
47+
[source,text]
48+
----
49+
输入 bytea(src_len 字节)
50+
51+
52+
pg_b64_encode() ← PostgreSQL 内部函数,输出纯 Base64(无换行)
53+
54+
55+
raw_b64(b64_len 字节) ← 长度 = ceil(src_len / 3) × 4
56+
57+
58+
按 64 字符分块,每块追加 '\n'
59+
60+
61+
输出 bytea(b64_len + num_lines 字节)
62+
----
63+
64+
=== 输出长度计算
65+
66+
[cols="2,3",options="header"]
67+
|===
68+
|参数 |公式
69+
|纯 Base64 长度 |`b64_len = pg_b64_enc_len(src_len) = ceil(src_len / 3) × 4`
70+
|行数 |`num_lines = ceil(b64_len / 64)`
71+
|最终输出长度 |`result_len = b64_len + num_lines`
72+
|===
73+
74+
例如,编码 `Hello`(5 字节)时:
75+
76+
* `b64_len` = 8(`SGVsbG8=`)
77+
* `num_lines` = 1(8 ≤ 64)
78+
* `result_len` = 8 + 1 = 9 字节(`SGVsbG8=\n`)
79+
80+
边界情况如下:
81+
82+
[cols="1,1,1,1",options="header"]
83+
|===
84+
|输入字节数 |`b64_len` |行数 |输出字节数
85+
|48 |64 |1 |65
86+
|49 |68 |2 |70
87+
|96 |128 |2 |130
88+
|===
89+
90+
=== 关键代码
91+
92+
[source,c]
93+
----
94+
/* 计算纯 Base64 长度,再计算行数 */
95+
b64_len = pg_b64_enc_len(src_len);
96+
num_lines = (b64_len + 63) / 64;
97+
result_len = b64_len + num_lines;
98+
99+
/* 调用 PostgreSQL 内部编码函数(不含换行) */
100+
encoded_len = pg_b64_encode(src_data, src_len, raw_b64, b64_len);
101+
102+
/* 按 64 字符切块,逐块写入并追加 LF */
103+
while (remaining > 0)
104+
{
105+
chunk = (remaining >= 64) ? 64 : remaining;
106+
memcpy(dst, p, chunk);
107+
dst += chunk;
108+
p += chunk;
109+
remaining -= chunk;
110+
*dst++ = '\n';
111+
}
112+
----
113+
114+
=== 边界行为
115+
116+
[cols="2,3",options="header"]
117+
|===
118+
|输入 |输出
119+
|`NULL` |`NULL`,由 SQL 层的 `STRICT` 修饰符处理
120+
|空 `bytea`(0 字节) |空 `bytea`(0 字节)
121+
|任意非空二进制 |Base64 文本,每 64 个字符一行,末行也有 `\n`
122+
|===
123+
124+
== BASE64_DECODE 实现原理
125+
126+
=== 设计目标
127+
128+
`BASE64_DECODE` 接受 `BASE64_ENCODE` 产生的带换行 Base64 字节序列,并将其还原为原始二进制数据。PostgreSQL 内部的 `pg_b64_decode()` 拒绝所有空白字符,而 Oracle 编码输出中包含 `\n`,因此必须在解码前剥离空白。
129+
130+
=== 解码流程
131+
132+
[source,text]
133+
----
134+
输入 bytea(含 \n 的 Base64 字节序列)
135+
136+
137+
剥离空白字符
138+
过滤 '\n'、'\r'、'\t' 和空格
139+
140+
141+
clean_buf(无空白的纯 Base64 字符)
142+
143+
144+
clean_len == 0? ── 是 ──▶ 返回空 bytea
145+
│ 否
146+
147+
pg_b64_decode() ← PostgreSQL 内部函数
148+
149+
150+
decoded_len < 0? ── 是 ──▶ ERROR: invalid base64 input
151+
│ 否
152+
153+
输出 bytea(decoded_len 字节)
154+
----
155+
156+
=== 空白剥离策略
157+
158+
接受的空白字符包括 `\n`(LF)、`\r`(CR)、`\t`(TAB)和空格。该设计同时兼容:
159+
160+
* Oracle `BASE64_ENCODE` 输出的 `\n` 换行
161+
* Windows 风格的 `\r\n` 换行
162+
* 人工格式化时引入的 TAB 和空格
163+
164+
[source,c]
165+
----
166+
for (i = 0; i < src_len; i++)
167+
{
168+
unsigned char c = (unsigned char) src_data[i];
169+
170+
if (c != '\n' && c != '\r' && c != '\t' && c != ' ')
171+
clean_buf[clean_len++] = src_data[i];
172+
}
173+
----
174+
175+
=== 错误处理
176+
177+
[cols="2,3",options="header"]
178+
|===
179+
|情形 |行为
180+
|`NULL` 输入 |返回 `NULL`,由 `STRICT` 修饰符处理
181+
|空 `bytea` 输入 |返回空 `bytea`
182+
|纯空白输入(如 `\x0a0d200a`) |返回空 `bytea`
183+
|无效 Base64 字符 |抛出 `ERROR: UTL_ENCODE.BASE64_DECODE: invalid base64 input`,错误码为 `ERRCODE_INVALID_PARAMETER_VALUE`
184+
|===
185+
186+
== PL/iSQL 包封装
187+
188+
C 函数注册在 `sys` 模式中,再由 PL/iSQL 包封装,对外提供 Oracle 风格的调用接口:
189+
190+
[source,sql]
191+
----
192+
-- 在 sys 模式中注册 C 函数
193+
CREATE FUNCTION sys.utl_encode_base64_encode(bytea) RETURNS bytea
194+
AS 'MODULE_PATHNAME', 'ivorysql_utl_encode_base64_encode'
195+
LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT;
196+
197+
CREATE FUNCTION sys.utl_encode_base64_decode(bytea) RETURNS bytea
198+
AS 'MODULE_PATHNAME', 'ivorysql_utl_encode_base64_decode'
199+
LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT;
200+
201+
-- 对外提供接口的 PL/iSQL 包
202+
CREATE PACKAGE utl_encode AS
203+
FUNCTION base64_encode(r IN RAW) RETURN RAW;
204+
FUNCTION base64_decode(r IN RAW) RETURN RAW;
205+
END utl_encode;
206+
207+
CREATE PACKAGE BODY utl_encode AS
208+
FUNCTION base64_encode(r IN RAW) RETURN RAW IS
209+
BEGIN RETURN utl_encode_base64_encode(r); END;
210+
211+
FUNCTION base64_decode(r IN RAW) RETURN RAW IS
212+
BEGIN RETURN utl_encode_base64_decode(r); END;
213+
END utl_encode;
214+
----
215+
216+
调用链路为:`utl_encode.base64_encode(r)` → PL/iSQL 包体 → `sys.utl_encode_base64_encode(bytea)` → C 函数。
217+
218+
== 与 PostgreSQL 内置函数的差异
219+
220+
[cols="2,2,2",options="header"]
221+
|===
222+
|特性 |PostgreSQL `encode(x, 'base64')` |Oracle `UTL_ENCODE.BASE64_ENCODE`
223+
|换行标准 |RFC 2045(76 字符/行) |RFC 1521(64 字符/行)
224+
|末行换行 |无 |有(`\n`)
225+
|输入和输出类型 |`bytea` → `text` |`RAW` → `RAW`(均为 `bytea`)
226+
|解码时空白处理 |`decode()` 接受换行 |`pg_b64_decode()` 不接受,需预处理
227+
|===
228+
229+
== 回归测试覆盖
230+
231+
测试文件为 `contrib/ivorysql_ora/sql/utl_encode.sql`。
232+
233+
[cols="2,3",options="header"]
234+
|===
235+
|测试类型 |覆盖内容
236+
|NULL 边界 |NULL 输入返回 NULL
237+
|空输入边界 |0 字节 `bytea` 返回 0 字节 `bytea`
238+
|已知值验证 |`Hello` 编码结果为 `SGVsbG8=\n`(9 字节)
239+
|行断边界 |48 字节 → 1 行 65 字节;49 字节 → 2 行 70 字节
240+
|大输入 |200 字节数据的多行编解码
241+
|往返一致性 |`decode(encode(x)) = x`
242+
|CRLF 兼容 |正确剥离 `\r\n` 换行
243+
|纯空白输入 |`\x0a0d200a` 解码后返回空 `bytea`
244+
|PL/iSQL 接口 |通过包调用验证端到端路径
245+
|===

0 commit comments

Comments
 (0)