After having discovered the Type_hander framework and learned how to build MariaDB Server from source, it’s time to code our first data type!
We will create a MariaDB plugin that registers a new MONEY type and instantiates a custom field object.
Our component won’t be exciting, but we want to understand how to use the framework and test it.
We want to prove that
- the plugin loads,
- the server sees the type hander,
- a
MONEYcolumn can create aField_moneyobject.
Everything else comes later.
Step 1: start with a minimal build file
The very first step is to create a directory to store our code. In the server’s source tree, in the plugin directory, we create a new subdirectory called type_money. We should see other types in the plugin directory already:
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
We need to provide a build file for our new data type, so we create the file CMakeLists.txt with the following content:
MYSQL_ADD_PLUGIN(type_money plugin.cc MODULE_ONLY RECOMPILE_FOR_EMBEDDED)
As you can see, I am referencing plugin.cc what seems to be the standard used name.
Here we use MODULE_ONLY. This means build this plugin only as a loadable shared module, not as a statically linked module into the MariaDB server. Also, it must be explicitly installed to work. If we compare with the uuid datatype, which is also a plugin shared object but always loaded, we can see that it uses MANDATORY keyword instead means it’s a required plugin; build cannot disable it.RECOMPILE_FOR_EMBEDDED is required, as a MariaDB data type plugin is not like a small UDF plugin. It subclasses internal server classes, such as Type_handler_* and Field_*. The MariaDB Type_handler design expects a custom type to provide those classes and to register the plugin. That means it depends heavily on server internals.
Step 2: define the type handler interface
We will define our Type_handler_money class that reuses the numeric behavior of DOUBLE.
This class will be stored in a header file (.h): sql_type_money.h:
class Type_handler_money : public Type_handler_double
{
public:
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;
};
This is the server-facing definition of the new type. By inheriting from Type_handler_double; the new MONEY type says, in effect, “treat me like a DOUBLE unless I explicitly override something”.
This means MONEY reuses all of DOUBLE‘s semantics by default — storage format, arithmetic operators, result type, aggregation rules — and only overrides the specific behaviors that need to differ.
Three methods matter here:
type_collection: returns the broader family this type belongs to. In our implementation,MONEYreuses the same collection asDOUBLE, which means MariaDB will classify it alongside the standard numeric aggregation pool.Column_definition_data_type_info_image(...): writes the custom type name into the.frmmetadata stored on disk. And this is where the override replaces “MONEY” in theSHOW CREATE STATEMENTinstead ofDOUBLE.make_table_field_from_def(...): creates the actualFieldobject used for a column definition that lives in the table row buffer. This is where the server converts SQL metadata, such as length, decimals, nullability, and flags, into a concrete runtime object.
Conceptually, the type handler answers the question: What kind of SQL type is this, and what field object should represent it in a table?
Step 3: define the field class
The second piece in our header is the field implementation:
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)
{}
const Type_handler *type_handler() const override;
void make_send_field(Send_field *field) override;
};
This class inherits from Field_double, which already provides the structure for number fields. Because of the inheritance, we don’t need to define how the value is stored or compared.
If Type_handler_money describes the type, then Field_money is the object that actually lives in rows, participates in comparisons, and converts values between SQL and internal memory.
Step 4: build the field from column metadata
In the implementation, Type_handler_money::type_collection() simply returns the collection for Type_handler_double. That means MariaDB continues to treat the type as part of the same general family as DOUBLE.
This is ideal for learning purposes.
The next important step is make_table_field_from_def(...).
This method allocates a Field_money object from MariaDB’s memory root and populates it from the parsed column attributes:
- column name
- record and null-bit addresses
- declared length
- decimal precision
- unsigned flag
That method is the bridge between DDL and runtime. When a user creates a column of type MONEY, MariaDB eventually calls this function to construct the field instance that will represent that column internally.
A simplified reading of the method is:
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);
}
A few details are worth pointing out:
new (root): uses MariaDB’sMEM_ROOTallocator rather than ordinary heap allocation.attr->lengthandattr->decimals: carry through the SQL declaration into the field object.f_is_dec(attr->pack_flag) == 0: interprets the field flags to derive whether the type should behave as unsigned.
This is the key factory method in the whole design. The server knows the type handler; the type handler knows how to instantiate the field.
Step 5: link the field back to the handler
Our field class implements:
const Type_handler *Field_money::type_handler() const
{
return &type_handler_money;
}
This method completes the process. The Field_money instance specifies its type handler, enabling the server to reference type metadata and behavior from field objects.
Without this mechanism, MariaDB would retain field objects lacking a definitive type identity.
We also define a result set header, one metadata packet per column that provides, for example, the column’s name, how to decode the bytes, the length, and more:
void Field_money::make_send_field(Send_field *field)
{
Field_double::make_send_field(field);
}
Step 6: value readers
Once a value is stored, MariaDB needs to read it back in different forms. We don’t need to maintain this as our MONEY data type behaves like DOUBLE and it inherits from it.
Step 7: comparison and sorting
A field type is not complete until MariaDB can compare and sort it. But once again, we don’t need to code it for the moment.
Step 8: register the type as a plugin
Defining a handler and a field is not enough on its own. MariaDB still needs to discover the new type through the plugin interface.
We do that in two steps. This is a reminder of what we already covered in part 1.
First, we create a data-type plugin descriptor:
static struct st_mariadb_data_type plugin_descriptor_type_money=
{
MariaDB_DATA_TYPE_INTERFACE_VERSION,
&type_handler_money
};
This tells MariaDB which handler object implements the type.
Then we register the plugin:
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;
This is the final integration step. It gives the plugin:
- a plugin kind:
MariaDB_DATA_TYPE_PLUGIN - the descriptor that points to the handler
- a name:
"money" - metadata such as author, description, version, and maturity
At that point, MariaDB can load the plugin and treat it as a server extension that contributes a new SQL data type.
Step 10: building our plugin
Our plugin data type still misses some include files. You can find the source code in this GitHub repository. There are branches for each article that requires it. The code is in the branch part2.
So, to build the code that we have put in the plugin/type_money directory (pay attention to put the files in the source directory, not in the build one), we go into our build directory, and we just run the following commands:
$ cd <wherever-our-build-is>/build-mariadb-debug
$ cmake --build plugin/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
That’s it!
Testing the new data type
We can start the server using mtr and try to load our plugin and create a table with a MONEY column:
$ cd mysql-test
$ ./mtr --start
And in another terminal:
$ client/mariadb -u root -S /var/tmp/mysqld.1.sock
Pay attention to using the right socket file.
And now in MariaDB client:
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)
That sounds great. In the next article, we will extend our new data type to do something different.
Meanwhile, happy coding!