Skip to content

Commit 541a290

Browse files
committed
VSIZE function implementation doc
1 parent 700c645 commit 541a290

4 files changed

Lines changed: 269 additions & 0 deletions

File tree

CN/modules/ROOT/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
**** xref:master/oracle_builtin_functions/rawtohex.adoc[rawtohex]
117117
**** xref:master/oracle_builtin_functions/stragg.adoc[stragg]
118118
**** xref:master/oracle_builtin_functions/dbtimezone_impl.adoc[dbtimezone]
119+
**** xref:master/oracle_builtin_functions/vsize.adoc[vsize]
119120
*** xref:master/gb18030.adoc[国标GB18030]
120121
* 参考指南
121122
** xref:master/tools_reference.adoc[工具参考]
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
2+
:sectnums:
3+
:sectnumlevels: 5
4+
5+
6+
= **功能概述**
7+
8+
IvorySQL提供兼容Oracle内置函数 ```VSIZE('parameter')``` ,用于返回参数在内部存储表示中所占用的字节数,
9+
即返回参数的“存储大小”。对于字符类型数据,返回其字节长度(不含变长头);对于定长类型(如 NUMBER、
10+
BOOLEAN、DATE、TIMESTAMP 等),返回该类型的存储宽度;参数为 NULL 时返回 NULL。
11+
12+
== 实现原理
13+
14+
VSIZE 需要接受任意数据类型的入参(字符、数值、布尔、日期时间等),并根据其存储方式(变长 varlena 类型
15+
或定长类型)分别计算字节数,这类与具体类型存储细节相关的逻辑无法用简单的 SQL 包装实现,因此本次开发
16+
使用 C 语言编写扩展函数 `ora_vsize`,注册为:
17+
18+
```
19+
sys.vsize(anycompatible) RETURNS int4
20+
```
21+
22+
使用 `anycompatible` 伪类型作为参数类型,使得该函数可以接受任意数据类型的入参,未显式指定类型的字符串
23+
字面量(如 `'abc'`)会按照 PostgreSQL 的默认规则解析为 text 类型,与 Oracle 中 `VSIZE('abc')` 的行为一致。
24+
函数声明为 `STRICT`,因此入参为 NULL 时直接返回 NULL,无需在函数体中额外处理。
25+
26+
函数实现位于 `contrib/ivorysql_ora/src/builtin_functions/misc_functions.c` 中的 `ora_vsize`:
27+
28+
* 首次调用时,通过 `get_fn_expr_argtype()` 取得实参的真实类型 OID,并调用 `get_typlen()` 获取该类型的
29+
`typlen`(存储长度),缓存到 `fcinfo->flinfo->fn_extra`,避免同一查询中重复查目录;后续调用直接从
30+
`fn_extra` 中读取缓存值。
31+
* `typlen == -1`:表示变长(varlena)类型,如 text、varchar2、numeric 等。此时调用
32+
`toast_raw_datum_size()` 获取该值的逻辑(解压缩后)大小 —— 该函数会统一处理 1 字节/4 字节头、压缩存储
33+
以及 TOAST 外部存储等各种情况,返回值统一按 4 字节头换算,因此再减去 `VARHDRSZ` 即可得到不含头部的
34+
有效数据字节数。这与 `octet_length()` 计算字节长度所采用的方式一致,因此 `VSIZE('abc') = LENGTHB('abc')`。
35+
* `typlen == -2`:表示 cstring 类型,返回其字符串长度加 1(含结尾 `\0`)。
36+
* 其余情况:为定长类型,直接返回该类型的 `typlen` 作为存储宽度(例如 int4 为 4,int8/float8/date/
37+
timestamp/timestamptz 均为 8,boolean 为 1)。
38+
39+
具体函数注册在 `builtin_functions--1.0.sql` 中完成:
40+
```sql
41+
/* VSIZE */
42+
/*
43+
* VSIZE: Oracle-compatible function returning the number of bytes in the
44+
* internal representation of the argument. Returns NULL for NULL input.
45+
* For varlena types the logical (decompressed) data size, excluding the
46+
* varlena header, is returned; for fixed-width types the storage width is
47+
* returned.
48+
*
49+
* The anycompatible pseudo-type accepts a value of any data type, and an
50+
* untyped string literal is resolved to text, so VSIZE('abc') works just
51+
* like in Oracle.
52+
*/
53+
CREATE FUNCTION sys.vsize(anycompatible)
54+
RETURNS int4
55+
AS 'MODULE_PATHNAME', 'ora_vsize'
56+
LANGUAGE C
57+
STRICT
58+
IMMUTABLE;
59+
/* End - VSIZE */
60+
```
61+
62+
C 函数实现(`misc_functions.c`):
63+
```c
64+
Datum
65+
ora_vsize(PG_FUNCTION_ARGS)
66+
{
67+
Datum value = PG_GETARG_DATUM(0);
68+
int32 result;
69+
int typlen;
70+
71+
/* On first call, get the input type's typlen, and save at *fn_extra */
72+
if (fcinfo->flinfo->fn_extra == NULL)
73+
{
74+
/* Lookup the datatype of the supplied argument */
75+
Oid argtypeid = get_fn_expr_argtype(fcinfo->flinfo, 0);
76+
77+
typlen = get_typlen(argtypeid);
78+
if (typlen == 0) /* should not happen */
79+
elog(ERROR, "cache lookup failed for type %u", argtypeid);
80+
81+
fcinfo->flinfo->fn_extra = MemoryContextAlloc(fcinfo->flinfo->fn_mcxt,
82+
sizeof(int));
83+
*((int *) fcinfo->flinfo->fn_extra) = typlen;
84+
}
85+
else
86+
typlen = *((int *) fcinfo->flinfo->fn_extra);
87+
88+
if (typlen == -1)
89+
{
90+
/*
91+
* varlena type. toast_raw_datum_size() normalizes 1-byte/4-byte
92+
* headers, compression and external (toasted) storage to the
93+
* logical (decompressed) size using the 4-byte header convention,
94+
* so subtracting VARHDRSZ yields the payload byte count in every
95+
* case -- the same pattern octet_length() uses.
96+
*/
97+
result = toast_raw_datum_size(value) - VARHDRSZ;
98+
}
99+
else if (typlen == -2)
100+
{
101+
/* cstring */
102+
result = strlen(DatumGetCString(value)) + 1;
103+
}
104+
else
105+
{
106+
/* ordinary fixed-width type */
107+
result = typlen;
108+
}
109+
110+
PG_RETURN_INT32(result);
111+
}
112+
```
113+
114+
== VSIZE 典型用例
115+
[cols="8,2"]
116+
|====
117+
|*用例语句*|*返回值*
118+
|SELECT vsize('abc'); | 3
119+
|SELECT vsize(CAST('abc' AS VARCHAR2)); | 3
120+
|SELECT vsize('abc'::varchar); | 3
121+
|SELECT vsize('abc'::char(10)); | 10
122+
|SELECT vsize('你好'::text); | 6
123+
|SELECT vsize(0::number); | 2
124+
|SELECT vsize(1::number); | 4
125+
|SELECT vsize(123::number); | 4
126+
|SELECT vsize(1.23::number); | 6
127+
|SELECT vsize(123::int4); | 4
128+
|SELECT vsize(123::int8); | 8
129+
|SELECT vsize(1.23::float8); | 8
130+
|SELECT vsize('NaN'::float8); | 8
131+
|SELECT vsize(true); | 1
132+
|SELECT vsize('2024-01-01'::date); | 8
133+
|SELECT vsize('2024-01-01 10:00:00'::timestamp); | 8
134+
|SELECT vsize('2024-01-01 10:00:00+08'::timestamptz); | 8
135+
|SELECT vsize(NULL::text); | NULL
136+
|SELECT vsize(repeat('a', 100000)); | 100000
137+
|====
138+
139+
对于同一字符串,`VSIZE` 与 `LENGTHB` 的结果一致:
140+
```
141+
SELECT vsize('abc') = lengthb('abc') AS same_as_lengthb;
142+
same_as_lengthb
143+
-----------------
144+
t
145+
```
146+
147+
即使数据经过压缩存储或 TOAST 到行外,`VSIZE` 仍然返回其未压缩的逻辑字节数:
148+
```
149+
CREATE TABLE vsize_big(a text);
150+
INSERT INTO vsize_big SELECT repeat('b', 200000) FROM generate_series(1, 10);
151+
SELECT bool_and(vsize(a) = lengthb(a)) AS toasted_matches_lengthb, min(vsize(a)) AS min_size FROM vsize_big;
152+
toasted_matches_lengthb | min_size
153+
-------------------------+----------
154+
t | 200000
155+
```

EN/modules/ROOT/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
*** xref:master/oracle_builtin_functions/rawtohex.adoc[rawtohex]
117117
*** xref:master/oracle_builtin_functions/stragg.adoc[stragg]
118118
*** xref:master/oracle_builtin_functions/dbtimezone_impl_en.adoc[dbtimezone]
119+
*** xref:master/oracle_builtin_functions/vsize_en.adoc[vsize]
119120
** xref:master/gb18030.adoc[GB18030 Character Set]
120121
* Reference
121122
** xref:master/tools_reference.adoc[Tool Reference]
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
2+
:sectnums:
3+
:sectnumlevels: 5
4+
5+
6+
= **Feature Overview**
7+
8+
IvorySQL provides the Oracle-compatible built-in function ```VSIZE('parameter')```, which returns the number of
9+
bytes occupied by the argument in its internal storage representation, i.e. the "storage size" of the argument.
10+
For character types, it returns the byte length (excluding the variable-length header); for fixed-width types
11+
(such as NUMBER, BOOLEAN, DATE, TIMESTAMP, etc.), it returns the storage width of that type; when the argument
12+
is NULL, it returns NULL.
13+
14+
== Implementation
15+
16+
VSIZE needs to accept an argument of any data type (character, numeric, boolean, date/time, etc.) and compute
17+
the byte count differently depending on its storage representation (variable-length varlena type or fixed-width
18+
type). This kind of logic, which depends on type-specific storage details, cannot be implemented with a simple
19+
SQL wrapper, so this feature is implemented as a C-language extension function `ora_vsize`, registered as:
20+
21+
```
22+
sys.vsize(anycompatible) RETURNS int4
23+
```
24+
25+
Using the `anycompatible` pseudo-type as the parameter type allows the function to accept an argument of any
26+
data type; an untyped string literal (such as `'abc'`) is resolved to the text type following PostgreSQL's
27+
default rules, matching the behavior of `VSIZE('abc')` in Oracle. The function is declared `STRICT`, so a NULL
28+
argument directly returns NULL without any extra handling in the function body.
29+
30+
The function is implemented as `ora_vsize` in `contrib/ivorysql_ora/src/builtin_functions/misc_functions.c`:
31+
32+
* On the first call, the actual type OID of the argument is obtained via `get_fn_expr_argtype()`, and
33+
`get_typlen()` is called to get that type's `typlen` (storage length), which is cached in
34+
`fcinfo->flinfo->fn_extra` to avoid repeated catalog lookups within the same query; subsequent calls read the
35+
cached value directly from `fn_extra`.
36+
* `typlen == -1`: indicates a variable-length (varlena) type, such as text, varchar2, or numeric. In this case,
37+
`toast_raw_datum_size()` is called to get the logical (decompressed) size of the value -- this function
38+
uniformly handles 1-byte/4-byte headers, compressed storage, and TOASTed (out-of-line) storage, and its return
39+
value is always normalized to the 4-byte header convention, so subtracting `VARHDRSZ` yields the payload byte
40+
count excluding the header. This is the same approach used by `octet_length()` to compute byte length, which
41+
is why `VSIZE('abc') = LENGTHB('abc')`.
42+
* `typlen == -2`: indicates the cstring type, and the string length plus 1 (including the terminating `\0`) is
43+
returned.
44+
* Otherwise: the type is fixed-width, and its `typlen` is returned directly as the storage width (for example,
45+
int4 is 4; int8/float8/date/timestamp/timestamptz are all 8; boolean is 1).
46+
47+
The function registration is done in `builtin_functions--1.0.sql`:
48+
```sql
49+
/* VSIZE */
50+
/*
51+
* VSIZE: Oracle-compatible function returning the number of bytes in the
52+
* internal representation of the argument. Returns NULL for NULL input.
53+
* For varlena types the logical (decompressed) data size, excluding the
54+
* varlena header, is returned; for fixed-width types the storage width is
55+
* returned.
56+
*
57+
* The anycompatible pseudo-type accepts a value of any data type, and an
58+
* untyped string literal is resolved to text, so VSIZE('abc') works just
59+
* like in Oracle.
60+
*/
61+
CREATE FUNCTION sys.vsize(anycompatible)
62+
RETURNS int4
63+
AS 'MODULE_PATHNAME', 'ora_vsize'
64+
LANGUAGE C
65+
STRICT
66+
IMMUTABLE;
67+
/* End - VSIZE */
68+
```
69+
70+
== Typical VSIZE examples
71+
[cols="8,2"]
72+
|====
73+
|*Example statement*|*Return value*
74+
|SELECT vsize('abc'); | 3
75+
|SELECT vsize(CAST('abc' AS VARCHAR2)); | 3
76+
|SELECT vsize('abc'::varchar); | 3
77+
|SELECT vsize('abc'::char(10)); | 10
78+
|SELECT vsize('你好'::text); | 6
79+
|SELECT vsize(0::number); | 2
80+
|SELECT vsize(1::number); | 4
81+
|SELECT vsize(123::number); | 4
82+
|SELECT vsize(1.23::number); | 6
83+
|SELECT vsize(123::int4); | 4
84+
|SELECT vsize(123::int8); | 8
85+
|SELECT vsize(1.23::float8); | 8
86+
|SELECT vsize('NaN'::float8); | 8
87+
|SELECT vsize(true); | 1
88+
|SELECT vsize('2024-01-01'::date); | 8
89+
|SELECT vsize('2024-01-01 10:00:00'::timestamp); | 8
90+
|SELECT vsize('2024-01-01 10:00:00+08'::timestamptz); | 8
91+
|SELECT vsize(NULL::text); | NULL
92+
|SELECT vsize(repeat('a', 100000)); | 100000
93+
|====
94+
95+
For the same string, `VSIZE` and `LENGTHB` produce the same result:
96+
```
97+
SELECT vsize('abc') = lengthb('abc') AS same_as_lengthb;
98+
same_as_lengthb
99+
-----------------
100+
t
101+
```
102+
103+
Even when the data is stored compressed or TOASTed out-of-line, `VSIZE` still returns its uncompressed logical
104+
byte count:
105+
```
106+
CREATE TABLE vsize_big(a text);
107+
INSERT INTO vsize_big SELECT repeat('b', 200000) FROM generate_series(1, 10);
108+
SELECT bool_and(vsize(a) = lengthb(a)) AS toasted_matches_lengthb, min(vsize(a)) AS min_size FROM vsize_big;
109+
toasted_matches_lengthb | min_size
110+
-------------------------+----------
111+
t | 200000
112+
```

0 commit comments

Comments
 (0)