Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CN/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
**** 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/vsize.adoc[vsize]
*** xref:master/gb18030.adoc[国标GB18030]
* 参考指南
** xref:master/tools_reference.adoc[工具参考]
Expand Down
103 changes: 103 additions & 0 deletions CN/modules/ROOT/pages/master/oracle_builtin_functions/vsize.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@

:sectnums:
:sectnumlevels: 5


= **功能概述**

IvorySQL提供兼容Oracle内置函数 ```VSIZE('parameter')``` ,用于返回参数在内部存储表示中所占用的字节数,
即返回参数的“存储大小”。对于字符类型数据,返回其字节长度(不含变长头);对于定长类型(如 NUMBER、
BOOLEAN、DATE、TIMESTAMP 等),返回该类型的存储宽度;参数为 NULL 时返回 NULL。

== 实现原理

VSIZE 需要接受任意数据类型的入参(字符、数值、布尔、日期时间等),并根据其存储方式(变长 varlena 类型
或定长类型)分别计算字节数,这类与具体类型存储细节相关的逻辑无法用简单的 SQL 包装实现,因此本次开发
使用 C 语言编写扩展函数 `ora_vsize`,注册为:

```
sys.vsize(anycompatible) RETURNS int4
```

使用 `anycompatible` 伪类型作为参数类型,使得该函数可以接受任意数据类型的入参,未显式指定类型的字符串
字面量(如 `'abc'`)会按照 PostgreSQL 的默认规则解析为 text 类型,与 Oracle 中 `VSIZE('abc')` 的行为一致。
函数声明为 `STRICT`,因此入参为 NULL 时直接返回 NULL,无需在函数体中额外处理。

函数实现位于 `contrib/ivorysql_ora/src/builtin_functions/misc_functions.c` 中的 `ora_vsize`:

* 首次调用时,通过 `get_fn_expr_argtype()` 取得实参的真实类型 OID,并调用 `get_typlen()` 获取该类型的
`typlen`(存储长度),缓存到 `fcinfo->flinfo->fn_extra`,避免同一查询中重复查目录;后续调用直接从
`fn_extra` 中读取缓存值。
* `typlen == -1`:表示变长(varlena)类型,如 text、varchar2、numeric 等。此时调用
`toast_raw_datum_size()` 获取该值的逻辑(解压缩后)大小 —— 该函数会统一处理 1 字节/4 字节头、压缩存储
以及 TOAST 外部存储等各种情况,返回值统一按 4 字节头换算,因此再减去 `VARHDRSZ` 即可得到不含头部的
有效数据字节数。这与 `octet_length()` 计算字节长度所采用的方式一致,因此 `VSIZE('abc') = LENGTHB('abc')`。
* `typlen == -2`:表示 cstring 类型,返回其字符串长度加 1(含结尾 `\0`)。
* 其余情况:为定长类型,直接返回该类型的 `typlen` 作为存储宽度(例如 int4 为 4,int8/float8/date/
timestamp/timestamptz 均为 8,boolean 为 1)。

具体函数注册在 `builtin_functions--1.0.sql` 中完成:
```sql
/* VSIZE */
/*
* VSIZE: Oracle-compatible function returning the number of bytes in the
* internal representation of the argument. Returns NULL for NULL input.
* For varlena types the logical (decompressed) data size, excluding the
* varlena header, is returned; for fixed-width types the storage width is
* returned.
*
* The anycompatible pseudo-type accepts a value of any data type, and an
* untyped string literal is resolved to text, so VSIZE('abc') works just
* like in Oracle.
*/
CREATE FUNCTION sys.vsize(anycompatible)
RETURNS int4
AS 'MODULE_PATHNAME', 'ora_vsize'
LANGUAGE C
STRICT
IMMUTABLE;
/* End - VSIZE */
```

== VSIZE 典型用例
[cols="8,2"]
|====
|*用例语句*|*返回值*
|SELECT vsize('abc'); | 3
|SELECT vsize(CAST('abc' AS VARCHAR2)); | 3
|SELECT vsize('abc'::varchar); | 3
|SELECT vsize('abc'::char(10)); | 10
|SELECT vsize('你好'::text); | 6
|SELECT vsize(0::number); | 2
|SELECT vsize(1::number); | 4
|SELECT vsize(123::number); | 4
|SELECT vsize(1.23::number); | 6
|SELECT vsize(123::int4); | 4
|SELECT vsize(123::int8); | 8
|SELECT vsize(1.23::float8); | 8
|SELECT vsize('NaN'::float8); | 8
|SELECT vsize(true); | 1
|SELECT vsize('2024-01-01'::date); | 8
|SELECT vsize('2024-01-01 10:00:00'::timestamp); | 8
|SELECT vsize('2024-01-01 10:00:00+08'::timestamptz); | 8
|SELECT vsize(NULL::text); | NULL
|SELECT vsize(repeat('a', 100000)); | 100000
|====

对于同一字符串,`VSIZE` 与 `LENGTHB` 的结果一致:
```
SELECT vsize('abc') = lengthb('abc') AS same_as_lengthb;
same_as_lengthb
-----------------
t
```

即使数据经过压缩存储或 TOAST 到行外,`VSIZE` 仍然返回其未压缩的逻辑字节数:
```
CREATE TABLE vsize_big(a text);
INSERT INTO vsize_big SELECT repeat('b', 200000) FROM generate_series(1, 10);
SELECT bool_and(vsize(a) = lengthb(a)) AS toasted_matches_lengthb, min(vsize(a)) AS min_size FROM vsize_big;
toasted_matches_lengthb | min_size
-------------------------+----------
t | 200000
```
1 change: 1 addition & 0 deletions EN/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,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/vsize_en.adoc[vsize]
** xref:master/gb18030.adoc[GB18030 Character Set]
* Reference
** xref:master/tools_reference.adoc[Tool Reference]
Expand Down
112 changes: 112 additions & 0 deletions EN/modules/ROOT/pages/master/oracle_builtin_functions/vsize_en.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@

:sectnums:
:sectnumlevels: 5


= **Feature Overview**

IvorySQL provides the Oracle-compatible built-in function ```VSIZE('parameter')```, which returns the number of
bytes occupied by the argument in its internal storage representation, i.e. the "storage size" of the argument.
For character types, it returns the byte length (excluding the variable-length header); for fixed-width types
(such as NUMBER, BOOLEAN, DATE, TIMESTAMP, etc.), it returns the storage width of that type; when the argument
is NULL, it returns NULL.

== Implementation

VSIZE needs to accept an argument of any data type (character, numeric, boolean, date/time, etc.) and compute
the byte count differently depending on its storage representation (variable-length varlena type or fixed-width
type). This kind of logic, which depends on type-specific storage details, cannot be implemented with a simple
SQL wrapper, so this feature is implemented as a C-language extension function `ora_vsize`, registered as:

```
sys.vsize(anycompatible) RETURNS int4
```

Using the `anycompatible` pseudo-type as the parameter type allows the function to accept an argument of any
data type; an untyped string literal (such as `'abc'`) is resolved to the text type following PostgreSQL's
default rules, matching the behavior of `VSIZE('abc')` in Oracle. The function is declared `STRICT`, so a NULL
argument directly returns NULL without any extra handling in the function body.

The function is implemented as `ora_vsize` in `contrib/ivorysql_ora/src/builtin_functions/misc_functions.c`:

* On the first call, the actual type OID of the argument is obtained via `get_fn_expr_argtype()`, and
`get_typlen()` is called to get that type's `typlen` (storage length), which is cached in
`fcinfo->flinfo->fn_extra` to avoid repeated catalog lookups within the same query; subsequent calls read the
cached value directly from `fn_extra`.
* `typlen == -1`: indicates a variable-length (varlena) type, such as text, varchar2, or numeric. In this case,
`toast_raw_datum_size()` is called to get the logical (decompressed) size of the value -- this function
uniformly handles 1-byte/4-byte headers, compressed storage, and TOASTed (out-of-line) storage, and its return
value is always normalized to the 4-byte header convention, so subtracting `VARHDRSZ` yields the payload byte
count excluding the header. This is the same approach used by `octet_length()` to compute byte length, which
is why `VSIZE('abc') = LENGTHB('abc')`.
* `typlen == -2`: indicates the cstring type, and the string length plus 1 (including the terminating `\0`) is
returned.
* Otherwise: the type is fixed-width, and its `typlen` is returned directly as the storage width (for example,
int4 is 4; int8/float8/date/timestamp/timestamptz are all 8; boolean is 1).

The function registration is done in `builtin_functions--1.0.sql`:
```sql
/* VSIZE */
/*
* VSIZE: Oracle-compatible function returning the number of bytes in the
* internal representation of the argument. Returns NULL for NULL input.
* For varlena types the logical (decompressed) data size, excluding the
* varlena header, is returned; for fixed-width types the storage width is
* returned.
*
* The anycompatible pseudo-type accepts a value of any data type, and an
* untyped string literal is resolved to text, so VSIZE('abc') works just
* like in Oracle.
*/
CREATE FUNCTION sys.vsize(anycompatible)
RETURNS int4
AS 'MODULE_PATHNAME', 'ora_vsize'
LANGUAGE C
STRICT
IMMUTABLE;
/* End - VSIZE */
```

== Typical VSIZE examples
[cols="8,2"]
|====
|*Example statement*|*Return value*
|SELECT vsize('abc'); | 3
|SELECT vsize(CAST('abc' AS VARCHAR2)); | 3
|SELECT vsize('abc'::varchar); | 3
|SELECT vsize('abc'::char(10)); | 10
|SELECT vsize('你好'::text); | 6
|SELECT vsize(0::number); | 2
|SELECT vsize(1::number); | 4
|SELECT vsize(123::number); | 4
|SELECT vsize(1.23::number); | 6
|SELECT vsize(123::int4); | 4
|SELECT vsize(123::int8); | 8
|SELECT vsize(1.23::float8); | 8
|SELECT vsize('NaN'::float8); | 8
|SELECT vsize(true); | 1
|SELECT vsize('2024-01-01'::date); | 8
|SELECT vsize('2024-01-01 10:00:00'::timestamp); | 8
|SELECT vsize('2024-01-01 10:00:00+08'::timestamptz); | 8
|SELECT vsize(NULL::text); | NULL
|SELECT vsize(repeat('a', 100000)); | 100000
|====

For the same string, `VSIZE` and `LENGTHB` produce the same result:
```
SELECT vsize('abc') = lengthb('abc') AS same_as_lengthb;
same_as_lengthb
-----------------
t
```

Even when the data is stored compressed or TOASTed out-of-line, `VSIZE` still returns its uncompressed logical
byte count:
```
CREATE TABLE vsize_big(a text);
INSERT INTO vsize_big SELECT repeat('b', 200000) FROM generate_series(1, 10);
SELECT bool_and(vsize(a) = lengthb(a)) AS toasted_matches_lengthb, min(vsize(a)) AS min_size FROM vsize_big;
toasted_matches_lengthb | min_size
-------------------------+----------
t | 200000
```
Loading