DBTIMEZONE 实现说明
1. 目的
本文档详细说明 IvorySQL 中 DBTIMEZONE 函数功能的实现原理。该功能提供一个数据库级、非会话级的固定时区值,通过 PostgreSQL 原生的 ALTER DATABASE … SET 机制持久化,实现 Oracle 数据库 DBTIMEZONE 函数的语义。
2. 实现说明
2.1. 系统分层架构
DBTIMEZONE 的实现分为四个层次,均位于 contrib/ivorysql_ora:
┌───────────────────────────────────────────────────────────┐
│ Layer 1: GUC 定义与权限层 (src/guc/guc.c + src/include/guc.h)│
│ ─ 新增自定义 GUC ivorysql.dbtimezone(PGC_SUSET) │
│ ─ check_dbtimezone():按 GucSource 拒绝会话内 SET/ALTER ROLE,│
│ 只允许 ALTER DATABASE ... SET;并校验偏移/区域名格式 │
└───────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────┐
│ Layer 2: 命令层拦截 (src/ivorysql_ora.c) │
│ ─ ivorysql_ora_ProcessUtility()(既有 ProcessUtility_hook)│
│ 新增 reject_alter_role_dbtimezone():在解析树层面直接 │
│ 拦截 ALTER ROLE ... SET/ALTER ROLE ALL SET,早于 Layer 1 │
│ 的 check hook 生效,命令本身直接报错,不留 catalog 残留 │
└───────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────┐
│ Layer 3: C 函数层 (src/builtin_functions/ │
│ datetime_datatype_functions.c) │
│ ─ ora_dbtimezone():读取 ivorysql_dbtimezone 变量并返回 text │
│ 与既有的 ora_sessiontimezone()(读取 session_timezone) │
│ 紧邻,实现方式一致、语义刻意区分 │
└───────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────┐
│ Layer 4: SQL 目录层 │
│ (src/builtin_functions/builtin_functions--1.0.sql) │
│ ─ CREATE FUNCTION sys.dbtimezone() ... STABLE │
│ 紧邻既有的 sys.sessiontimezone() │
└───────────────────────────────────────────────────────────┘
本功能没有新增语法(不需要 Oracle 解析器/AST/目录列层面的改动)——dbtimezone() 是一个普通的 STABLE SQL 函数,配合一个自定义 GUC,复用 PostgreSQL 已有的 per-database 配置机制即可实现。
2.2. 设计方案:新增 GUC
GUC 本身不是"per-database 专属存储",而是复用了 PostgreSQL 对任意 GUC 都支持的 ALTER DATABASE/ROLE … SET 通用机制(持久化在 pg_db_role_setting 系统表),没有为 DBTIMEZONE 单独设计目录字段。
2.3. GUC 定义与权限模型
2.3.2. 权限模型:只能通过 ALTER DATABASE 设置
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 目录)。
仅设置 context = PGC_SUSET 不足以区分"会话内 SET`"和"`ALTER DATABASE … SET`"——两者内部同样以 `PGC_SUSET 身份调用 set_config_option()。真正能区分调用来源的是 check hook 收到的 GucSource source 参数:
GucSource |
触发场景 | 是否允许 |
|---|---|---|
|
会话内 |
拒绝 |
|
|
拒绝 |
|
|
拒绝 |
|
客户端连接选项(如 |
拒绝 |
|
|
拒绝 |
|
|
允许 |
|
执行 |
允许(否则命令本身都执行不了) |
|
启动默认值、 |
允许(保证集群启动不受影响) |
|
重放一个已校验过的值(如并行 worker 同步) |
允许(否则并行查询会报错) |
2.3.3. check_dbtimezone() 实现
contrib/ivorysql_ora/src/guc/guc.c:
/* Backing variable for ivorysql.dbtimezone, read by dbtimezone(). */
char *ivorysql_dbtimezone = NULL;
static bool
check_dbtimezone(char **newval, void **extra, GucSource source)
{
char *str = *newval;
if (source == PGC_S_SESSION ||
source == PGC_S_USER ||
source == PGC_S_DATABASE_USER ||
source == PGC_S_CLIENT ||
source == PGC_S_GLOBAL)
{
GUC_check_errcode(ERRCODE_CANT_CHANGE_RUNTIME_PARAM);
GUC_check_errmsg("parameter \"ivorysql.dbtimezone\" cannot be set");
GUC_check_errdetail("\"ivorysql.dbtimezone\" can only be set with "
"ALTER DATABASE ... SET, not within a session "
"or per-role.");
return false;
}
/* [+-]HH:MI 格式校验,范围 -12:59 ~ +14:00(与 Oracle 一致) */
if (strlen(str) == 6 && ... )
{
...
}
/* 否则必须是合法的时区区域名(复用 pg_tzset() 校验) */
if (!pg_tzset(str))
{
GUC_check_errdetail("\"%s\" is not a valid UTC offset (+/-HH:MI) "
"or time zone name.", str);
return false;
}
return true;
}
void
IvorysqlOraDefineGucs(void)
{
DefineCustomStringVariable("ivorysql.dbtimezone",
"Sets the database time zone reported by dbtimezone().",
"Can only be set with ALTER DATABASE ... SET, not with a "
"plain SET or ALTER ROLE ... SET. Requires superuser, or a "
"role granted permission via "
"GRANT SET ON PARAMETER ivorysql.dbtimezone TO <role>.",
&ivorysql_dbtimezone,
"+00:00",
PGC_SUSET,
0,
check_dbtimezone,
NULL,
NULL);
}
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 提示。
2.4. 命令层拦截:ALTER ROLE … SET
contrib/ivorysql_ora/src/ivorysql_ora.c 已有一个 ProcessUtility_hook,在其中新增:
static void
reject_alter_role_dbtimezone(Node *parsetree)
{
AlterRoleSetStmt *stmt;
VariableSetStmt *setstmt;
if (nodeTag(parsetree) != T_AlterRoleSetStmt)
return;
stmt = (AlterRoleSetStmt *) parsetree;
setstmt = stmt->setstmt;
if (setstmt == NULL || setstmt->name == NULL)
return; /* RESET ALL, or malformed */
if (setstmt->kind == VAR_RESET || setstmt->kind == VAR_RESET_ALL)
return; /* clearing an override is always fine */
if (pg_strcasecmp(setstmt->name, "ivorysql.dbtimezone") == 0)
ereport(ERROR,
(errcode(ERRCODE_CANT_CHANGE_RUNTIME_PARAM),
errmsg("parameter \"ivorysql.dbtimezone\" cannot be set"),
errdetail("\"ivorysql.dbtimezone\" can only be set with "
"ALTER DATABASE ... SET, not within a session "
"or per-role."),
errhint("Use ALTER DATABASE ... SET ivorysql.dbtimezone "
"instead, or ALTER ROLE ... RESET "
"ivorysql.dbtimezone to remove a stale per-role "
"override.")));
}
并在 ivorysql_ora_ProcessUtility() 里,调用 standard_ProcessUtility()(或上一个已安装的 hook)之前插入这个检查——命令一旦匹配就直接 ereport(ERROR, …),ALTER ROLE 不会被执行到写 catalog 那一步。
2.5. SQL 函数层
2.5.1. ora_dbtimezone() C 函数
contrib/ivorysql_ora/src/builtin_functions/datetime_datatype_functions.c,紧邻既有的 ora_sessiontimezone():
/*
* returns the time zone of the database, as set by
* ivorysql.dbtimezone. Unlike sessiontimezone(), this value is
* independent of the session's TimeZone setting.
*/
Datum
ora_dbtimezone(PG_FUNCTION_ARGS)
{
PG_RETURN_TEXT_P(cstring_to_text(ivorysql_dbtimezone));
}
对比 ora_sessiontimezone() 读取的是 session_timezone(会话级 pg_tz *),ora_dbtimezone() 直接读取 Layer 1 定义的 GUC 字符串。
2.5.2. 目录声明 sys.dbtimezone()
contrib/ivorysql_ora/src/builtin_functions/builtin_functions—1.0.sql,紧邻 sys.sessiontimezone():
CREATE FUNCTION sys.dbtimezone()
RETURNS text
AS 'MODULE_PATHNAME','ora_dbtimezone'
LANGUAGE C
STRICT
STABLE;
标记为 STABLE 而非 IMMUTABLE:返回值可能因 ALTER DATABASE … SET 而改变(虽然一次连接内不会变),与 sessiontimezone() 的标记方式保持一致。该函数最终随扩展脚本 `ivorysql_ora—1.0.sql`分发。
3. 错误处理
3.1. 会话内 SET 被拒绝
SET ivorysql.dbtimezone = '+08:00';
-- ERROR: parameter "ivorysql.dbtimezone" cannot be set
-- DETAIL: "ivorysql.dbtimezone" can only be set with ALTER DATABASE ... SET, not within a session or per-role.
错误由 check_dbtimezone() 中 source == PGC_S_SESSION 分支主动抛出。
3.2. ALTER ROLE … SET 在命令层直接被拒绝
ALTER ROLE myrole IN DATABASE mydb SET ivorysql.dbtimezone = '+09:00';
-- ERROR: parameter "ivorysql.dbtimezone" cannot be set
-- DETAIL: "ivorysql.dbtimezone" can only be set with ALTER DATABASE ... SET, not within a session or per-role.
-- HINT: Use ALTER DATABASE ... SET ivorysql.dbtimezone instead, or ALTER ROLE ... RESET ivorysql.dbtimezone to remove a stale per-role override.
ALTER ROLE myrole SET ivorysql.dbtimezone = '+09:00'; -- 同样报错
ALTER ROLE ALL SET ivorysql.dbtimezone = '+09:00'; -- 同样报错
ALTER ROLE myrole IN DATABASE mydb RESET ivorysql.dbtimezone; -- OK,不受影响
错误由 reject_alter_role_dbtimezone()(Layer 2,ivorysql_ora.c 的 ProcessUtility_hook)在解析树层面直接抛出,早于命令真正执行、早于 pg_db_role_setting 被写入。详见上文"命令层拦截"小节。
3.3. 非法值 / 超出范围偏移在 ALTER DATABASE 阶段即报错
ALTER DATABASE mydb SET ivorysql.dbtimezone = 'not_a_zone';
-- ERROR: invalid value for parameter "ivorysql.dbtimezone": "not_a_zone"
-- DETAIL: "not_a_zone" is not a valid UTC offset (+/-HH:MI) or time zone name.
ALTER DATABASE mydb SET ivorysql.dbtimezone = '+15:00';
-- ERROR: invalid value for parameter "ivorysql.dbtimezone": "+15:00"
-- DETAIL: time zone offset "+15:00" is out of range for DBTIMEZONE (-12:59 to +14:00)
错误来自 check_dbtimezone() 的偏移/区域名格式校验分支,走默认的 invalid value for parameter 文案(未设置 GUC_check_errmsg)。
3.4. 未授权的普通用户执行 ALTER DATABASE
\c mydb normal_user
ALTER DATABASE mydb SET ivorysql.dbtimezone = '+08:00';
-- ERROR: permission denied to set parameter "ivorysql.dbtimezone"
context = PGC_SUSET 且 normal_user 未被 GRANT SET ON PARAMETER 授权,权限检查在到达 check_dbtimezone() 之前就失败,因此报错文案是 PostgreSQL 通用的 GUC 权限错误,而非本功能自定义的错误信息。
4. 已知限制
-
未走 Oracle 的
CREATE DATABASE … TIME_ZONE语法:当前只能通过 PostgreSQL 原生的ALTER DATABASE … SET设置,没有在 IvorySQL 的 Oracle 语法层(ora_gram.y)增加对应的CREATE/ALTER DATABASE … SET TIME_ZONE关键字兼容写法。 -
偏移 / 区域名格式未做精细区分:Oracle 实际上对
DBTIMEZONE(数据库级)和SESSIONTIMEZONE/TIME_ZONE(会话级)在偏移与区域名的允许范围上有细节差异,本实现为简化起见统一按"偏移或区域名皆可"处理。