1、概述
本文旨在解决基于海山数据库(He3DB)在兼容 MySQL 生态应用时,缺乏 DATE_ADD 和 DATE_SUB 函数的问题。我们通过在内核中引入自定义 C 函数和修改语法解析器(gram.y),实现了对 MySQL 独有的 11 种复合时间间隔格式(如 DAY_HOUR、MINUTE_SECOND 等)的完整支持。核心思想是将 MySQL 风格的函数调用和间隔字符串,高效地转换为 PostgreSQL 原生可识别的 Interval 类型,并利用 PG 已有的时间戳运算能力完成计算 。
2、背景与动机
2.1 兼容性挑战
MySQL 允许使用一个字符串值结合一个复合单位来表示时间间隔,例如 INTERVAL '1 10' DAY_HOUR。PostgreSQL 的 INTERVAL 类型无法直接解析这种格式,因此我们需要在内核中搭建一个翻译层。
2.2 兼容目标
兼容后,以下 SQL 语句(以及类似的 DATE_SUB 语句)应能在我们的系统中正常运行,并返回与 MySQL 一致的结果:
-- 1. 标准字符串格式 SELECT TIME('10:20:30') AS standard_time; -- 结果:10:20:30 -- 2. 数字(视为秒数) SELECT TIME(7200) AS sec_to_time; -- 结果:02:00:00(7200秒=2小时) -- 3. 时间戳 -- 5. 复合单位(以MINUTE_SECOND为例): SELECT DATE_ADD('2100-12-31 23:59:59', INTERVAL '1:1' MINUTE_SECOND); |
核心结构体与函数介绍
为了解析 MySQL 的复合间隔格式,我们在 src/backend/utils/adt/timestamp.c 文件中引入了两个关键的 C 语言函数:parse_mysql_compound_interval 和 date_add_mysql_compound。
3.1 核心解析函数parse_mysql_compound_interval
parse_mysql_compound_interval函数是兼容方案的核心。它负责解析MySQL风格的各种复杂时间间隔字符串。
函数原型:
static Interval * parse_mysql_compound_interval(const char *value, const char *unit) |
输入参数:
value: 字符串类型,代表要添加的时间间隔的具体值和格式,例如 '1:1', '-1 10', '1.999999'。
unit: 字符串类型,代表复合间隔单位,例如 'MINUTE_SECOND', 'DAY_HOUR', 'SECOND_MICROSECOND。
内部状态:
microseconds: 计算得到的时间部分(微秒精度)。
days: 计算得到的天数部分。
months: 计算得到的月数部分。
is_negative: 布尔标志,判断整个间隔是否为负数(以 '-' 开头)。
功能说明:
该函数通过将 unit 转换为大写后,与一系列预定义的复合单位关键词进行比对。一旦匹配成功,就使用 sscanf 或 strtod 按照预设的格式解析 value 字符串,并将最终结果统一转换为一个PostgreSQL原生的 Interval 结构体。这种设计巧妙地将MySQL的特殊语法“翻译”成了He3DB/PG能够理解的内在表示。
3.2 主接口函数date_add_mysql_compound
这是暴露给数据库执行器的C语言函数接口,遵循PostgreSQL的函数管理器约定。
函数原型:
Datum date_add_mysql_compound(PG_FUNCTION_ARGS) |
关键流程:
- 使用 PG_GETARG_* 宏从参数列表中提取出输入的日期时间戳、间隔值和间隔单位。
- 调用上述的 parse_mysql_compound_interval 函数,将字符串形式的间隔解析为结构化的 Interval 对象。
- 通过 DirectFunctionCall2 直接调用He3DB内核中已有的 timestamp_pl_interval 函数,完成最终的日期时间加法运算。
- 使用 PG_RETURN_TIMESTAMP 返回结果。
核心代码解析
我们通过扩展语法解析器和新增功能函数来实现对MySQL DATE_ADD 的兼容。
-
- 语法解析器扩展(gram.y)
为了能够识别 DATE_ADD 关键字以及一系列复杂的复合间隔单位,我们必须在其语法解析器中进行注册和定义。
代码片段:
/* 1. 令牌声明:告知词法分析器这些是新关键字 */ %token DATE_ADD %token SECOND_MICROSECOND MINUTE_SECOND ... YEAR_MONTH // 此处省略其他单位 /* 2. 类型关联 */ %type <str> mysql_compound_interval_unit /* 3. 语法规则定义:指明这些关键字在SQL中的组合方式 */ mysql_compound_interval_unit: SECOND_MICROSECOND { $$ = "SECOND_MICROSECOND"; } | MINUTE_SECOND { $$ = "MINUTE_SECOND"; } // ... 其他单位的规则 ; |
**令牌声明:**将 DATE_ADD 及所有复合间隔单位(如 MINUTE_SECOND)定义为新的语法标记(Token),这样词法分析阶段才能正确识别它们。
**规则定义:**这部分建立了语法规则:当在SQL中遇到 SECOND_MICROSECOND 这个词时,应生成一个值为 "SECOND_MICROSECOND" 的字符串。这个字符串正是后续 parse_mysql_compound_interval 函数赖以工作的 unit 参数。
-
- 语法规则重写(gram.y)
在 src/backend/parser/gram.y 文件中,我们对 func_expr_common_subexpr 规则进行了核心修改,实现 SQL 语句的语法转换(Rewriting)。我们针对 DATE_ADD 定义了三条独立的语法规则,以覆盖 MySQL 支持的所有间隔类型:复合间隔、标准整数间隔、标准字符串间隔。
4.2.1 规则 1: MySQL 复合间隔 (字符串值 + 复合单位)
这是针对我们新增功能的规则。它直接将调用重写为对我们自定义的 C 函数 date_add_mysql_compound 的调用。
/* MySQL DATE_ADD with compound interval (字符串值 + 复合单位) */ | DATE_ADD '(' a_expr ',' INTERVAL Sconst mysql_compound_interval_unit ')' { FuncCall *n = makeFuncCall( SystemFuncName("date_add_mysql_compound"), // 调用自定义C函数 list_make3($3, // 基础时间表达式 makeStringConst($6, @6), // 间隔值 Sconst (字符串) makeStringConst($7, @7)), // 间隔单位 (字符串) COERCE_EXPLICIT_CALL, @1); $$ = (Node *) n; } |
4.2.2 规则 2: MySQL 标准间隔 (整数值 + 标准单位)
这是针对标准间隔但值为整数的情况(如 INTERVAL 1 DAY)。由于 INTERVAL 构造器只接受字符串,我们需要手动将整数 $6 转换为字符串,然后构建 TypeCast 节点。
/* MySQL DATE_ADD with standard interval (整数值 + 标准单位) */ | DATE_ADD '(' a_expr ',' INTERVAL Iconst opt_interval ')' { A_Const *iconst; TypeName *typename; TypeCast *tc; FuncCall *n; char buf[32]; /* 1. 将整数 Iconst ($6) 转换为字符串 */ snprintf(buf, sizeof(buf), "%d", (int)$6); iconst = makeNode(A_Const); iconst->val.sval.type = T_String; iconst->val.sval.sval = pstrdup(buf); iconst->location = @6; /* 2. 创建 interval 类型 TypeCast 节点 */ typename = SystemTypeName("interval"); if ($7 != NIL) typename->typmods = $7; tc = makeNode(TypeCast); tc->arg = (Node *) iconst; // TypeCast 的输入是字符串常量 tc->typeName = typename; /* 3. 重写为调用 PG 内部 date_add 函数 (即 + 运算符的别名) */ n = makeFuncCall(SystemFuncName("date_add"), list_make2($3, (Node *) tc), COERCE_EXPLICIT_CALL, @1); $$ = (Node *) n; } |
4.2.3 规则 3: MySQL 标准间隔 (字符串值 + 标准单位)
这是针对标准间隔但值为字符串的情况(如 INTERVAL '1' DAY)。逻辑与规则 2 类似,但不需要将整数转换为字符串。
/* MySQL DATE_ADD with standard interval (字符串值 + 标准单位) */ | DATE_ADD '(' a_expr ',' INTERVAL Sconst opt_interval ')' { // ... 逻辑与规则 2 相同,但直接使用 $6 (字符串) 作为 A_Const 的值 ... /* 1. 使用 $6 (字符串) 作为 A_Const 的值 */ // ... /* 2. 创建 interval 类型 TypeCast 节点 */ // ... /* 3. 重写为调用 PG 内部 date_add 函数 */ // ... } |
4.2.4 DATE_SUB的隐式反向处理
DATE_SUB 的兼容性是通过在语法解析阶段,对间隔值字符串执行取反操作后,再调用 date_add_mysql_compound 实现的。
对于复合间隔: 对 Sconst 字符串进行负号添加/移除操作。
对于标准间隔: 对 Iconst 整数值进行取反操作,或对 Sconst 字符串进行负号处理。
这样,所有 DATE_SUB 的调用都被转换为 DATE_ADD 加上一个负间隔。
复合间隔解析核心逻辑
parse_mysql_compound_interval 函数内部是一个大型的条件分支,针对每种支持的复合单位进行精准解析。
以 MINUTE_SECOND 为例:
else if (strcmp(unit_upper, "MINUTE_SECOND") == 0) { /* 格式: '1:30' */ int minutes, seconds; if (sscanf(parse_value, "%d:%d", &minutes, &seconds) == 2) { microseconds microseconds = (minutes * USECS_PER_MINUTE) + (seconds * USECS_PER_SEC); } else { goto error; // 格式不匹配,跳转到错误处理 } |
代码解析:
格式匹配:对于 MINUTE_SECOND 单位,期望的输入格式是 '分钟:秒',例如 '1:1' 代表1分1秒。
数值提取:使用 sscanf 函数,严格按照 "%d:%d" 的模板去拆分 parse_value 字符串。
单位换算:这是实现精确计算的基石。我们将分钟和秒全部统一换算为微秒这一基准单位。
-
- USECS_PER_MINUTE:每分钟对应的微秒数(60 * 1,000,000)。
- USECS_PER_SEC:每秒对应的微秒数(1,000,000)。
错误处理:如果字符串格式不符合预期(例如,对于 MINUTE_SECOND 却传入了 '1.5'),则流程会跳转到 error标签,抛出清晰的错误信息,引导用户使用正确的格式。
整体内核处理流程解析
5.1 SQL 输入与初步解析
-
用户输入 SQL 语句: 流程始于用户执行 SELECT DATE_ADD('...', INTERVAL '...' UNIT) 形式的 SQL 命令。
-
PG 内核:词法与语法解析: PostgreSQL 内核的词法分析器 (Lexer) 和语法分析器 (Parser) 开始工作,识别 DATE_ADD 函数和 INTERVAL 子句。
5.2 语法规则匹配与分支决策
- 是否匹配 DATE_ADD/DATE_SUB 规则?
在 src/backend/parser/gram.y 文件中,内核匹配我们在 func_expr_common_subexpr 规则中添加的 DATE_ADD/DATE_SUB 规则。
- 识别单位 UNIT 是复合单位及标准 PG 格式吗?
这一关键的菱形判断节点,根据语法解析的结果来区分间隔单位的类型,将处理流程分为两条路径:
路径 A (标准 PG 格式): 如果单位是 PG 原生支持的单位(例如 DAY, MONTH),流程进入左侧路径。
路径 B (复合单位): 如果单位是 MySQL 复合格式(例如 MINUTE_SECOND),流程进入右侧路径。
5.3 路径 A:标准间隔处理(左侧)
如果单位是标准 PG 格式:
-
Grammar 转换:重写为 timestamp +/- interval 运算符: 语法解析器将整个 DATE_ADD 调用重写为一个 PostgreSQL 内部表达式,即 timestamp + interval 运算符表达式。这一步利用了 PostgreSQL 强大的运算符重载机制。
-
执行 PG 原生运算符: 转换完成后,内核直接执行 timestamp + interval 运算,无需额外的自定义代码。
5.4 路径 B:复合间隔处理(右侧)
如果单位是 MySQL 复合单位,流程进入我们自定义的兼容逻辑:
- Grammar 转换:重写为 date_add_mysql_compound 函数调用:无论是 DATE_ADD 还是经过取反处理的 DATE_SUB,最终都被重写为一个统一的自定义函数调用:date_add_mysql_compound(Timestamp, ValueStr, UnitStr)。
- 调用 C 函数 date_add_mysql_compound: 执行器调用我们在 timestamp.c 中注册的 C 语言函数。
- 调用 parse_mysql_compound_interval: 在 date_add_mysql_compound 内部,立即调用核心解析函数 parse_mysql_compound_interval。
- C 函数:解析 ValueStr,转换为 PG Interval 结构体:
该函数根据 UnitStr 对 ValueStr 进行精确解析(如 sscanf 或 strtod)。将解析出的年、月、日、时、分、秒、微秒转换为 PG 内部所需的 Interval 结构体(months、days、time)。
- 调用 PG 内置 timestamp_pl_interval 运算:
date_add_mysql_compound 函数随后调用 PG 内置的 timestamp_pl_interval 函数,将原始时间戳和新生成的 Interval 结构体进行相加运算。
5.5 结果返回
执行 PG 原生运算 / 返回结果: 无论是通过路径 A 还是路径 B,最终都汇聚到 PG 原生的时间戳运算能力,计算出结果并返回给用户。
总结
本方案通过语法扩展 + 复合间隔解析 + 原生运算的三层架构,成功完整复现了 MySQL DATE_ADD 函数的功能。通过本方案,项目成功降低了从 MySQL 迁移至 海山数据库(He3DB) 的业务改造成本,为多数据库兼容提供了可复用的技术范式。