Table of Contents
Database Abstraction Layer
(preliminary text by Jan, needs refactoring)
The database subsystem in SER 2.1 was significantly changed from previous versions. A few major reasons for extensive overhaul of the database subsystem include:
- Support for multiple databases at the same time (even within a single SER module).
- Support for high-available database setups, in other words the possibility to use multiple database instances at the same time.
- Support for database load balancing and fail-scenarios without the need to re-implement it in every database driver.
- Support for pre-compiled database commands/queries in databases that support it.
- Support for per-driver specific options.
- Better re-use of code commonly shared among database drivers, such as connection pooling and database URI parsing.
Design Goals
- Try to avoid using SQL queries assembled at runtime. If the database driver supports pre-compiled SQL queries, that is queries that are assembled and planned once and executed many times, try to use this and assemble queries in mod_init or child_init (during the SER initialization phase).
- The database abstraction layer should be designed in a way which would make integration with non-SQL based database systems easy and straightforward.
- Support for command executed on multiple database servers at the same time with various load-balancing and high-availability strategies.
Naming Conventions
Names of all core data structures in the database API start with `db_` prefix. For example `db_con` is a core db structure that represents a database connection, `db_uri` is a structure representing database URI, and so on. The `db_` prefix is reserved for the data structures in the SER core only and should not be used in data structures defined in database drivers to avoid naming conflicts.
Driver specific data structures, that is the data structures defined in SER database drivers for various database servers, such as MySQL, PostgreSQL, should have names starting with a two letter prefix that is unique for the driver. For example all data structures defined in the mysql SER module start with prefix "my_", thus the data structure `my_con` is the mysql-specific equivalent of structure `db_con`.
Extending Data Structures
TBD
Core Data Structures
Core data structures is a set of data structures that are defined in the database abstraction layer in SER core. They are defined in the header fields under ser/db directory.
''db_ctx'': Context
Database context is a data structure used to bind multiple database connections and an algorithm which controls how database commands are executed over those connections. Database contexts are used by database commands. Every database command contains a reference to one context which is used to communicate with database servers(s).
The purpose of the database context is to abstract database commands from database connections. This allows to separate fail-over and high-availability logic from database commands.
Every database context contains an array of connections and a textual identifier that is used to identify it.
''db_uri'': URI
''db_con'': Connections
Physical database connections, pooled, shared across modules.
''db_cmd'': Commands
Physical database connections, pooled, shared across modules
''db_drv'': Driver Structure Prototype
''db_fld'': Fields
Database record, there may be multiple records per result
''db_gen'': Shared Structure Members
''db_pool_entry'': Connection Pools
Pools of database connections shared across SER modules.
''db_rec'': Records
Database record, there may be multiple records per result.
''db_res'': Results
Physical database connections, pooled, shared across modules
Writing a New Database Driver
Database drivers are just ordinary SER modules. They typically do not provide any functions which would be directly usable from the configuration script. Instead they export an API which exports their functions and data structures to the data abstraction layer in SER.
A database driver is basically a collection of data structures and functions which in form of a SER module. Database drivers can be loaded into SER just like any other SER modules. There is nothing special about them in this respect.
The first thing you need to do if you want to create a new database driver for SER is to create a new SER module. The name of the module should be the same as the string identifying the database in database URI used in the configuration script of SER. If you name your new module "abcde" then the module will be used when the administrator uses a URI with scheme "abcde:" in the configuration file.
As the next step you should try to compile your new (yet empty) database driver. Make sure that you link it with the database client library that you are going to use and that you include at least one header file from the library.
If your driver compiles fine then you can define a minimalistic SER module interface (without any functions and parameters). Here is one example of such minimal SER module interface:
#include "../../sr_module.h" MODULE_VERSION static cmd_export_t cmds[]={ {0,0,0,0,0} }; /* Exported parameters static param_export_t params[] = { {0, 0, 0} }; struct module_exports exports= { "abcde", cmds, 0, /* RPC methods */ params, /* param exports */ 0, /* module initialization function */ (response_function) 0, 0, 0, 0 /* per-child init function */ };
After that you can compile your module again and then you can try to load the module into SER. The module wouldn't do anything yet but if SER starts fine then your skeleton driver module is working and you can proceed to implementation of various data structures and functions your driver must contain in order to be useful.
Extending Core Data Structures
The database related core data structures only contain a minimal set of variables and attributes. Typically it is the minimim amount of information that every database driver will use.
If you take a look at db_uri as an example, the data structure that represents databse URIs:
typedef struct db_uri { db_gen_t gen; /* Generic part of the structure */ str scheme; /* URI scheme */ str body; /* Entire URI body */ db_uri_cmp_t* cmp; /* Comparison function */ } db_uri_t;
You will see that except for some auxiliary variables the data structure contains only two fields related to the URI, the scheme and the body. It does not parse the body of the URI because it does not know anything about the format, different database drivers may use different formats of the body.
For example, if the URI was ''mysql://ser:heslo@localhost/ser'' then the scheme
part would contain mysql
and the body part would contain
ser:heslo@localhost/ser
.
But the database driver, which would be mysql in this example, would most likely need to parse the URI body even more and store various parts of it separately in separate variables because this is who the corresponding function of the mysqlclient library expects them.
And because there is no space for additional variables in db_uri
structure,
the driver has to create its own–driver specific–data structure and attach
is to the generic one. We say that the driver extends the generic db_uri
structure with driver-specific payload.
Define Payload Structure
In our example the mysql driver would define a structure called my_uri which would contain some additional variables.
struct my_uri { db_drv_t drv; char* username; char* password; char* host; unsigned short port; char* database; };
And this data structure will the extend the generic structure. Note the first variable in the data structure:
db_drv_t drv;
This is an auxiliary data structure which contains some data that has to be present in every structure with extends a core structure. We will not go into details yet, all you need to remember here is that:
- Every extension (that is structures that are supposed to extend the core db api structures) structure that you define must have this variable.
- Furthermore this variable has to be the first variable defined in the structure!
The rest of the variables contains various parts of the URI body.
Extending Core Structures
We now have the my_uri
structure declared in the mysql module, but how do we
make the core data layer use it? This section describes the process of
extending the core structure in the data abstraction layer.
First of all, all core data structure in the data abstraction layer are created whenever the data layer needs them to be created and destroyed whenever the data layer does not need them anymore. The database driver does not know anything about it nor it can affect it any way.
This is one of the bigger changes from the previous versions of the SER data abstraction layer. In previous versions SER would just call functions in database drivers and the database drivers had to create/destroy all data structures themselves.
Starting with SER 2.1, it is the data abstraction layer in the core who decideds when are the core data structures created/destroyed. The database driver cannot affect it (we simplify here a little bit), but it can define a function which will be called when a data structure is created and it can also define a function which will be called when the very same data structure is about to be destroyed. These two functions can be used to extend the core structure.
In our previous example we defined a data structure called my_uri
and we
want to have the core structure db_uri
extended with this structure. When
SER creates a new object from the db_uri
structure, it would search the list
of functions exported by the database driver for a function with the same
name. In this case it would search the list of all functions exported by the
mysql module and if it finds a function called db_uri
then the function will
be called immediately after the object is created.
Because the mysql driver wants to extend the structure, it will export the function in its module API:
static cmd_export_t cmds[] = { {"db_uri", (cmd_function)my_uri, 0, 0, 0},
and defines the function to be called:
int my_uri(db_uri_t* uri) { struct my_uri* res; res = (struct my_uri*)pkg_malloc(sizeof(struct my_uri)); if (res == NULL) { ERR("mysql: No memory left\n"); goto error; } memset(res, '\0', sizeof(struct my_uri)); if (db_drv_init(&res->drv, my_uri_free) < 0) goto error; if (parse_mysql_uri(res, &uri->body) < 0) goto error; DB_SET_PAYLOAD(uri, res); uri->cmp = my_uri_cmp; return 0; error: if (res) { db_drv_free(&res->drv); if (res) pkg_free(res); } return -1; }
The function gets the newly created db_uri object, which contains the body of the URI for the function to parse. As you can see above the function does a few steps worth explaining.
First of all a new structure my_uri
is allocated and the memory of the
object is reset.
After that the data structure must be initialized by calling db_drv_init
function. This function is used to initialize the mysterious db_drv_t drv
variable which we mentioned in the section describing my_uri
.
The second parameter is a pointer to another function, my_uri_free
, more on
that later.
parse_mysql_uri
parses the URI body stored in db_uri→body
.
And then the structure is ready to be attached to the db_uri
structure to
extend it, this is done by calling DB_SET_PAYLOAD
macro. The macro takes a
ponter to the generic db_uri
structure as the first parameter and a pointer
to the newly created my_uri
as the second parameter and extends the generic
structure.
The newly created structure is now attached to it and contains all the additional information that the mysql driver needs.
Destroying Payload
When the core data layer destroys the generic structure it also has to destroy the driver specific payload which we just attached to it. Because it does not know anything about its variables and how/if they need to be destroyed, it will call a function which we provided to it and which will take care of the payload. Here is how the function can look like:
static void my_uri_free(db_uri_t* uri, struct my_uri* payload) { if (payload == NULL) return; db_drv_free(&payload->drv); if (payload->username) pkg_free(payload->username); if (payload->password) pkg_free(payload->password); if (payload->host) pkg_free(payload->host); if (payload->database) pkg_free(payload->database); pkg_free(payload); }
Accessing Payload
Accessing the driver specific payload attached to a core structure is
easy. A function called by the data abstraction layer would receive a pointer
to a generic structure as a parameter and it can call DB_GET_PAYLOAD
macro
to obtain a pointer to the payload:
/* * Compare two connection identifiers */ static unsigned char my_uri_cmp(db_uri_t* uri1, db_uri_t* uri2) { struct my_uri* muri1, *muri2; if (!uri1 || !uri2) return 0; muri1 = DB_GET_PAYLOAD(uri1); muri2 = DB_GET_PAYLOAD(uri2); if (muri1->port != muri2->port) return 0; if (cmpstr(muri1->username, muri2->username, strcmp)) return 0; if (cmpstr(muri1->password, muri2->password, strcmp)) return 0; if (cmpstr(muri1->host, muri2->host, strcasecmp)) return 0; if (cmpstr(muri1->database, muri2->database, strcmp)) return 0; return 1; }
The example above shows function my_uri_cmp which is used to compare two mysql URIs. The function would extract the payload from both of them and then compare various fields in the payloads.