Mercurial > hg > MonetDB-extend
view reverse/README.rst @ 57:d77cd888a2f2
We should say something about how to return from the bulk function.
author | Sjoerd Mullender <sjoerd@acm.org> |
---|---|
date | Thu, 24 Mar 2022 18:37:39 +0100 (2022-03-24) |
parents | 68263b10998e |
children | 02895996506d |
line wrap: on
line source
.. This Source Code Form is subject to the terms of the Mozilla Public .. License, v. 2.0. If a copy of the MPL was not distributed with this .. file, You can obtain one at http://mozilla.org/MPL/2.0/. .. .. Copyright 2013-2021 MonetDB B.V. .. This document is written in reStructuredText (see http://docutils.sourceforge.net/ for more information). Use ``rst2html.py`` to convert this file to HTML. =========================================== Implementing a Simple User-Defined Function =========================================== :Author: Sjoerd Mullender :Organization: CWI, MonetDB B.V. :Abstract: In this short tutorial we describe how to create a simple UDF for MonetDB/SQL. Introduction ------------ In this directory we show how to make a simple user-defined function (UDF) that can be used in MonetDB/SQL. The function is implemented in C. In order to make the function available to SQL, we need to provide the correct C interface, a MAL interface, and an SQL interface. We will discuss them all, starting from SQL and going down towards the actual implementation. We want to create a function that allows us to write something like this: .. code-block:: sql SELECT revstr(strcol) FROM table; where ``table`` is an SQL table with a column called ``strcol`` which is of type ``VARCHAR`` (or any other string type). Implementation -------------- We will first create an interface to do a simple one-value-at-a-time (*scalar*) operation: .. code-block:: sql SELECT revstr('string'); The SQL catalog will need to be extended with a definition of the ``revstr`` function as follows: .. code-block:: sql CREATE FUNCTION revstr(src STRING) RETURNS STRING EXTERNAL NAME revstr.revstr; The statement tells the SQL system that there is a function called ``revstr`` which takes a ``STRING`` argument and produces a ``STRING`` result. The function is implemented using the MAL interface ``revstr.revstr``. Note that ``STRING`` is equivalent to ``CHARACTER LARGE OBJECT`` or ``CLOB``. This statement will normally be executed once when the database is created, after which it is part of the SQL catalog. How this is accomplished exactly we will leave until later in this tutorial. For now let it suffice to note that the SQL query is encoded as a C string and stored in the variable ``reverse_sql``: .. code-block:: c static char reverse_sql[] = "CREATE FUNCTION revstr(src STRING)" " RETURNS STRING EXTERNAL NAME revstr.revstr;"; At the SQL side we don't have to do anything more. The SQL layer produces an intermediate code called MAL (which stands for MonetDB Assembly Language). The external name above refers to a command that is defined in MAL, so now that we have the SQL interface, we need to create the MAL interface. The MAL interface of the function looks like this: .. code-block:: module revstr; command revstr(ra1:str):str address UDFreverse comment "Reverse a string"; The SQL engine uses the convention that a *bulk* variant of a scalar operation (i.e., a variant that works on a complete column and produces a column as opposed to a function that works on a single value and produces a single value) has the same name but is located in a module with the string ``bat`` prepended. So, the bulk version of the ``revstr.revstr`` function can also be created: .. code-block:: module batrevstr; command revstr(b:bat[:str]):bat[:str] address UDFBATreverse comment "Reverse a column of strings"; This MAL code also needs to be encoded in the C source. This is done as follows: .. code-block:: c static mel_func reverse_init_funcs[] = { command("revstr", "revstr", UDFreverse, false, "Reverse a string", args(1,2, arg("",str),arg("ra1",str))), command("batrevstr", "revstr", UDFBATreverse, false, "Reverse a BAT of strings", args(1,2, batarg("",str),batarg("b",str))), { .imp=NULL } /* sentinel */ }; A C array with elements of type ``mel_func`` is created, and each MAL command is one element of this array. The array ends with a sentinel, a value that is "empty" and can thus be recognized as the end of the array. Each element in the array is an instance of the macro ``command`` which has a bunch of arguments. In order, they are: MAL module name (string), MAL function name (string), C function (pointer to function), a Boolean flag to indicate whether this is an "unsafe" operation (one with side effects), a comment (string--not currently used but must be present), a description of the function arguments. The function arguments are encoded using the ``args`` macro with the following arguments. The number of return values (MAL functions can return 1 or more values), the total number of arguments (i.e. the sum of return values and input arguments), and then for each return argument and each input argument a description of the argument itself. Each argument is an instance of a macro. There are various forms, but here we use two. ``arg`` describes a scalar argument and has two parameters, the MAL name of the argument and the MAL type. ``batarg`` describes a BAT argument and also has two parameters, the MAL name of the argument and the MAL type of the elements of the BAT. Note that implementing a bulk version is optional. If it does not exist, the scalar version will be called multiple times. However, calling the scalar version multiple (sometimes very many) times incurs significant overhead, hence it is usually a good idea to implement the bulk version. Now we come to the actual implementation of the feature. The MAL interfaces of the scalar and bulk versions of the ``revstr`` function translates to the following C interfaces: .. code-block:: c static char *UDFreverse(char **retval, const char **arg); static char *UDFBATreverse(bat *retval, const bat *arg); The return value of the C functions is normally ``MAL_SUCCEED`` which translates to ``NULL``. If an error occurs, the return should be a freshly allocated string that contains the error message. The string must be allocated with the function ``GDKmalloc`` or one of its variants (``GDKzalloc``, ``GDKstrdup``) since it will be freed with ``GDKfree`` when the interpreter is done with the message. Most of the time this can be done by calling the macro ``throw`` which is defined as ``return createException``, so calling ``throw`` with arguments will actually call ``createException`` with those same arguments and return the result. The function ``createException`` has three or more arguments, the first is ``MAL``, the second is the name of the MAL function, the third is a ``printf`` format string, and remaining arguments are values that are used by the format string. A minimal example is: .. code-block:: c static char *UDFreverse(char **retval, const char **arg) { (void) retval; (void) arg; /* we're not using these */ throw(MAL, "revstr.revstr", "Not yet implemented"); } MAL commands can return any number of values. These values are returned by the C function in the first arguments of the C function, hence the two ``retval`` arguments. The arguments of the MAL interface follow the return values in the argument list of the C function (the ``arg`` arguments). We have added the ``const`` keyword to indicate that the arguments will not be altered. The MonetDB code usually uses the C type ``str`` which is defined to be ``char *``, so you could define the functions also as: .. code-block:: c static str UDFreverse(str *retval, const str *arg); static str UDFBATreverse(bat *retval, const bat *arg); Note that the definitions are not entirely equivalent. The target of the ``const`` keyword is different for the first function. The type ``bat`` is defined as ``int``, so you may see ``int`` in some places in the MonetDB source code in equivalent positions. These functions must be located in a dynamically loadable module (``.so`` file on Linux, ``.dll`` on Windows), and this module must have the name ``lib_reverse.so`` (or ``lib_reverse.dll``). The functions are only directly referenced from the ``reverse_init_funcs`` array that we have defined above, so the functions are declared as ``static`` functions. Scalar Version ~~~~~~~~~~~~~~ We will now first focus on the implementation of the scalar function and return to the bulk version later. The input of the function ``UDFreverse`` is a (NULL-terminated) string. The function is called with a pointer to the string pointer, so ``*arg`` is the actual string and ``**arg`` the first byte of the string. The result of the operation is also a (NULL-terminated) string. Since the caller does not know what the size of the result will be, it provides a pointer to where the result is to be put. The callee is responsible for allocating the necessary space. This means that we need to do something like this: .. code-block:: c *retval = GDKmalloc(size); // copy data into *retval In the case of this function, calculating the needed space is easy, although we need to do error checking as well: .. code-block:: c *retval = GDKmalloc(strlen(*arg) + 1); if (*retval == NULL) throw(MAL, "revstr.revstr", MAL_MALLOC_FAIL); // reverse the string in *arg into *retval return MAL_SUCCEED; In the actual algorithm we have also taken into account that strings in MonetDB are always stored in the UTF-8 encoding. In addition, our implementation checks for the special value ``str_nil`` which is the C representation of the SQL ``NULL`` value for strings (which in MAL is called ``nil:str``). Bulk Version ~~~~~~~~~~~~ The bulk version gets as input a pointer to a BAT identifier (a value of type ``bat``). It also returns a BAT identifier of a newly created BAT through the output pointer. It is important to realize that BATs are reference counted with two reference counters. There is a *logical* reference count (``lref``) and a *physical* reference count (``ref``). C code is only allowed to look at the actual data when the *physical* reference count is greater than zero and the BAT is loaded into (virtual) memory. A newly created BAT has a physical reference count of one and a logical reference count of zero. Before returning a new BAT, the physical reference count must be converted to a logical reference count. When a C function is called by the MAL interpreter, it passes a *logical* reference to any BAT parameters. This means that the C function must first make sure that the BAT gets a *physical* reference and is loaded into memory. We start with doing just that. We do that by calling ``BATdescriptor`` which increments the physical reference count and loads the BAT into memory (if it wasn't there already): .. code-block:: c BAT *b; b = BATdescriptor(*arg); if (b == NULL) throw(MAL, "batrevstr.revstr", RUNTIME_OBJECT_MISSING); When we're done with this BAT, we will need to decrement the physical reference count again. We do that by calling ``BBPunfix``: .. code-block:: c BBPunfix(b->batCacheid); Note that ``b->batCacheid`` is equal to ``*arg``. We need to create the result BAT ourselves. We know the type, and we know that the size of the BAT will be the same as the input BAT. Hence we can use this code: .. code-block:: c bn = COLnew(b->hseqbase, TYPE_str, BATcount(b), TRANSIENT); The arguments of ``COLnew`` are the head seqbase, the type of the column, the initial size (in number of entries) of the to-be-allocated BAT, and ``TRANSIENT`` to indicate that this BAT is temporary. ``COLnew`` guarantees that there is space for at least the specified number of elements, or it returns ``NULL``. Since we call ``BUNappend`` to add entries to the BAT, we're actually not concerned about the size of the new BAT (``BUNappend`` takes care of resizing if necessary), but from an efficiency point of view, it's better to create the BAT with the required size (growing a BAT can be expensive). We must set the head sequence base of the new BAT to be the same as that of the input BAT. Note that for variable-sized types (such as the strings we are dealing with here), ``BUNappend`` can still fail due to not enough memory, even though we supposedly allocated enough. The strings have to be stored somewhere, and ``COLnew`` has no way of knowing how large the total area for the strings must be, so ``BUNappend`` may have to grow the memory area for the strings, and that can fail. Iterating through the source BAT is done using a standard mechanism: .. code-block:: c BATiter bi; BUN p, q; bi = bat_iterator(b); BATloop(b, p, q) { ... } bat_iterator_end(&bi); ``BATloop`` is a macro that translates to a C ``for`` loop, using the first argument to set the bounds, the second argument to iterate through the BAT, and the third argument as the loop limit. The second argument can be used inside the body as an argument to e.g. ``BUNtail``. We make use of a BAT iterator (type ``BATiter``) which is initialized with a call to ``bat_iterator(b)``. Since this call increments a reference count, we need to match the initialization with a call to ``bat_iterator_end(&bi)``. The calls to ``bat_iterator`` and ``bat_iterator_end`` cannot fail. The need for ``bat_iterator_end`` is new since the Jul2021 release (11.41.X). The body of the loop first retrieves the current value from the column: .. code-block:: c src = (const char *) BUNtail(bi, p); We then use this string in the same way as in the scalar function. The reversed string in ``dst`` is appended to the result BAT: .. code-block:: c BUNappend(bn, dst, false); The third argument to ``BUNappend`` must always be ``false``. ``BUNappend`` returns ``GDK_SUCCEED`` for success or ``GDK_FAIL`` for failure. ``BUNappend`` is marked in the include file as a function of which the result *must* be checked. It is a good convention to always check whether the result is (not) equal to ``GDK_SUCCEED`` so that if in the future different errors are returned, the code keeps working. BAT Properties -------------- MonetDB makes extensive use of a number of property flags that can be set on BATs. It is crucial that these property flags don't lie. When the server is started with the ``-d10`` option, the server checks these property flags and exits with a failed assertion when a flag is set incorrectly (or the server issues a warning when it was built with assertions disabled). Property flags are Boolean flags, i.e. they are either true (set) or false (not set). When a property flag is not set, it means that either the property doesn't hold or it is unknown whether the property holds. The property flags are ``tsorted`` the column is sorted in ascending order ``trevsorted`` the column is sorted in descending order ``tkey`` all values in the column are distinct ``tnonil`` there are no nil values in the column ``tnil`` there are nil values in the column (this property isn't used internally) In addition to these flags there are a few other properties: ``tnosorted`` the location that proofs the column is *not* sorted ``tnorevsorted`` the location that proofs the column is *not* reverse sorted ``tnokey[0]`` and ``tnokey[1]`` two locations that together proof *not* all values are distinct The property flags ``tsorted`` and ``trevsorted`` may both be set at the same time. When they are, it implies that all values are equal to each other. When the ``tsorted`` property is unset, the ``tnosorted`` property is a position in the BAT where the previous value is not less than or equal to the position itself. If the ``tnosorted`` value is 0, we truly don't know whether the BAT is sorted. Similarly for the ``trevsorted`` and ``tnorevsorted`` properties. The two ``tnokey`` values are either both 0 (we really don't know), or two distinct locations whose values are equal. The ``tno``... values *must* be zero if the corresponding property is set, they may be zero if the corresponding property in unset. Note that most of the properties are true for an empty column, hence when ``COLnew`` returns, all property flags except for ``nil`` are set (there are no nils in an empty column). This means that as soon as you start adding data to a column, you must deal with the property flags. The simplest solution is to just clear all properties: .. code-block:: c bn->tsorted = bn->trevsorted = false; bn->tkey = false; bn->tnil = false; bn->tnonil = false; bn->tnosorted = bn->tnorevsorted = 0; bn->tnokey[0] = bn->tnokey[1] = 0; Note also that the function ``BUNappend`` maintains the property flags as best it can. That is why in the example we didn't need to do anything with the property flags. Returning the Result -------------------- When the return BAT has been created and filled, it needs to be returned. This is done by assigning the BAT id to the space the result pointer points to. In addition, we need to deal with the reference counting mentioned before. The input BAT's reference that we got from ``BATdescriptor`` should be released with ``BBPunfix`` and one physical reference to the new BAT needs to be converted to one logical reference. This is done by a call to BBPkeepref. And finally, the function should return and indicate that no errors occurred. This is done by returning the value ``MAL_SUCCEED`` (which is just a ``NULL`` pointer). .. code-block:: c BBPunfix(b->batCacheid); BBPkeepref(bn->batCacheid); *retval = bn->batCacheid; return MAL_SUCCEED; Informing the Server -------------------- So far we have created the necessary C code that can be called by the interpreter when the appropriate SQL query is executed. However, we still need to tell the server that this code actually exists. We have already hinted at this, but here we will finish that part. Once the ``.so`` or ``.ddl`` file has been created and installed, the server needs to be told about it. This is done be calling the server with an extra argument: .. code-block:: sh mserver5 --loadmodule=reverse ... where ``...`` represents any other arguments not covered by this tutorial. When the server gets this ``--loadmodule`` argument, it loads the library. And here we use a trick that is available in dynamically loaded libraries. We tell the system to automatically execute some code when the library is loaded: .. code-block:: c #include "mal_import.h" #include "sql_import.h" #ifdef _MSC_VER #undef read #pragma section(".CRT$XCU",read) #endif LIB_STARTUP_FUNC(init_reverse) { mal_module("revstr", NULL, reverse_init_funcs); sql_register("revstr", reverse_sql); } The ``LIB_STARTUP_FUNC`` macro is defined in one of the include files. It has an argument which is a name that should be unique. The convention is ``init_`` followed by the name of the module. This macro is the start of a function definition, so it is followed by the body of the function that is executed when the library is loaded. The function calls two functions. The first, ``mal_module``, registers the MAL functions that we have defined. The arguments are the name of the module, an array of elements of type ``mel_atom`` (not used here), and an array of elements of type ``mel_func`` which contains our MAL functions. The second function, ``sql_register`` registers the SQL query that needs to be executed to enter the SQL function into the catalog. Makefile -------- To bring all of this together, we have a ``Makefile``. It uses the ``pkgconf`` command to find the location of the MonetDB installation and the arguments needed to compile the module. (If you don't have ``pkgconf``, you may be able to replace it with ``pkg-config``.) This Makefile works on Fedora Linux if you have the package ``MonetDB-SQL-server5-devel`` with all its dependencies installed (available starting in the Oct2020-SP2 bugfix release), and on Debian/Ubuntu if you have the package ``monetdb5-sql-dev`` with all its dependencies installed (available starting in the Oct2020-SP2 bugfix release). The file may need to be changed for other systems. Note that even in the Oct2020-SP5 release there are some include files missing. A Note About Names ------------------ The name *BAT* originally stood for *Binary Association Table*. This suggests, correctly, that there were actually two values per row in a BAT that were associated. The two values where the *head* and *tail* values of, what was called, a *Binary UNit* or *BUN*. The head and tail columns of a BAT had independent types, but it turned out that most of the time we used the type *oid* (*Object IDentifier*) for the head column, and the values were, most of the time, consecutive. Since then we have made sure that the head values were always of type oid and consecutive, which meant that we could do away with the complete head column. The only thing that we still needed (and still need) is the first value of the old head column. This value is called *hseqbase* (head sequence base). There are still many vestiges of the old head and tail columns, especially when accessing values in what used to be the tail column and now is the only column. So we have functions such as ``BUNtail`` that have nothing to do anymore with a tail. Also, the term BUN was repurposed to being the name we have given to the index of the C array that holds the values of what used to be the tail column. For more information see the blog post `MonetDB goes headless`__. __ https://www.monetdb.org/blog/monetdb-goes-headless