Skip to content

Commit 700c645

Browse files
authored
Merge pull request #298 from bigplaice/dbtimezone
add doc for dbtimezone feature
2 parents 6fad944 + 7f7238e commit 700c645

6 files changed

Lines changed: 920 additions & 0 deletions

File tree

CN/modules/ROOT/nav.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
** xref:master/oracle_compatibility/compat_create_index_online.adoc[22、索引 ONLINE 参数]
3232
** xref:master/oracle_compatibility/compat_stragg.adoc[23、STRAGG 函数]
3333
** xref:master/oracle_compatibility/compat_alter_index_unusable.adoc[24、禁用索引]
34+
** xref:master/oracle_compatibility/compat_dbtimezone.adoc[25、dbtimezone]
3435
* 容器化与云服务
3536
** 容器化指南
3637
*** xref:master/containerization/k8s_deployment.adoc[K8S部署]
@@ -114,6 +115,7 @@
114115
**** xref:master/oracle_builtin_functions/userenv.adoc[userenv]
115116
**** xref:master/oracle_builtin_functions/rawtohex.adoc[rawtohex]
116117
**** xref:master/oracle_builtin_functions/stragg.adoc[stragg]
118+
**** xref:master/oracle_builtin_functions/dbtimezone_impl.adoc[dbtimezone]
117119
*** xref:master/gb18030.adoc[国标GB18030]
118120
* 参考指南
119121
** xref:master/tools_reference.adoc[工具参考]
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
:sectnums:
2+
:sectnumlevels: 5
3+
4+
= DBTIMEZONE 实现说明
5+
6+
== 目的
7+
8+
本文档详细说明 IvorySQL 中 `DBTIMEZONE` 函数功能的实现原理。该功能提供一个数据库级、非会话级的固定时区值,通过 PostgreSQL 原生的 `ALTER DATABASE ... SET` 机制持久化,实现 Oracle 数据库 `DBTIMEZONE` 函数的语义。
9+
10+
== 实现说明
11+
12+
=== 系统分层架构
13+
14+
`DBTIMEZONE` 的实现分为四个层次,均位于 `contrib/ivorysql_ora`:
15+
16+
```
17+
┌───────────────────────────────────────────────────────────┐
18+
│ Layer 1: GUC 定义与权限层 (src/guc/guc.c + src/include/guc.h)│
19+
│ ─ 新增自定义 GUC ivorysql.dbtimezone(PGC_SUSET) │
20+
│ ─ check_dbtimezone():按 GucSource 拒绝会话内 SET/ALTER ROLE,│
21+
│ 只允许 ALTER DATABASE ... SET;并校验偏移/区域名格式 │
22+
└───────────────────────────────────────────────────────────┘
23+
24+
┌───────────────────────────────────────────────────────────┐
25+
│ Layer 2: 命令层拦截 (src/ivorysql_ora.c) │
26+
│ ─ ivorysql_ora_ProcessUtility()(既有 ProcessUtility_hook)│
27+
│ 新增 reject_alter_role_dbtimezone():在解析树层面直接 │
28+
│ 拦截 ALTER ROLE ... SET/ALTER ROLE ALL SET,早于 Layer 1 │
29+
│ 的 check hook 生效,命令本身直接报错,不留 catalog 残留 │
30+
└───────────────────────────────────────────────────────────┘
31+
32+
┌───────────────────────────────────────────────────────────┐
33+
│ Layer 3: C 函数层 (src/builtin_functions/ │
34+
│ datetime_datatype_functions.c) │
35+
│ ─ ora_dbtimezone():读取 ivorysql_dbtimezone 变量并返回 text │
36+
│ 与既有的 ora_sessiontimezone()(读取 session_timezone) │
37+
│ 紧邻,实现方式一致、语义刻意区分 │
38+
└───────────────────────────────────────────────────────────┘
39+
40+
┌───────────────────────────────────────────────────────────┐
41+
│ Layer 4: SQL 目录层 │
42+
│ (src/builtin_functions/builtin_functions--1.0.sql) │
43+
│ ─ CREATE FUNCTION sys.dbtimezone() ... STABLE │
44+
│ 紧邻既有的 sys.sessiontimezone() │
45+
└───────────────────────────────────────────────────────────┘
46+
```
47+
48+
本功能没有新增语法(不需要 Oracle 解析器/AST/目录列层面的改动)——`dbtimezone()` 是一个普通的 `STABLE` SQL 函数,配合一个自定义 GUC,复用 PostgreSQL 已有的 per-database 配置机制即可实现。
49+
50+
=== 设计方案:新增 GUC
51+
52+
GUC 本身不是"per-database 专属存储",而是复用了 PostgreSQL 对任意 GUC 都支持的 `ALTER DATABASE/ROLE ... SET` 通用机制(持久化在 `pg_db_role_setting` 系统表),没有为 `DBTIMEZONE` 单独设计目录字段。
53+
54+
=== GUC 定义与权限模型
55+
56+
==== 命名规范
57+
58+
GUC 名为 `ivorysql.dbtimezone`,与项目现有自定义 GUC 命名惯例保持一致。对应的 C 端变量为 `ivorysql_dbtimezone`。
59+
60+
==== 权限模型:只能通过 ALTER DATABASE 设置
61+
62+
`ALTER DATABASE dbname SET <guc> = value` 实际上有两层独立的权限检查:数据库对象本身的权限(是否有权 `ALTER` 这个库,owner 或超级用户即可)与 GUC 参数本身的权限(是否允许"设置"这个参数,与是否拥有该数据库无关)。`ivorysql.dbtimezone` 的 `context` 设为 `PGC_SUSET`,因此第二层默认只有超级用户能通过;若需要委派给普通角色,超级用户需额外执行 `GRANT SET ON PARAMETER ivorysql.dbtimezone TO <role>;`(对应 PostgreSQL 15+ 引入的 `pg_parameter_acl` 目录)。
63+
64+
仅设置 `context = PGC_SUSET` 不足以区分"会话内 `SET`"和"`ALTER DATABASE ... SET`"——两者内部同样以 `PGC_SUSET` 身份调用 `set_config_option()`。真正能区分调用来源的是 check hook 收到的 `GucSource source` 参数:
65+
66+
[cols="2,3,1"]
67+
|===
68+
| `GucSource` | 触发场景 | 是否允许
69+
70+
| `PGC_S_SESSION`
71+
| 会话内 `SET ivorysql.dbtimezone = ...;`
72+
| 拒绝
73+
74+
| `PGC_S_USER`
75+
| `ALTER ROLE rolename SET ...`(不区分数据库)
76+
| 拒绝
77+
78+
| `PGC_S_DATABASE_USER`
79+
| `ALTER ROLE rolename IN DATABASE dbname SET ...`
80+
| 拒绝
81+
82+
| `PGC_S_CLIENT`
83+
| 客户端连接选项(如 `PGOPTIONS`)
84+
| 拒绝
85+
86+
| `PGC_S_GLOBAL`
87+
| `ALTER ROLE ALL SET ...`(全局默认)
88+
| 拒绝
89+
90+
| `PGC_S_DATABASE`
91+
| `ALTER DATABASE dbname SET ...` 在连接建立时生效
92+
| 允许
93+
94+
| `PGC_S_TEST`
95+
| 执行 `ALTER DATABASE ... SET` 命令本身时的校验阶段
96+
| 允许(否则命令本身都执行不了)
97+
98+
| `PGC_S_DEFAULT` / `PGC_S_DYNAMIC_DEFAULT` / `PGC_S_FILE` / `PGC_S_ARGV` / `PGC_S_ENV_VAR`
99+
| 启动默认值、`postgresql.conf`、`postmaster` 命令行/环境变量
100+
| 允许(保证集群启动不受影响)
101+
102+
| `PGC_S_OVERRIDE`
103+
| 重放一个已校验过的值(如并行 worker 同步)
104+
| 允许(否则并行查询会报错)
105+
|===
106+
107+
==== `check_dbtimezone()` 实现
108+
109+
`contrib/ivorysql_ora/src/guc/guc.c`:
110+
111+
[source,c]
112+
----
113+
/* Backing variable for ivorysql.dbtimezone, read by dbtimezone(). */
114+
char *ivorysql_dbtimezone = NULL;
115+
116+
static bool
117+
check_dbtimezone(char **newval, void **extra, GucSource source)
118+
{
119+
char *str = *newval;
120+
121+
if (source == PGC_S_SESSION ||
122+
source == PGC_S_USER ||
123+
source == PGC_S_DATABASE_USER ||
124+
source == PGC_S_CLIENT ||
125+
source == PGC_S_GLOBAL)
126+
{
127+
GUC_check_errcode(ERRCODE_CANT_CHANGE_RUNTIME_PARAM);
128+
GUC_check_errmsg("parameter \"ivorysql.dbtimezone\" cannot be set");
129+
GUC_check_errdetail("\"ivorysql.dbtimezone\" can only be set with "
130+
"ALTER DATABASE ... SET, not within a session "
131+
"or per-role.");
132+
return false;
133+
}
134+
135+
/* [+-]HH:MI 格式校验,范围 -12:59 ~ +14:00(与 Oracle 一致) */
136+
if (strlen(str) == 6 && ... )
137+
{
138+
...
139+
}
140+
141+
/* 否则必须是合法的时区区域名(复用 pg_tzset() 校验) */
142+
if (!pg_tzset(str))
143+
{
144+
GUC_check_errdetail("\"%s\" is not a valid UTC offset (+/-HH:MI) "
145+
"or time zone name.", str);
146+
return false;
147+
}
148+
149+
return true;
150+
}
151+
152+
void
153+
IvorysqlOraDefineGucs(void)
154+
{
155+
DefineCustomStringVariable("ivorysql.dbtimezone",
156+
"Sets the database time zone reported by dbtimezone().",
157+
"Can only be set with ALTER DATABASE ... SET, not with a "
158+
"plain SET or ALTER ROLE ... SET. Requires superuser, or a "
159+
"role granted permission via "
160+
"GRANT SET ON PARAMETER ivorysql.dbtimezone TO <role>.",
161+
&ivorysql_dbtimezone,
162+
"+00:00",
163+
PGC_SUSET,
164+
0,
165+
check_dbtimezone,
166+
NULL,
167+
NULL);
168+
}
169+
----
170+
171+
`GUC_check_errcode(ERRCODE_CANT_CHANGE_RUNTIME_PARAM)` + `GUC_check_errmsg(...)` 把会话内 `SET` 被拒绝时的报错改成 `parameter "ivorysql.dbtimezone" cannot be set`,而非泛用的 `invalid value for parameter ...: "..."` ——这类拒绝的原因是"这个参数不能这样设置"而不是"这个值不合法",用专门的 errcode/errmsg 更准确地表达语义;格式/范围校验失败(`GUC_check_errdetail` 但不设 `GUC_check_errmsg`)则仍走默认的 `invalid value for parameter` 提示。
172+
173+
=== 命令层拦截:`ALTER ROLE ... SET`
174+
175+
`contrib/ivorysql_ora/src/ivorysql_ora.c` 已有一个 `ProcessUtility_hook`,在其中新增:
176+
177+
[source,c]
178+
----
179+
static void
180+
reject_alter_role_dbtimezone(Node *parsetree)
181+
{
182+
AlterRoleSetStmt *stmt;
183+
VariableSetStmt *setstmt;
184+
185+
if (nodeTag(parsetree) != T_AlterRoleSetStmt)
186+
return;
187+
188+
stmt = (AlterRoleSetStmt *) parsetree;
189+
setstmt = stmt->setstmt;
190+
191+
if (setstmt == NULL || setstmt->name == NULL)
192+
return; /* RESET ALL, or malformed */
193+
194+
if (setstmt->kind == VAR_RESET || setstmt->kind == VAR_RESET_ALL)
195+
return; /* clearing an override is always fine */
196+
197+
if (pg_strcasecmp(setstmt->name, "ivorysql.dbtimezone") == 0)
198+
ereport(ERROR,
199+
(errcode(ERRCODE_CANT_CHANGE_RUNTIME_PARAM),
200+
errmsg("parameter \"ivorysql.dbtimezone\" cannot be set"),
201+
errdetail("\"ivorysql.dbtimezone\" can only be set with "
202+
"ALTER DATABASE ... SET, not within a session "
203+
"or per-role."),
204+
errhint("Use ALTER DATABASE ... SET ivorysql.dbtimezone "
205+
"instead, or ALTER ROLE ... RESET "
206+
"ivorysql.dbtimezone to remove a stale per-role "
207+
"override.")));
208+
}
209+
----
210+
211+
并在 `ivorysql_ora_ProcessUtility()` 里,调用 `standard_ProcessUtility()`(或上一个已安装的 hook)之前插入这个检查——命令一旦匹配就直接 `ereport(ERROR, ...)`,`ALTER ROLE` 不会被执行到写 catalog 那一步。
212+
213+
=== SQL 函数层
214+
215+
==== `ora_dbtimezone()` C 函数
216+
217+
`contrib/ivorysql_ora/src/builtin_functions/datetime_datatype_functions.c`,紧邻既有的 `ora_sessiontimezone()`:
218+
219+
[source,c]
220+
----
221+
/*
222+
* returns the time zone of the database, as set by
223+
* ivorysql.dbtimezone. Unlike sessiontimezone(), this value is
224+
* independent of the session's TimeZone setting.
225+
*/
226+
Datum
227+
ora_dbtimezone(PG_FUNCTION_ARGS)
228+
{
229+
PG_RETURN_TEXT_P(cstring_to_text(ivorysql_dbtimezone));
230+
}
231+
----
232+
233+
对比 `ora_sessiontimezone()` 读取的是 `session_timezone`(会话级 `pg_tz *`),`ora_dbtimezone()` 直接读取 Layer 1 定义的 GUC 字符串。
234+
235+
==== 目录声明 `sys.dbtimezone()`
236+
237+
`contrib/ivorysql_ora/src/builtin_functions/builtin_functions--1.0.sql`,紧邻 `sys.sessiontimezone()`:
238+
239+
[source,sql]
240+
----
241+
CREATE FUNCTION sys.dbtimezone()
242+
RETURNS text
243+
AS 'MODULE_PATHNAME','ora_dbtimezone'
244+
LANGUAGE C
245+
STRICT
246+
STABLE;
247+
----
248+
249+
标记为 `STABLE` 而非 `IMMUTABLE`:返回值可能因 `ALTER DATABASE ... SET` 而改变(虽然一次连接内不会变),与 `sessiontimezone()` 的标记方式保持一致。该函数最终随扩展脚本 `ivorysql_ora--1.0.sql`分发。
250+
251+
=== 与 PG_PARSER 的关系
252+
253+
不同于 `ALTER INDEX ... UNUSABLE` 那种只存在于 Oracle 语法层的新语句,`dbtimezone()` 是普通的 SQL 函数,语法上不受 `compatible_db`/`ivorysql.compatible_mode` 限制:任何解析模式下都可以用 `sys.dbtimezone()` 显式限定调用;只有以裸函数名 `dbtimezone()` 调用(依赖 `search_path` 能解析到 `sys` 模式)时才与 Oracle 兼容模式的 search_path 行为相关,这属于 `sys` schema 本身的可见性问题,非本功能特有。
254+
255+
== 错误处理
256+
257+
=== 会话内 SET 被拒绝
258+
259+
[source,sql]
260+
----
261+
SET ivorysql.dbtimezone = '+08:00';
262+
-- ERROR: parameter "ivorysql.dbtimezone" cannot be set
263+
-- DETAIL: "ivorysql.dbtimezone" can only be set with ALTER DATABASE ... SET, not within a session or per-role.
264+
----
265+
错误由 `check_dbtimezone()` 中 `source == PGC_S_SESSION` 分支主动抛出。
266+
267+
=== ALTER ROLE ... SET 在命令层直接被拒绝
268+
269+
[source,sql]
270+
----
271+
ALTER ROLE myrole IN DATABASE mydb SET ivorysql.dbtimezone = '+09:00';
272+
-- ERROR: parameter "ivorysql.dbtimezone" cannot be set
273+
-- DETAIL: "ivorysql.dbtimezone" can only be set with ALTER DATABASE ... SET, not within a session or per-role.
274+
-- HINT: Use ALTER DATABASE ... SET ivorysql.dbtimezone instead, or ALTER ROLE ... RESET ivorysql.dbtimezone to remove a stale per-role override.
275+
276+
ALTER ROLE myrole SET ivorysql.dbtimezone = '+09:00'; -- 同样报错
277+
ALTER ROLE ALL SET ivorysql.dbtimezone = '+09:00'; -- 同样报错
278+
279+
ALTER ROLE myrole IN DATABASE mydb RESET ivorysql.dbtimezone; -- OK,不受影响
280+
----
281+
错误由 `reject_alter_role_dbtimezone()`(Layer 2,`ivorysql_ora.c` 的 `ProcessUtility_hook`)在解析树层面直接抛出,早于命令真正执行、早于 `pg_db_role_setting` 被写入。详见上文"命令层拦截"小节。
282+
283+
=== 非法值 / 超出范围偏移在 ALTER DATABASE 阶段即报错
284+
285+
[source,sql]
286+
----
287+
ALTER DATABASE mydb SET ivorysql.dbtimezone = 'not_a_zone';
288+
-- ERROR: invalid value for parameter "ivorysql.dbtimezone": "not_a_zone"
289+
-- DETAIL: "not_a_zone" is not a valid UTC offset (+/-HH:MI) or time zone name.
290+
291+
ALTER DATABASE mydb SET ivorysql.dbtimezone = '+15:00';
292+
-- ERROR: invalid value for parameter "ivorysql.dbtimezone": "+15:00"
293+
-- DETAIL: time zone offset "+15:00" is out of range for DBTIMEZONE (-12:59 to +14:00)
294+
----
295+
错误来自 `check_dbtimezone()` 的偏移/区域名格式校验分支,走默认的 `invalid value for parameter` 文案(未设置 `GUC_check_errmsg`)。
296+
297+
=== 未授权的普通用户执行 ALTER DATABASE
298+
299+
[source,sql]
300+
----
301+
\c mydb normal_user
302+
ALTER DATABASE mydb SET ivorysql.dbtimezone = '+08:00';
303+
-- ERROR: permission denied to set parameter "ivorysql.dbtimezone"
304+
----
305+
`context = PGC_SUSET` 且 `normal_user` 未被 `GRANT SET ON PARAMETER` 授权,权限检查在到达 `check_dbtimezone()` 之前就失败,因此报错文案是 PostgreSQL 通用的 GUC 权限错误,而非本功能自定义的错误信息。
306+
307+
== 已知限制
308+
309+
1. **未走 Oracle 的 `CREATE DATABASE ... TIME_ZONE` 语法**:当前只能通过 PostgreSQL 原生的 `ALTER DATABASE ... SET` 设置,没有在 IvorySQL 的 Oracle 语法层(`ora_gram.y`)增加对应的 `CREATE/ALTER DATABASE ... SET TIME_ZONE` 关键字兼容写法。
310+
2. **偏移 / 区域名格式未做精细区分**:Oracle 实际上对 `DBTIMEZONE`(数据库级)和 `SESSIONTIMEZONE`/`TIME_ZONE`(会话级)在偏移与区域名的允许范围上有细节差异,本实现为简化起见统一按"偏移或区域名皆可"处理。

0 commit comments

Comments
 (0)