Showing posts with label transaction. Show all posts
Showing posts with label transaction. Show all posts

Wednesday, February 19, 2014

New feature? / Nova funcionalidade?

This article is written in English and Portuguese (original version here)
Este artigo está escrito em Inglês e Português (versão original aqui)


English version:

The fun...

Working with Informix has been much fun, but sometimes for strange reasons. One of those is the consequence of not being considered a "mainstream" database. From time to time I see references to features of "mainstream" databases that do amuse me. It surely happens the other way around, but the echoes of that are naturally smaller and could be expected from what bloggers and analysts would consider a non-Tier 1 database. So, when this happens It really makes my day...
This time, while browsing the Net, I noticed several articles or posts talking about a "new fascinating feature" of SQL Server 2014 CTP2 (pre-release) called Delayed Transaction Durability. Well... After reading some of those posts including the "official source" I was a bit surprised that this is just what we call buffered logging! Yes... The ability to delay logical log buffer flush until the logical log buffer is full. And yes, same as us, it means that an application can assume something is committed while if there is a database crash it won't (the fast recovery process will rollback the transaction).
So, as you may expect, most bloggers were careful to note this could lead to data loss. In fact the database integrity is preserved, but because the application receives the commit "ok" message before it was actually written to disk, a crash in the specific interval would mean the transaction would be incomplete so a rollback would happen during recovery.
So why use it? Performance... But in fact most customers don't want to risk and they usually choose unbuffered logging.

What we have

But why am I writing this? Just to make fun of our competitor and the fact that they're announcing and talking about a feature that everybody else (Informix, Oracle, DB2, MySQL, Postgres...) seems to have? Not really... although it is funny to see this situation over and over again (happened recently with SQL Server's high availability options also...).
The fact is that there's more to this than what immediately comes to mind. First, I'd bet most of our customers know about the [BUFFERED] LOG option of the CREATE DATABASE statement, but they possibly don't know about the SET [BUFFERED] LOG statement. Did I catch you? Read on... Secondly, because there are at least two databases that implemented this better than us (and I find it very little ambitious from Microsoft to implement just what I'd call "basic" - if you're doing something new, you may as well aim for the best available). So, let's start by the SQL statement SET [BUFFERED] LOG.
I could track this down to at least version 7.3 of Informix Dynamic Server's (1998) documentation as well as the Online engine. And this matches more or less the functionality that SQL Server is implementing now (16 years later, not bad, right?). It means that even in an unbuffered database, you can ask the server to work with your session as if it was setup for BUFFERED logging. In other words, COMMITs issued by sessions that execute SET BUFFERED LOG won't cause the flush of the logical log buffer to disk. They will behave as if you had created the database with BUFFERED LOG. Consequently you're possibly contributing to the database performance, while you open the window to "data loss" only in your session.
Alternatively you can execute the SET LOG statement and ask the server to flush the logical log buffer on every COMMIT you make. You'll make sure that your commits are persisted to disk even if you're working on a BUFFERED database.
We can see the effect of this statement quite easily. The test case I created is fairly simple:
  1. Create a very simple table with an ID (INTEGER) and some other column - VAL (CHAR(1)) - with 1M rows in an UNBUFFERED LOG database
  2. Create a procedure that accepts the number of records to update, the commit interval and the new value
  3. Reset the engine counters (onstat -z)
  4. Set either BUFFERED or UNBUFFERED LOG level for the session
  5. Execute the procedure with some values
  6. Check the statistics with onstat -l
  7. Repeat from 3 using a different logging mode and compare the times and specially the counter values
So let's do it. The table and procedure SQL is this:
castelo@primary:informix-> cat test_buf.sql 
DROP PROCEDURE IF EXISTS test_proc;
DROP TABLE IF EXISTS test_data;
SELECT LEVEL id,"A" val FROM sysmaster:sysdual CONNECT BY LEVEL <= 1000000 INTO RAW test_data IN dbs1 EXTENT SIZE 5000 NEXT SIZE 5000;
ALTER TABLE test_data TYPE(standard);

CREATE PROCEDURE test_proc(total_rec INTEGER, commit_interval INTEGER, new_value CHAR) RETURNING INTEGER;

DEFINE total_counter, commit_counter, v_id, cycle INTEGER;

LET total_counter=0;
LET commit_counter=0;
LET cycle = 0;

BEGIN WORK;
FOREACH c1 WITH HOLD FOR
SELECT
        id
INTO v_id
FROM
        test_data

        UPDATE test_data SET val = new_value WHERE CURRENT OF c1;
        LET total_counter = total_counter + 1;
        LET commit_counter = commit_counter + 1;
        IF commit_counter = commit_interval
        THEN
                LET cycle = cycle + 1;
                COMMIT WORK;
                LET commit_counter = 0;
                BEGIN WORK;
        END IF;
        IF total_counter = total_rec
        THEN
                COMMIT WORK;
                RETURN cycle;
        END IF
END FOREACH;

END PROCEDURE;
Now, let's try it with a COMMIT interval of 100 records and UNBUFFERED LOG. The code and output is this:
castelo@primary:informix-> dbaccess -e stores run_unbuf.sql 

Database selected.

SET LOG;
Log set.


EXECUTE FUNCTION sysadmin:task('onstat', '-z');


(expression)  
              IBM Informix Dynamic Server Version 12.10.FC2 -- On-Line -- Up 09
              :01:52 -- 287720 Kbytes
              
               

1 row(s) retrieved.


SELECT CURRENT YEAR TO FRACTION FROM systables WHERE tabid = 1;

(expression)            

2014-02-17 18:57:09.622

1 row(s) retrieved.


EXECUTE PROCEDURE test_proc(500000,100,'U');

(expression) 

        5000

1 row(s) retrieved.


SELECT CURRENT YEAR TO FRACTION FROM systables WHERE tabid = 1;

(expression)            

2014-02-17 18:57:20.242

1 row(s) retrieved.



Database closed. 

It took around 11-12s but the real important part is this:
castelo@primary:informix-> onstat -l

IBM Informix Dynamic Server Version 12.10.FC2 -- On-Line -- Up 09:02:15 -- 287720 Kbytes

Physical Logging
Buffer bufused  bufsize  numpages   numwrits   pages/io
  P-2  15       64       14         0          0.00
      phybegin         physize    phypos     phyused    %used   
      2:53             62500      50947      44         0.07    

Logical Logging
Buffer bufused  bufsize  numrecs    numpages   numwrits   recs/pages pages/io
  L-2  0        64       510094     20018      5015       25.5       4.0     
        Subsystem    numrecs    Log Space used
        OLDRSAM      510094     38569892

Note that we've done 5015 write operations to disk. On each of them, on average we were writing four pages of logical log buffer. Each page contains around 25 records, so as we've asked for a COMMIT interval each 100 rows, everything matches what we'd expect.
Let's try with BUFFERED LOG:
castelo@primary:informix-> dbaccess -e stores run_buf.sql 

Database selected.

SET BUFFERED LOG;
Log set.


EXECUTE FUNCTION sysadmin:task('onstat', '-z');


(expression)  
              IBM Informix Dynamic Server Version 12.10.FC2 -- On-Line -- Up 09
              :07:52 -- 287720 Kbytes
              
               

1 row(s) retrieved.


SELECT CURRENT YEAR TO FRACTION FROM systables WHERE tabid = 1;

(expression)            

2014-02-17 19:03:08.698

1 row(s) retrieved.


EXECUTE PROCEDURE test_proc(500000,100,'B');

(expression) 

        5000

1 row(s) retrieved.


SELECT CURRENT YEAR TO FRACTION FROM systables WHERE tabid = 1;

(expression)            

2014-02-17 19:03:17.004

1 row(s) retrieved.



Database closed.
It took 8-9s, so it's a bit faster, but this is a VM, with no more activity... But the more interesting part is the logical log statistics we get from onstat -l:

castelo@primary:informix-> onstat -l

IBM Informix Dynamic Server Version 12.10.FC2 -- On-Line -- Up 09:10:44 -- 287720 Kbytes

Physical Logging
Buffer bufused  bufsize  numpages   numwrits   pages/io
  P-1  18       64       17         0          0.00
      phybegin         physize    phypos     phyused    %used   
      2:53             62500      50974      29         0.05    

Logical Logging
Buffer bufused  bufsize  numrecs    numpages   numwrits   recs/pages pages/io
  L-2  0        64       510095     19119      313        26.7       61.1    
        Subsystem    numrecs    Log Space used
        OLDRSAM      510095     38570264


Let's compare both outputs:
  • The number of records and log space used it roughly the same
  • The number of records per page is roughly the same
  • The number of writes (313) is much less than for UNBUFFERED mode (5015)
  • The number of pages on each write (average) if much higher now (61.1) as opposed to 4 in the previous test (I had a LOGBUFF size of 128KB)

What we're missing

Now, let's look at the other more interesting aspect of this.... I mentioned earlier that at least two databases do this in a smarter way than Informix. I'm thinking about DB2 and Oracle. How can this be done in a smarter way? Well, as you notice, the BUFFERED logging is a trade-off. You exchange security for performance (you give away the first and gain on the second). What if there was a better solution? What if you could gain on I/O performance, by reducing the number of operations while not giving away the durability of your data? It may seem impossible, but it's actually very easy and has been done. Let's assume what we have right now in most customers:
  • Lots of sessions and most of them do a commit from time to time
  • Many sessions making commits from time to time, usually means a very frequent commit rate
  • Very frequent commit rates means that we'll do a lot of logical log flushes per second. This is usually noticeable from the average pages per logical log flush. On busy systems with UNBUFFERED LOG this tends to be 1
The way other RDBMs can be configured is to don't flush on every commit but:
  1. Flush when the buffer is full (this always happen)
  2. Flush the logical log buffer if an amount of time has elapsed since the last flush, or flush only after a specific number of COMMITS have been issued
  3. Only send the ok to the application after you effectively flush the buffer that contains the COMMIT
This may seem a bit strange, because you're effectively "holding back" the applications. But keep in mind that this delay can be very small and is optional. The difference between this and the BUFFERED LOG is that the application will probably get only a slight delay, but more importantly, it won't receive an OK of an "uncommitted" COMMIT. When it gets the "ok", the data is securely flushed to the logical logs. Assuming this can be configure at the session level, we can get the best of both worlds (as there  is no gain without loss, the loss here is the probably slight delay, which for interactive applications at least would be unnoticeable)

I've seen situations where the I/O rate on the logical logs can be a bottleneck. As such I've created an RFE (Request For Enhancement) 45166 . If you like the idea and you've seen this happen on your system, vote for it


Versão Portuguesa:

A parte engraçada...

Trabalhar com Informix tem sido bastante engraçado, mas por vezes é por razões estranhas. Uma delas é a consequência de não ser considerada uma base de dados mainstream. De tempos a tempos vejo referências a "novas" funcionalidades nas bases de dados mais populares que me fazem sorrir. Certamente que o contrário também acontece, mas os ecos dessas "novas" funcionalidades no Informix são sempre menores que nos outros,  e seria algo normal numa base de dados que muitos bloggers e analistas não consideram Tier-1. Portanto quando tal acontece, ganho o dia...
Desta feita, ao navegar pela Internet, reparei em vários artigos referindo-se a (minha tradução) "funcionalidade nova e fascinante" do SQL Server 2014 CTP2 (ante-visão) chamada Delayed Transaction Durability. Bom... Depois de ler alguns destes artigos, incluindo o "oficial" fiquei um pouco surpreendido que isto seja apenas aquilo a que chamamos buffered logging! Sim... A possibilidade de atrasar o flush do buffer do logical log até que o mesmo buffer se encontre cheio, em vez de o fazer a cada COMMIT. E sim, tal como nós, isto significa que a aplicação assume que algo foi efetivamente "COMMITed", ao passo que se houver uma queda inesperada da base de dados na verdade não foi (o processo de fast recovery irá fazer rollback da transação).
Portanto, como seria de esperar, muitos autores de blogs foram cautelosos e referiram que isto pode levar à perda de dados. Na verdade a integridade da base de dados é mantida, mas como a aplicação recebe o "ok" antes de os dados estarem efetivamente escritos em disco, uma queda num intervalo específico, significaria que a transação estaria incompleta , levando portanto a um rollback durante o processo de recovery.
Então para quê usar isto? Rapidez... Mas na realidade a maioria dos clientes não quer arriscar, e habitualmente escolhem unbuffered logging.

A parte que temos

Mas porque estou a escrever isto? Apenas para brincar com a concorrência, realçando o facto de estarem a anunciar uma funcionalidade que todas a bases de dados (Informix, DB2, Oracle, MySQL, Postgres...) já têm? Não... não é por isso. Apesar de ser divertido verificar este tipo de situações com frequência (aconteceu recentemente com as suas funcionalidades de alta disponibilidade...).
O ponto é que este assunto tem outros aspetos interessantes para além do óbvio. Primeiro, apostaria que a maioria dos nossos clientes conhecem a opção [BUFFERED] LOG da instrução CREATE DATABASE. mas possivelmente desconhecem a instrução SET [BUFFERED] LOG. Apanhei-o? Continue a ler... Em segundo, existem pelo menos duas bases de dados que implementaram isto de forma mais "inteligente" que o Informix (e parece-me pouco ambicioso da parte da Microsoft fazer a implementação "básica" - se vamos criar algo de novo, porque não apontar para o melhor possível?). Veremos como isso pode ser feito e quais as diferenças. Comecemos então pela instrução SET [BUFFERED] LOG.
Consegui encontrar referências a esta instrução pelo menos tão antigas quanto a versão 7.3 do Informix Dynamic Server (1998), e também na documentação do motor Online. E isto mapeia mais ou menos diretamente com a funcionalidade que o SQL Server está a receber agora (16 anos depois não é mau, certo?). Significa que mesmo numa base de dados criada com unbuffered logging, podemos pedir ao servidor que trabalhe na nossa sessão como se estivesse em BUFFERED LOG. Por outras palavras, o COMMIT efetuado por sessões que executem o SET BUFFERED LOG, não força o flush do buffer do logical log. As sessões comportam-se como se tivéssemos criado a base de dados com BUFFERED LOG. Assim estaremos a contribuir para o aumento da performance da base de dados, ao mesmo tempo que limitamos a possibilidade de "perda de dados" apenas à(s) sessão que executou esta instrução.
Noutro cenário podemos executar a instrução SET LOG e pedir ao servidor que faça o flush do logical log buffer em cada COMMIT que façamos. Garantiremos que todos os nossos COMMITs são escritos em disco, antes de recebermos o "ok", mesmo que a base de dados esteja em modo BUFFERED.
Podemos ver o efeito desta instrução de forma bastante fácil. O caso de teste que criei é bastante simples:
  1. Criar uma tabela muito simples com um ID (INTEGER) e uma outra coluna - VAL (CHAR(1)) - com 1M de registos numa base de dados criada com UNBUFFERED LOG
  2. Criar um procedimento que aceita o número de registos a alterar, o intervalo de COMMIT e um novo valor para a coluna VAL
  3. Fazer o reset dos contadores do motor (com onstat -z)
  4. Estabelecer o modo BUFFERED ou UNBUFFERED LOG na nossa sessão
  5. Executar o procedimento com certos valores
  6. Verificar as estatísticas com onstat -l
  7. Repetir a partir do ponto 3 usando um modo de LOG diferente e comparar os tempos e mais importante os valores dos contadores
Vamos lá então fazê-lo. A tabela e o procedimento são os seguintes:
castelo@primary:informix-> cat test_buf.sql 
DROP PROCEDURE IF EXISTS test_proc;
DROP TABLE IF EXISTS test_data;
SELECT LEVEL id,"A" val FROM sysmaster:sysdual CONNECT BY LEVEL <= 1000000 INTO RAW test_data IN dbs1 EXTENT SIZE 5000 NEXT SIZE 5000;
ALTER TABLE test_data TYPE(standard);

CREATE PROCEDURE test_proc(total_rec INTEGER, commit_interval INTEGER, new_value CHAR) RETURNING INTEGER;

DEFINE total_counter, commit_counter, v_id, cycle INTEGER;

LET total_counter=0;
LET commit_counter=0;
LET cycle = 0;

BEGIN WORK;
FOREACH c1 WITH HOLD FOR
SELECT
        id
INTO v_id
FROM
        test_data

        UPDATE test_data SET val = new_value WHERE CURRENT OF c1;
        LET total_counter = total_counter + 1;
        LET commit_counter = commit_counter + 1;
        IF commit_counter = commit_interval
        THEN
                LET cycle = cycle + 1;
                COMMIT WORK;
                LET commit_counter = 0;
                BEGIN WORK;
        END IF;
        IF total_counter = total_rec
        THEN
                COMMIT WORK;
                RETURN cycle;
        END IF
END FOREACH;

END PROCEDURE;
Vamos tentar com um intervalo de de COMMIT e UNBUFFERED LOG. O código e o resultado é o seguinte:
castelo@primary:informix-> dbaccess -e stores run_unbuf.sql 

Database selected.

SET LOG;
Log set.


EXECUTE FUNCTION sysadmin:task('onstat', '-z');


(expression)  
              IBM Informix Dynamic Server Version 12.10.FC2 -- On-Line -- Up 09
              :01:52 -- 287720 Kbytes
              
               

1 row(s) retrieved.


SELECT CURRENT YEAR TO FRACTION FROM systables WHERE tabid = 1;

(expression)            

2014-02-17 18:57:09.622

1 row(s) retrieved.


EXECUTE PROCEDURE test_proc(500000,100,'U');

(expression) 

        5000

1 row(s) retrieved.


SELECT CURRENT YEAR TO FRACTION FROM systables WHERE tabid = 1;

(expression)            

2014-02-17 18:57:20.242

1 row(s) retrieved.



Database closed. 
Demorou à volta de 11-12s, mas a parte mais importante é esta:
castelo@primary:informix-> onstat -l

IBM Informix Dynamic Server Version 12.10.FC2 -- On-Line -- Up 09:02:15 -- 287720 Kbytes

Physical Logging
Buffer bufused  bufsize  numpages   numwrits   pages/io
  P-2  15       64       14         0          0.00
      phybegin         physize    phypos     phyused    %used   
      2:53             62500      50947      44         0.07    

Logical Logging
Buffer bufused  bufsize  numrecs    numpages   numwrits   recs/pages pages/io
  L-2  0        64       510094     20018      5015       25.5       4.0     
        Subsystem    numrecs    Log Space used
        OLDRSAM      510094     38569892

Repare que fizemos 5015 operações de escrita em disco. Em cada uma delas, em média, escrevemos 4 páginas do logical log buffer. Cada página contém em média 25 registos (de log), portanto como pedimos COMMITs de 100 em 100 registos os valores batem certo com o que seria expectável.
Vamos tentar com BUFFERED LOG:
castelo@primary:informix-> dbaccess -e stores run_buf.sql 

Database selected.

SET BUFFERED LOG;
Log set.


EXECUTE FUNCTION sysadmin:task('onstat', '-z');


(expression)  
              IBM Informix Dynamic Server Version 12.10.FC2 -- On-Line -- Up 09
              :07:52 -- 287720 Kbytes
              
               

1 row(s) retrieved.


SELECT CURRENT YEAR TO FRACTION FROM systables WHERE tabid = 1;

(expression)            

2014-02-17 19:03:08.698

1 row(s) retrieved.


EXECUTE PROCEDURE test_proc(500000,100,'B');

(expression) 

        5000

1 row(s) retrieved.


SELECT CURRENT YEAR TO FRACTION FROM systables WHERE tabid = 1;

(expression)            

2014-02-17 19:03:17.004

1 row(s) retrieved.



Database closed.
Demorou 8-9s, por isso foi um pouco mais rápido, mas isto é uma máquina virtual sem mais actividade... Mas a parte mais interessante são as estatísticas do logical log que obtemos com o onstat -l:

castelo@primary:informix-> onstat -l

IBM Informix Dynamic Server Version 12.10.FC2 -- On-Line -- Up 09:10:44 -- 287720 Kbytes

Physical Logging
Buffer bufused  bufsize  numpages   numwrits   pages/io
  P-1  18       64       17         0          0.00
      phybegin         physize    phypos     phyused    %used   
      2:53             62500      50974      29         0.05    

Logical Logging
Buffer bufused  bufsize  numrecs    numpages   numwrits   recs/pages pages/io
  L-2  0        64       510095     19119      313        26.7       61.1    
        Subsystem    numrecs    Log Space used
        OLDRSAM      510095     38570264

Vamos comparar ambos os outputs:
  • O número de registos e o espaço em log é praticamente o mesmo
  • O número de registos por página é praticamente o mesmo
  • O número de escritas (313) é muito menos que o efetuado em modo UNBUFFERED  (5015)
  • O número de páginas escritas em cada operação (média) é muito maior (61.1) do que o teste anterior que deu 4 páginas por escrita (tinha o LOGBUFF definido como 128KB)

O que nos falta

Bom, vamos agora ver outro aspecto importante deste assunto... Como referi, existem pelo menos duas bases de dados que fazem isto de forma mais eficiente e interessante que o Informix. Estou a pensar no DB2 e no Oracle. Como é que isto pode ser feito de forma mais inteligente? Bom, como terá reparado, o BUFFERED logging é uma troca. Trocamos segurança por rapidez (damos a primeira e recebemos a segunda). Mas e se houver uma melhor solução? E se conseguissemos ganhar rapidez (fazendo menos I/O) e ao mesmo tempo não abdicar da segurança que a escrita prévia nos dá? Pode parecer impossível, mas na verdade pode ser muito fácil e já foi feito. Vamos assumir o cenário que encontro em muitos clientes atualmente:
  • Muitas sessões e a maioria delas fazem um COMMIT de vez em quando
  • Muitas sessões a fazerem COMMIT "de vez em quando", traduz-se normalmente num ritmo bastante alto de COMMITs
  • Um ritmo de COMMITs muito alto implica que façamos muitos flushes do logical log buffer por segundo. Isto é normalmente visível pelo número médio de páginas escritas em cada operação de I/O (flush), que em sistemas configurados em UNBUFFERED e com bastante actividade tende a ser 1
A forma como outras RDBMS podem ser configuradas é não fazer flush em cada COMMIT, mas:
  1. Fazer o flush quando o bufer enche (isto acontece sempre)
  2. Fazer o flush do logical log biffer se passou um determinado tempo desde o último flush, ou fazer o flush após um certo número de COMMITS terem sido executados
  3. Apenas enviar o "ok" às aplicacções quando fazemos efectivamente o flush do biuffer que contém o COMMIT por elas executado
Isto pode parecer um pouco estranho, porque estamos efetivamente a "atrasar" as aplicações. Mas considere que este atraso é muito pequeno e opcional. A diferença entre isto e o BUFFERED LOG é que a aplicação apenas sofre um ligeiro atraso, mas mais importante não recebe "ok" de um COMMIT que efectuou mas que ainda não foi garantido em disco. Quando recebe o "ok" é certo que os registos do log já foram escritos e persistidos em disco. Tendo em conta que isto pode ser configurado ao nível da sessão, podemos obter o melhor dos dois mundos (embora como não haja ganhos sem perdas, a perda aqui será o pequeno atraso, mas este para aplicações interativas é provavelmente negligenciável)

Já vi situações onde o ritmo de operações de I/O nos logical logs pode ser um "fúnil". Dái ter registado um RFE (Request For Enhancement) 45166 . Se gosta da ideia e já viu o mesmo acontecer no seu sistema vote neste pedido.

Wednesday, November 24, 2010

UDRs: In transaction? / Em transacção?

This article is written in English and Portuguese
Este artigo está escrito em Inglês e Português

English version:

Introduction

I just checked... This will be post 100!!!... I've never been so active in the blog... We have Panther (full of features that I still haven't covered), I did some work with OAT and tasks that I want to share, and besides that I've been trying some new things... Yes... Although I've been working with Informix for nearly 20 years (it's scaring to say this but it's true...) there are aspects that I usually don't work with. I'd say the one I'm going to look into today is not used by the vast majority of the users. And that's a shame because:

  1. It can solve problems that we aren't able to solve in any other way
  2. If it was more used, it would be improved faster
Also, many people think that this is what marked the decline of the Informix company. You probably already figured out that I'm talking about extensibility. To recall a little bit of history, in 1995, Informix had the DSA architecture in version 7. And it acquired the Illustra company, founded by Michael Stonebraker and others. Mr. Stonebraker already had a long history of innovation (which he kept improving up to today) and he stayed with Informix for some years. All the technology around Datablades and extensibility in Informix comes from there... Informix critics say that the company got so absorbed in the extensibility features (that it believed would be the "next wave") that it loosed the market focus. Truth is that the extensibility never became a mainstream feature neither in Informix or in other databases, and all of them followed Informix launch of Universal Server (1996): Oracle, IBM DB2 etc.

But, this article will not focus on the whole extensibility concept. It would be impossible and tedious to try to cover it in one blog article. Instead I'll introduce one of it's aspects: User Defined Routines (UDRs), and in particular routines written using the C language.

There is a manual about UDRs, and I truly recommend that you read it. But here I'll follow another approach: We'll start with a very simple problem that without C UDRs would be impossible to solve, define a solution for it, and go all the way to implement it and use it with an example.


The problem

Have you ever faced a situation where you're writing a stored procedure in SPL, and you want to put some part of it inside a transaction, but you're afraid that the calling code is already in a transaction?
You could workaround this by initiating the transaction and catching the error (already in transaction) with an ON EXCEPTION block.
But this may have other implications (ON EXCEPTION blocks are tricky when the procedure is called from a trigger). So it would be nice to check if the session is already in a transaction. A natural way to do that would be a call to DBINFO(...), but unfortunately current versions (up to 11.7.xC1) don't allow that. Meaning there is no DBINFO() parameter that makes it return that information.


Solution search

One important part of Informix extensibility is the so called Datablade API. It's a set of programmable interfaces that we can use inside Datablades code and also inside C UDRs. The fine infocenter has a reference manual for the Datablade API functions. A quick search there for "transaction" makes a specific function come up: mi_transaction_state()
The documentation states that when calling it (no parameters needed) it will return an mi_integer (let's assume integer for now) type with one of these values:
  • MI_NO_XACT
    meaning we don't have a transaction open
  • MI_IMPLICIT_XACT
    meaning we have an implicit transaction open (for example if we're connected to an ANSI mode database)
  • MI_EXPLICIT_XACT
    meaning we have an explicit transaction open
This is all we need conceptually.... Now we need to transform ideas into runnable code!

Starting the code

In order to implement a C UDR function we should proceed through several steps:
  1. Create the C code skeleton
  2. Create the C code function using the Datablade API
  3. Create a makefile that has all the needed instructions to generate the executable code in a format the engine can use
  4. Compile the code
  5. Use SQL to define the new function, telling the engine where it can find the function and the interface to call it, as well as the language and other function attributes
  6. Test it!



Create the C code skeleton

Informix provides a tool called DataBlade Developers Kit (DBDK) which includes several components: Blade Manager, Blade Pack and Bladesmith. Blade Manager allows us to register the datablades against the databases, the Blade Pack does the "packaging" of all the Datablades files (executable libraries, documentation files, SQL files etc.) that make up a datablade. Finally Bladesmith helps us to create the various components and source code files. It's a development tool that only runs on Windows but can also be used to prepare files for Unix/Linux. For complex projects it may be a good idea to use Bladesmith, but for this very simple example I'll do it by hand. Also note I'm just creating a C UDR. These tools are intended to deal with much more complex projects. A Datablade can include new datatypes, several functions etc.
So, for our example I took a peek at the recent Informix Developer's Handbook to copy the examples.

Having looked at the examples above, it was easy to create the C code:


/*
This simple function returns an integer to the calling SQL code with the following meaning:
0 - We're not within a transaction
1 - We're inside an implicit transaction
2 - We're inside an explicit (BEGIN WORK...) transaction
-1 - Something unexpected happened!
*/

#include <milib.h>

mi_integer get_transaction_state_c( MI_FPARAM *fp)
{
mi_integer i,ret;
i=mi_transaction_state();
switch(i)
{
case MI_NO_XACT:
ret=0;
break;
case MI_IMPLICIT_XACT:
ret=1;
break;
case MI_EXPLICIT_XACT:
ret=2;
break;
default:
ret=-1;
}
return (ret);
}

I've put the above code in a C source file called get_transaction_state_c.c

Create the makefile

Again, for the makefile I copied some examples and came up with the following. Please consider this as an example only. I'm not an expert on makefile building and this is just a small project.


include $(INFORMIXDIR)/incl/dbdk/makeinc.linux

MI_INCL = $(INFORMIXDIR)/incl
CFLAGS = -DMI_SERVBUILD $(CC_PIC) -I$(MI_INCL)/public $(COPTS)
LINKFLAGS = $(SHLIBLFLAG) $(SYMFLAG)


all: get_transaction_state.so

clean:
rm -f get_transaction_state.so get_transaction_state_c.o


get_transaction_state_c.o: get_transaction_state_c.c
$(CC) $(CFLAGS) -o $@ -c $?

get_transaction_state.so: get_transaction_state_c.o
$(SHLIBLOD) $(LINKFLAGS) -o $@ $?


Note that this is a GNU Make makefile. The first line includes a makefile that IBM supplies with Informix. It basically contains variables or macro definitions. You should adapt the include directive to your system (the name of the makefile can vary with the platform) and make sure that the variables I use are also defined in your system base makefile.
After that I define some more variables and I create the makefile targets. I just want it to build the get_transaction_state.so dynamically loadable library and for that I'm including the object (get_transaction_state_c.o) generated from my source code (get_transaction_state_c.c). Pretty simple if you have basic knowledge about makefiles

Compile the code

Once we have the makefile we just need to run a simple command to make it compile:


cheetah@pacman.onlinedomus.net:informix-> make
cc -DMI_SERVBUILD -fpic -I/usr/informix/srvr1150uc7/incl/public -g -o get_transaction_state_c.o -c get_transaction_state_c.c
gcc -shared -Bsymbolic -o get_transaction_state.so get_transaction_state_c.o
cheetah@pacman.onlinedomus.net:informix->
The two commands run are the translation of the macros/variables defined in the makefile(s) and they simply compile the source code (1st command) and then generate the dynamic loadable library. If all goes well (as it naturally happened in the output above), we'll have a library on this location, ready for usage by Informix:


cheetah@pacman.onlinedomus.net:informix-> ls -lia *.so
267913 -rwxrwxr-x 1 informix informix 5639 Nov 23 22:06 get_transaction_state.so
cheetah@pacman.onlinedomus.net:informix-> file *.so
get_transaction_state.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (GNU/Linux), dynamically linked, not stripped
cheetah@pacman.onlinedomus.net:informix->

Use SQL to define the function

Now that we have executable code, in the form of a dynamic loadable library, we need to instruct Informix to use it. For that we will create a new function, telling the engine that it's implemented in C language and the location where it's stored. For that I created a simple SQL file:


cheetah@pacman.onlinedomus.net:informix-> ls *.sql
get_transaction_state_c.sql
cheetah@pacman.onlinedomus.net:informix-> cat get_transaction_state_c.sql
DROP FUNCTION get_transaction_state_c;

CREATE FUNCTION get_transaction_state_c () RETURNING INTEGER
EXTERNAL NAME '/home/informix/udr_tests/get_transaction_state/get_transaction_state.so'
LANGUAGE C;
cheetah@pacman.onlinedomus.net:informix->


So, let's run it...:


cheetah@pacman.onlinedomus.net:informix-> dbaccess stores get_transaction_state_c.sql

Database selected.


674: Routine (get_transaction_state_c) can not be resolved.

111: ISAM error: no record found.
Error in line 1
Near character position 36

Routine created.


Database closed.

cheetah@pacman.onlinedomus.net:informix->


Note that the -674 error is expected, since my SQL includes a DROP FUNCTION. If I were using 11.7 (due to several tests I don't have it ready at this moment) I could have used the new syntax "DROP IF EXISTS...".

So, after this step I should have a function callable from the SQL interface with the name get_transaction_state_c(). It takes no arguments and returns an integer value.

Test it!

Now it's time to see it working. I've opened a session in stores database and did the following:
  1. Run the function. It returned "0", meaning no transaction was opened.
  2. Than I opened a transaction and run it again. It returned "2", meaning an explicit transaction was opened
  3. I closed the transaction and run the function by the third time. As expected it returned "0"
Here is the output:


cheetah@pacman.onlinedomus.net:informix-> dbaccess stores -

Database selected.

> EXECUTE FUNCTION get_transaction_state_c();


(expression)

0

1 row(s) retrieved.

> BEGIN WORK;

Started transaction.

> EXECUTE FUNCTION get_transaction_state_c();


(expression)

2

1 row(s) retrieved.

> ROLLBACK WORK;

Transaction rolled back.

> EXECUTE FUNCTION get_transaction_state_c();


(expression)

0

1 row(s) retrieved.

>

We haven't seen it returning "1". That happens when we're inside an implicit transaction. This situation can be seen if we use the function in an ANSI mode database. For that I'm going to use another database (stores_ansi), and naturally I need to create the function there (using the previous SQL statements). Then I repeat more or less the same steps and the result is interesting:


cheetah@pacman.onlinedomus.net:informix-> dbaccess stores_ansi -

Database selected.

> EXECUTE FUNCTION get_transaction_state_c();


(expression)

0

1 row(s) retrieved.

> SELECT COUNT(*) FROM systables;


(count(*))

83

1 row(s) retrieved.

> EXECUTE FUNCTION get_transaction_state_c();


(expression)

1

1 row(s) retrieved.

>
If you notice it, the first execution returns "0". Since I have not done any operations there is no transaction opened. But just after a simple SELECT, the return is "1", meaning an implicit transaction is opened. This has to do with the nature and behavior of ANSI mode databases.
If you use them and you intend to use this function you must take that into consideration. Or you could simply map the "1" and "2" output of the mi_transaction_state() function return into simply a "1". This would signal that a transaction is opened (omitting the distinction between implicit and explicit transactions).

Final considerations

Please keep in mind that this article serves more as a light introduction to the C language UDRs than to solve a real problem. If you need to know if you're already in transaction (inside a stored procedure for example) you can use this solution, but you could as well try to open a transaction and capture and deal with the error inside an ON EXCEPTION block.

Also note that if this is a real problem for your applications, even if you're inside an already opened transaction, you can make your procedure code work as a unit, by using the SAVEPOINT functionality introduced in 11.50. So, in simple pseudo-code it would be done like this:

  1. Call get_transaction_state_c()
  2. If we're inside a transaction then set TX="SVPOINT" and create a savepoint called "MYSVPOINT" and goto 4)
  3. If we're not inside a transaction than set TX="TX" and create one. Goto 4
  4. Run our procedure code
  5. If any error happens test TX variable. Else goto 8
  6. If TX=="TX" then ROLLBACK WORK. Return error
  7. Else, if TX=="SVPOINT" then ROLLBACK WORK TO SAVEPOINT 'MYSVPOINT'. Return error
  8. Return success
After this introduction, I hope to be able to write a few more articles related to this topic. The basic idea is that sometimes it's very easy to extend the functionality of Informix. And I feel that many customers don't take advantage of this.
Naturally, there are implications on writing C UDRs. The example above is terribly simple, and it will not harm the engine. But when you're writing code that will be run by the engine a lot of questions pop up.... Memory usage, memory leaks, security stability... But there are answers to this concerns. Hopefully some of them (problems and solutions) will be covered in future articles.


Versão Portuguesa:

Introdução

Acabei de verificar.... Este será o centésimo artigo!!! Nunca estive tão activo no blog... Temos a versão Panther (11.7) (cheia de funcionalidades sobre as quais ainda não escrevi), fiz algum trabalho com tarefas do OAT que quero partilhar, e para além disso tenho andado a testar coisas novas... Sim... Apesar de já trabalhar com Informix há perto de 20 anos (é assustador dizer isto, mas é verdade...) há áreas de funcionalidade com as quais não lido habitualmente. Diria que aquela sobre a qual vou debruçar-me hoje não é usada pela maioria dos utilizadores. E isso é uma pena porque:
  1. Permite resolver problemas que não têm outra solução
  2. Se fosse mais usada seria melhorada mais rapidamente
Adicionalmente, existe muita gente que pensa que isto foi o que marcou o declínio da empresa Informix. Possivelmente já percebeu que estou a falar da extensibilidade. Para relembrar um pouco de história, em 1995, a Informix tinha a arquitectura DSA (Dynamic Scalable Architecture) na versão 7. E adquiriu a empresa Illustra, fundada por Michael Stonebraker e outros. O senhor Stonebraker já tinha um longo passado de inovação (que prossegue ainda actualmente) e permaneceu na Informix durante alguns anos. Toda a tecnologia à volta dos datablades e extensibilidade teve aí a sua origem.... Os críticos da Informix dizem que a companhia ficou tão absorvida pelas funcionalidades de extensibilidade (que acreditava serem a próxima "vaga") que perdeu o foco do mercado. A verdade é que a extensibilidade nunca se tornou em algo generalizado nem no Informix nem em outras bases de dados, e todas elas seguiram o lançamento do Informix Universal Server (1996): Oracle, IBM DB2 etc.

Mas este artigo não irá focar todo o conceito de extensibilidade. Seria impossível e entediante tentar cobrir tudo isso num artigo de blog. Em vez disso vou apenas introduzir um dos seus aspectos: User Defined Routines (UDRs), e em particular rotinas escritas usando linguagem C.

Existe um manual que cobre os UDRs, e eu recomendo vivamente a sua leitura. Mas aqui seguirei outra abordagem: Começarei com um problema muito simples, que sem um UDR em C seria impossível de resolver, definirei uma solução para o mesmo, e prosseguirei até à implementação e utilização da solução com um exemplo.

O problema

Alguma vez esteve numa situação em que estivesse a escrever um procedimento em SPL, e quisesse colocar parte dele dentro de uma transacção, mas tivesse receio que o código que chama o procedimento já estivesse com uma transacção aberta?

Poderia contornar o problema iniciando uma transacção e apanhando o erro (already in transaction) com um bloco de ON EXCEPTION

Mas isto teria outras implicações (os blocos de ON EXCEPTION podem criar problemas se o procedimento for chamado de um trigger). Portanto seria bom poder verificar se a sessão já está no meio de uma transacção. Uma forma natural de o fazer seria recorrer à função DBINFO(...), mas infelizmente as versões actuais (até à 11.7.xC1) não permitem isso. Ou seja, não há nenhum parâmetro desta função que permita obter a informação que necessitamos.

Pesquisa da solução

Uma parte importante da extensibilidade no Informix é a chamada Datable API. É um conjunto de interfaces programáveis que podemos usar dentro de datablades e também dentro de UDRs em C. O infocenter tem um manual de referência das funções do Datablade API. Uma pesquisa rápida por "transaction" faz aparecer uma função: mi_transaction_state()

A documentação indica que quando a chamamos (não requer parâmetros) irá retornar um valor do tipo mi_integer (equivale a um inteiro) com um destes valores:
  • MI_NO_XACT
    significa que não temos uma transacção aberta
  • MI_IMPLICIT_XACT
    significa que temos uma transacção implícita aberta (por exemplo se estivermos conectados a uma base de dados em modo ANSI)
  • MI_EXPLICIT_XACT
    significa que temos uma transacção explícita aberta
Isto é tudo o que necessitamos conceptualmente.... Agora precisamos de transformar uma ideia em código executável!

Começando o código

Para implementarmos um UDR em C necessitamos de efectuar vários passos:
  1. Criar o esqueleto do código C
  2. Criar a função com código C usando o datablade API
  3. Criar um makefile que tenha todas as instruções necessárias para gerar o código executável num formato que possa ser usado pelo motor
  4. Compilar o código
  5. Usar SQL para definir uma nova função, indicando ao motor onde pode encontrar a função, o interface para a chamar bem como a linguagem usada e outros atributos da função
  6. Testar!

Criar o código em C

O Informix fornece uma ferramenta chamada DataBlade Developers Kit (DBDK) que incluí vários componentes: Blade Manager, Blade Pack e Bladesmith. O Blade Manager permite-nos registar datablades em bases de dados, o Blade Pack faz o "empacotamento" de todos os ficheiros de um datablade (bibliotecas executáveis, ficheiros de documentação, ficheiros SQL, etc.). Finalmente o Bladesmith ajuda-nos a criar vários componentes e código fonte. É uma ferramenta de desenvolvimento que apenas corre em Windows mas que pode ser usado para preparar ficheiros para Unix/Linux. Para projectos complexos será boa ideia usar o Bladesmith mas para este exemplo simples farei tudo à mão. Apenas estou a criar um UDR em C. Estas ferramentas destinam-se a lidar com projectos muito mais complexos. Um Datablade pode incluir novos tipos de dados, várias funções etc.
Assim, para o nosso exemplo dei uma espreitadela ao recente Informix Developer's Handbook para copiar alguns exemplos.

Depois de ver os exemplos referidos, foi fácil criar o código em C:
/*
This simple function returns an integer to the calling SQL code with the following meaning:
0 - We're not within a transaction
1 - We're inside an implicit transaction
2 - We're inside an explicit (BEGIN WORK...) transaction
-1 - Something unexpected happened!
*/

#include <milib.h>

mi_integer get_transaction_state_c( MI_FPARAM *fp)
{
mi_integer i,ret;
i=mi_transaction_state();
switch(i)
{
case MI_NO_XACT:
ret=0;
break;
case MI_IMPLICIT_XACT:
ret=1;
break;
case MI_EXPLICIT_XACT:
ret=2;
break;
default:
ret=-1;
}
return (ret);
}

Coloquei o código acima num ficheiro chamado get_transaction_state_c.c


Criar o makefile

Também para o makefile, limitei-me a copiar alguns exemplos e gerei o seguinte. Por favor considere isto apenas como um exemplo. Não sou especialista em construção de makefiles e isto é apenas um pequeno projecto.


include $(INFORMIXDIR)/incl/dbdk/makeinc.linux

MI_INCL = $(INFORMIXDIR)/incl
CFLAGS = -DMI_SERVBUILD $(CC_PIC) -I$(MI_INCL)/public $(COPTS)
LINKFLAGS = $(SHLIBLFLAG) $(SYMFLAG)


all: get_transaction_state.so

clean:
rm -f get_transaction_state.so get_transaction_state_c.o


get_transaction_state_c.o: get_transaction_state_c.c
$(CC) $(CFLAGS) -o $@ -c $?

get_transaction_state.so: get_transaction_state_c.o
$(SHLIBLOD) $(LINKFLAGS) -o $@ $?


Este makefile destina-se ao GNU Make. A primeira linha incluí um makefile fornecido pela IBM com o Informix. Este, basicamente, contém definições de variáveis e macros. Deve adaptar a directiva include ao seu sistema (o nome do makefile pode variar com a plataforma) e garanta que as variáveis que usei estão definidas no makefile base do seu sistema.
Depois disso defini mais algumas variáveis e criei os targets. Apenas quero que gere a biblioteca dinâmica get_transaction_state.so e para isso estou a incluir o objecto (get_transaction_state_c.o) gerado a partir do meu código fonte (get_transaction_state_c.c). Bastante simples se tiver conhecimentos básicos de makefiles.


Compilar o código

Depois de termos o makefile apenas necessitamos de um comando simples para executar a compilação:


cheetah@pacman.onlinedomus.net:informix-> make
cc -DMI_SERVBUILD -fpic -I/usr/informix/srvr1150uc7/incl/public -g -o get_transaction_state_c.o -c get_transaction_state_c.c
gcc -shared -Bsymbolic -o get_transaction_state.so get_transaction_state_c.o
cheetah@pacman.onlinedomus.net:informix->
Os dois comandos executados são a tradução dos macros/variáveis definidos no(s) makefiles(s), e apenas compilam o código fonte (primeiro comando) e depois geram a biblioteca dinâmica. Se tudo correr bem (como naturalmente aconteceu no output acima), teremos a biblioteca nesta localização, pronta a ser usada pelo Informix:
cheetah@pacman.onlinedomus.net:informix-> ls -lia *.so
267913 -rwxrwxr-x 1 informix informix 5639 Nov 23 22:06 get_transaction_state.so
cheetah@pacman.onlinedomus.net:informix-> file *.so
get_transaction_state.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (GNU/Linux), dynamically linked, not stripped
cheetah@pacman.onlinedomus.net:informix->

Usar SQL para definir a função

Agora que temos o código executável, na forma de uma biblioteca dinâmica, precisamos de instruir o Informix para usá-la. Para isso vamos criar uma nova função, dizendo ao motor que está implementada em linguagem C e qual a localização onde está guardada. Faremos isso com um script SQL simples:

cheetah@pacman.onlinedomus.net:informix-> ls *.sql
get_transaction_state_c.sql
cheetah@pacman.onlinedomus.net:informix-> cat get_transaction_state_c.sql
DROP FUNCTION get_transaction_state_c;

CREATE FUNCTION get_transaction_state_c () RETURNING INTEGER
EXTERNAL NAME '/home/informix/udr_tests/get_transaction_state/get_transaction_state.so'
LANGUAGE C;
cheetah@pacman.onlinedomus.net:informix->


Vamos executá-lo...:


cheetah@pacman.onlinedomus.net:informix-> dbaccess stores get_transaction_state_c.sql

Database selected.


674: Routine (get_transaction_state_c) can not be resolved.

111: ISAM error: no record found.
Error in line 1
Near character position 36

Routine created.


Database closed.

cheetah@pacman.onlinedomus.net:informix->


Repare que o erro -674 é expectável, dado que o meu SQL incluí a instrução DROP FUNCTION (e ela ainda não existe). Se estivesse a usar a versão 11.7 (devido a vários testes não a tenho operacional agora) podia ter usado a nova sintaxe "DROP IF EXISTS".

Portanto depois deste passo, devo ter uma função que pode ser chamada pela interface SQL com o nome get_transaction_state_c(). Não recebe argumentos e retorna um valor inteiro.


Testar!

Agora é tempo de a ver a trabalhar. Abri uma sessão na base de dados stores e fiz o seguinte:
  1. Corri a função. Retornou "0", o que significa que não havia transacção aberta
  2. Depois abri uma transacção e executei a função novamente. Retornou "2", o que significa que uma transacção explícita estava aberta
  3. Fechei a transacção e corri a função pela terceira vez. Como esperado retornou "0"
Aqui está o output:


cheetah@pacman.onlinedomus.net:informix-> dbaccess stores -

Database selected.

> EXECUTE FUNCTION get_transaction_state_c();


(expression)

0

1 row(s) retrieved.

> BEGIN WORK;

Started transaction.

> EXECUTE FUNCTION get_transaction_state_c();


(expression)

2

1 row(s) retrieved.

> ROLLBACK WORK;

Transaction rolled back.

> EXECUTE FUNCTION get_transaction_state_c();


(expression)

0

1 row(s) retrieved.

>

Não vimos o retorno "1". Isso acontece quando estamos dentro de uma transacção implícita. Esta situação pode ser vista se a função estiver a ser executada numa base de dados em modo ANSI. Para isso vou usar uma outra base de dados (stores_ansi), e naturalmente necessito de criar a função aqui (usando as instruções SQL anteriores). Depois repito mais ou menos os mesmos passos e o resultado é interessante:

cheetah@pacman.onlinedomus.net:informix-> dbaccess stores_ansi -

Database selected.

> EXECUTE FUNCTION get_transaction_state_c();


(expression)

0

1 row(s) retrieved.

> SELECT COUNT(*) FROM systables;


(count(*))

83

1 row(s) retrieved.

> EXECUTE FUNCTION get_transaction_state_c();


(expression)

1

1 row(s) retrieved.

>
Se reparar, a primeira execução retornou "0". Como ainda não tinha efectuado nenhuma operação não havia transacção aberta. Mas logo a seguir a um simples SELECT já retorna "1", o que significa que uma transacção implícita estava aberta. Isto prende-se com a natureza e comportamento das bases de dados em modo ANSI.
Se as usa e pretende usar esta função terá de ter isto em consideração. Ou poderá simplesmente mapear o retorno "1" e "2" da função mi_transaction_state() no retorno "1" da função criada por si. Isto sinalizaria que uma transacção estava aberta (omitindo a distinção entre transacção implícita e explícita).

Considerações finais

Relembro que este artigo serve mais como introdução aos UDRs na linguagem C que propriamente para resolver um problema real.
Se necessitar de saber se já está numa transacção (dentro de uma stored procedure por exemplo) pode usar esta solução, mas também podia tentar abrir uma transacção e capturar e gerir o erro dentro de um bloco ON EXCEPTION.

Chamo a atenção também para que se isto é um problema real das suas aplicações, mesmo que esteja já dentro de uma transacção, pode fazer com que o código da sua stored procedure trabalhe como uma unidade, usando a funcionalidade dos SAVEPOINTs, introduzida na versão 11.50. Em pseudo-código seria feito assim:

  1. Chamar a get_transaction_state_c()
  2. Se estamos dentro de uma transacção então estabelecer TX="SVPOINT" e criar um savepoint chamado "MYSVPOINT". Ir para 4)
  3. Se não estamos dentro de uma transacção então estabelecer TX="TX" e criar uma. Ir para 4)
  4. Correr o código da procedure
  5. Se ocorrer algum erro testat a variável TX. Ir para 8)
  6. Se TX =="TX" então ROLLBACK WORK. Retornar erro.
  7. Senão, SE TX=="SVPOINT" então ROLLBACK WORK TO SAVEPOINT 'MYSVPOINT'. Retornar erro
  8. Retornar sucesso
Depois desta introdução espero conseguir escrever mais alguns artigos relacionados com este tópico. A ideia base é que algumas vezes é muito fácil estender a funcionalidade base do Informix. E sinto que muitos clientes não tiram partido disto.
Naturalmente há implicações em escrever UDRs em C. O exemplo acima é terrivelmente simples, e não trará prejuízo ao motor. Mas quando escrevemos código que será executado pelo motor surgem uma série de questões... Utilização de memória, fugas de memória, segurança, estabilidade.... Mas existem respostas para estas preocupações. Espero que alguns (problema e soluções) sejam cobertos em artigos futuros.