使用 Type_handler 向 MariaDB 添加新数据类型 – 第 2 部分

作者: Frédéric Descamps
原文链接:https://mariadb.org/adding-a-new-data-type-to-mariadb-with-type_handler-part-2/


发现了 Type_handler 框架学习了如何从源码构建 MariaDB Server 之后,是时候编写我们的第一个数据类型了!

我们将创建一个 MariaDB 插件,用于注册一个新的 MONEY 类型,并实例化一个自定义字段对象。

我们的组件不会很复杂,但我们要理解如何使用该框架并进行测试。

我们希望验证以下几点:

  • 插件能够加载,
  • 服务器能够识别该类型处理器(type handler),
  • 一个 MONEY 列能够创建一个 Field_money 对象。

其他所有内容后续再考虑。

第一步:从一个最小的构建文件开始

第一步是创建一个目录来存放我们的代码。在服务器源码树的插件目录中,我们创建一个名为 type_money 的新子目录。在插件目录中应该已经能看到其他类型了:

plugin
├── type_assoc_array
├── type_cursor
├── type_geom
├── type_inet
├── type_mysql_json
├── type_mysql_timestamp
├── type_test
├── type_uuid
└── type_xmltype
$ cd server/plugin
$ mkdir type_money
# cd money

我们需要为新的数据类型提供一个构建文件,因此创建包含以下内容的文件 CMakeLists.txt

CMakeLists.txtMYSQL_ADD_PLUGIN(type_money plugin.cc MODULE_ONLY RECOMPILE_FOR_EMBEDDED)

如你所见,我引用了 plugin.cc,这似乎是标准的命名惯例。

这里我们使用了 MODULE_ONLY。这意味着仅将此插件构建为可加载的共享模块,而非静态链接到 MariaDB 服务器中的模块。此外,必须显式安装它才能工作。如果与 uuid 数据类型(它也是一个插件共享对象,但始终被加载)进行比较,可以看到它使用了 MANDATORY 关键字,这意味着它是一个必需的插件;构建时无法禁用它。RECOMPILE_FOR_EMBEDDED 是必需的,因为 MariaDB 数据类型插件不像一个小的 UDF(用户自定义函数)插件。它继承了内部服务器类,例如 Type_handler_*Field_*MariaDBType\_handler设计期望自定义类型提供这些类并注册插件。这意味着它严重依赖于服务器内部实现。

步骤 2:定义类型处理器接口

我们将定义 Type_handler_money 类,该类重用 DOUBLE 的数值行为。

此类将存储在一个头文件(.h)中:

sql_type_money.h:

sql_type_money.hclass Type_handler_money : public Type_handler_double
{
public:  

```cpp
const Type_collection *type_collection() const override;
  bool Column_definition_data_type_info_image(Binary_string *to,
                                             const Column_definition &def)
                                             const override;

  Field *make_table_field(MEM_ROOT *root,
                          const LEX_CSTRING *name,
                          const Record_addr &rec,
                          const Type_all_attributes &attr,
                          TABLE_SHARE *share) const override;

  Field *make_table_field_from_def(TABLE_SHARE *share,
                                 MEM_ROOT *root,
                                 const LEX_CSTRING *name,
                                 const Record_addr &rec,
                                 const Bit_addr &bit,
                                 const Column_definition_attributes *attr,
                                 uint32 flags) const override;
};
</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>这是新类型面向服务端的定义。通过继承 <code>Type_handler_double</code>,新的 <code>MONEY</code> 类型实际上表明:“除非我显式覆盖某些内容,否则请像对待 <code>DOUBLE</code> 一样对待我”。</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>这意味着 <code>MONEY</code> 默认复用 <code>DOUBLE</code> 的所有语义——存储格式、算术运算符、结果类型、聚合规则——并且只覆盖需要不同的特定行为。</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>这里涉及三个方法:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul class="wp-block-list"><!-- wp:list-item -->
<li><strong>type_collection</strong>:返回此类型所属的更大族系。在我们的实现中,<code>MONEY</code> 复用与 <code>DOUBLE</code> 相同的集合,这意味着 MariaDB 会将其归类到标准数值聚合池中。</li>
<!-- /wp:list-item -->

<!-- wp:list-item -->
<li><strong>Column_definition_data_type_info_image(...)</strong>:将自定义类型名称写入存储在磁盘上的 <code>.frm</code> 元数据中。正是在这里,覆盖操作将 <code>SHOW CREATE STATEMENT</code> 中的“<code>**MONEY**</code>”替换为 <code>DOUBLE</code>。</li>
<!-- /wp:list-item -->

<!-- wp:list-item -->
<li><strong>make_table_field_from_def(...)</strong>:创建实际的 <code>Field</code> 对象,用于驻留在表行缓冲区中的列定义。服务器在此处将 SQL 元数据(如长度、小数位数、可空性和标志)转换为具体的运行时对象。</li>
<!-- /wp:list-item --></ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>从概念上讲,类型处理器回答了以下问题:<em>这是什么类型的 SQL 类型,在表中应该用什么字段对象来表示它?</em></p>
<!-- /wp:paragraph -->

<!-- wp:heading -->
<h2 class="wp-block-heading">步骤 3:定义字段类</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>我们头文件中的第二部分是字段实现:</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p><code>*sql_type_money.h*</code></p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code>class Field_money : public Field_double
{
public:
  Field_money(const LEX_CSTRING &name, const Record_addr &addr,
              enum utype unireg_check_arg, uint32 len_arg,
              decimal_digits_t dec_arg, bool zero_arg, bool unsigned_arg)
    : Field_double(addr.ptr(), len_arg, addr.null_ptr(), addr.null_bit(),
                 unireg_check_arg, &name, dec_arg, zero_arg, unsigned_arg)
  {}

```cpp
const Type_handler *type_handler() const override;
  void make_send_field(Send_field *field) override;
};

该类继承自 Field_double(双精度字段),后者已提供了数字字段的结构。由于继承关系,我们无需定义值的存储或比较方式。

如果说 Type_handler_money(货币类型处理器)描述了类型,那么 Field_money(货币

字段)就是实际存在于行中、参与比较、并在 SQL 与内部内存之间转换值的对象。

第 4 步:从列元数据构建字段

在实现中,Type_handler_money::type_collection() 直接返回 Type_handler_double 的集合。这意味着 MariaDB 继续将该类型视为与 DOUBLE 同属一个大家族。

这对于学习目的来说非常理想。

下一个重要步骤是 make_table_field_from_def(...)

该方法从 MariaDB 的内存根中分配一个 Field_money 对象,并根据解析后的列属性填充它:

  • 列名
  • 记录和空位地址
  • 声明长度
  • 小数精度
  • 无符号标志

该方法是 DDL(数据定义语言)与运行时之间的桥梁。当用户创建 MONEY(货币)类型的列时,MariaDB 最终会调用此函数来构造代表该列的内部字段实例。

该方法的简化解读如下:

*plugin.cc*

Field *Type_handler_money::make_table_field_from_def(TABLE_SHARE *share,
                                 MEM_ROOT *root,
                                 const LEX_CSTRING *name,
                                 const Record_addr &rec,
                                 const Bit_addr &bit,
                                 const Column_definition_attributes *attr,
                                 uint32 flags) const
{
  (void) share;
  (void) bit;
  (void) flags;
  return new (root) Field_money(*name,
                                rec,
                                attr->unireg_check,
                                attr->length,
                                attr->decimals,
                                f_is_zerofill(attr->pack_flag) != 0,
                                f_is_dec(attr->pack_flag) == 0);
}

有几个细节值得指出:

  • **new (root)**:使用 MariaDB 的 MEM_ROOT 分配器,而非普通的堆分配。
  • attr->length 和 attr->decimals:将 SQL 声明中的信息传递到字段对象(field object)中。
  • **f_is_dec(attr->pack_flag) == 0**:解释字段标志(field flags),以推导该类型是否应表现为无符号(unsigned)。

这是整个设计中的关键工厂方法。服务器知道类型处理器(type handler);类型处理器知道如何实例化字段(field)。

第 5 步:将字段链接回处理器

我们的字段类实现了:

*plugin.cc*

const Type_handler *Field_money::type_handler() const
{
  return &type_handler_money;
}

此方法完成整个过程。Field_money 实例指定了其类型处理器(type handler),使服务器能够从字段对象中引用类型元数据(type metadata)和行为。

如果没有这一机制,MariaDB 将保留缺乏明确类型标识的字段对象。

我们还定义了一个结果集头部(result set header),每列对应一个元数据包(metadata packet),提供例如列名、如何解码字节、长度等信息。

void Field_money::make_send_field(Send_field *field)
{
  Field_double::make_send_field(field);
}

步骤 6:值读取器(value readers)

一旦值被存储,MariaDB 需要以不同形式将其读回。我们无需维护这一部分,因为我们的 MONEY 数据类型的行为与 DOUBLE 相同,并且继承自它。

步骤 7:比较与排序(comparison and sorting)

在 MariaDB 能够比较和排序之前,字段类型是不完整的。但同样,目前我们无需为此编写代码。

步骤 8:将类型注册为插件(plugin)

仅定义处理程序(handler)和字段(field)是不够的。MariaDB 还需要通过插件接口发现新的类型。

我们分两步完成。这是对第 1 部分中已介绍内容的回顾。

首先,我们创建一个数据类型插件描述符(data-type plugin descriptor):

static struct st_mariadb_data_type plugin_descriptor_type_money=
{
  MariaDB_DATA_TYPE_INTERFACE_VERSION,
  &type_handler_money
};

这告诉 MariaDB 哪个 handler object(处理器对象)实现了该类型。

然后我们注册插件:

maria_declare_plugin(type_money)
{
  MariaDB_DATA_TYPE_PLUGIN,
  &plugin_descriptor_type_money,
  "money",
  "lefred",
  "Data type MONEY",
  PLUGIN_LICENSE_GPL,
  0,
  0,
  0x0001,
  NULL,
  NULL,
  "0.1",
  MariaDB_PLUGIN_MATURITY_EXPERIMENTAL
}
maria_declare_plugin_end;

这是最后的集成步骤。它为插件提供了:

  • 插件种类:MariaDB_DATA_TYPE_PLUGIN
  • 指向处理程序的描述符
  • 名称:"money"
  • 元数据,如作者、描述、版本和成熟度

至此,MariaDB 可以加载该插件,并将其视为贡献新 SQL 数据类型的服务器扩展。

第 10 步:构建我们的插件

我们的插件数据类型仍然缺少一些包含文件。你可以在 这个 GitHub 仓库 中找到源代码。每个需要它的文章都有对应的分支。代码位于 part2 分支 中。

因此,要构建我们放在 plugin/type_money 目录中的代码(注意将文件放在源目录中,而不是构建目录中),我们进入构建目录,然后运行以下命令:

$ cd <wherever-our-build-is>/build-mariadb-debug
$ cmake --build . --target=type_money
[ 22%] Built target mysqlservices
[ 22%] Built target uca-dump
[ 22%] Built target GenUnicodeDataSource
[ 77%] Built target mysys
[100%] Built target strings
[100%] Built target dbug
[100%] Built target comp_err
[100%] Built target GenError
[100%] Building CXX object plugin/type_money/CMakeFiles/type_money.dir/plugin.cc.o
[100%] Linking CXX shared module type_money.so
[100%] Built target type_money

就这样!

测试新数据类型

我们可以使用 mtr 启动服务器,尝试加载我们的插件并创建一个包含 MONEY 列的表:

$ cd mysql-test
$ ./mtr --start

然后在另一个终端中:

$ client/mariadb -u root -S /var/tmp/mysqld.1.sock

注意使用正确的套接字文件。

现在在 MariaDB 客户端中:

MariaDB [(none)]> install soname 'type_money';
Query OK, 0 rows affected (0.003 sec)

MariaDB [(none)]> use test;
Database changed

MariaDB [test]> create table t1 (id int auto_increment primary key,
                                 amount money);
Query OK, 0 rows affected (0.002 sec)

MariaDB [test]> insert into t1 (amount) values (12.3);
Query OK, 1 row affected (0.021 sec)

MariaDB [test]> select * from t1;
+----+--------+
| id | amount |
+----+--------+
|  1 |   12.3 |
+----+--------+
1 row in set (0.000 sec)

这看起来很棒。在下一篇文章中,我们将扩展我们的新数据类型(data type)以实现不同的功能。

与此同时,祝编码愉快!