协会地址:上海市长宁区古北路620号图书馆楼309-313室
使用 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)以实现不同的功能。
与此同时,祝编码愉快!







