大云海山数据库(He3DB)兼容 MySQL 日期函数: DATE_ADD的内核实现与复合间隔支持

0 阅读9分钟

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. 时间戳
SELECT time('2023-12-25 15:30:45'::timestamp); -- 15:30:45
-- 4. 日期
SELECT time('2023-12-25'::date); -- 00:00:00

-- 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)

关键流程:

  1. 使用 PG_GETARG_* 宏从参数列表中提取出输入的日期时间戳、间隔值和间隔单位。
  2. 调用上述的 parse_mysql_compound_interval 函数,将字符串形式的间隔解析为结构化的 Interval 对象。
  3. 通过 DirectFunctionCall2 直接调用He3DB内核中已有的 timestamp_pl_interval 函数,完成最终的日期时间加法运算。
  4. 使用 PG_RETURN_TIMESTAMP 返回结果。

核心代码解析

我们通过扩展语法解析器和新增功能函数来实现对MySQL DATE_ADD 的兼容。

    1. 语法解析器扩展(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 参数。

    1. 语法规则重写(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标签,抛出清晰的错误信息,引导用户使用正确的格式。

整体内核处理流程解析

Workflow

5.1 SQL 输入与初步解析

  1. 用户输入 SQL 语句: 流程始于用户执行 SELECT DATE_ADD('...', INTERVAL '...' UNIT) 形式的 SQL 命令。

  2. PG 内核:词法与语法解析: PostgreSQL 内核的词法分析器 (Lexer) 和语法分析器 (Parser) 开始工作,识别 DATE_ADD 函数和 INTERVAL 子句。

5.2 语法规则匹配与分支决策

  1. 是否匹配 DATE_ADD/DATE_SUB 规则?

在 src/backend/parser/gram.y 文件中,内核匹配我们在 func_expr_common_subexpr 规则中添加的 DATE_ADD/DATE_SUB 规则。

  1. 识别单位 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 复合单位,流程进入我们自定义的兼容逻辑:

  1. Grammar 转换:重写为 date_add_mysql_compound 函数调用:无论是 DATE_ADD 还是经过取反处理的 DATE_SUB,最终都被重写为一个统一的自定义函数调用:date_add_mysql_compound(Timestamp, ValueStr, UnitStr)。
  2. 调用 C 函数 date_add_mysql_compound: 执行器调用我们在 timestamp.c 中注册的 C 语言函数。
  3. 调用 parse_mysql_compound_interval: 在 date_add_mysql_compound 内部,立即调用核心解析函数 parse_mysql_compound_interval。
  4. C 函数:解析 ValueStr,转换为 PG Interval 结构体:

该函数根据 UnitStr 对 ValueStr 进行精确解析(如 sscanf 或 strtod)。将解析出的年、月、日、时、分、秒、微秒转换为 PG 内部所需的 Interval 结构体(months、days、time)。

  1. 调用 PG 内置 timestamp_pl_interval 运算:

date_add_mysql_compound 函数随后调用 PG 内置的 timestamp_pl_interval 函数,将原始时间戳和新生成的 Interval 结构体进行相加运算。

5.5 结果返回

执行 PG 原生运算 / 返回结果: 无论是通过路径 A 还是路径 B,最终都汇聚到 PG 原生的时间戳运算能力,计算出结果并返回给用户。

总结

本方案通过语法扩展 + 复合间隔解析 + 原生运算的三层架构,成功完整复现了 MySQL DATE_ADD 函数的功能。通过本方案,项目成功降低了从 MySQL 迁移至 海山数据库(He3DB) 的业务改造成本,为多数据库兼容提供了可复用的技术范式。