Wednesday, March 7, 2012

Too little, too late? / Muito pouco, muito tarde?

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


English Version:

I recently got an advice on how to make better use of Twitter... And so I did... I increased the number of people or accounts I'm following and today I was flooded by messages about the launch of one of the database competitors. If you've been paying attention to the net you probably know which one I'm talking about... I've seen some references before and I decided to investigate a little further what were the new features causing all this buzz around it... I must grant credit to the company behind it, since it was not difficult to get information and find several articles and papers and people talking about it.

I think that in the IT field we tend to close ourselves around what we know better. I've seen it in Oracle DBAs, in people working with Informix (you should know it's much harder to find an Informix DBA than a DBA from any other database since we tend to have several hats and play several roles) and with people working in different environments (z/OS is a classical example). And apparently it also happens with people working with SQL Server. I'd say that only this can explain all this enthusiasm... Let me explain why, by picking two of the flagship features of it's new version (I'll be using the terms I've found on the Internet in blogs, articles and so on):
  • AlwaysOn
    Believe it or not this is a form of replication that allows databases to be put together in groups that use a primary server and one or more secondary servers. The replication can be synchronous, or asynchronous. The secondary servers can accept read only queries. It includes some sort of connection redirection and something I could not exactly understand that allows temporary statistics to be computed on the secondaries and stored in temporary spaces...
  • ColumnStore indexes
    This is interesting.... It combines several technologies like in-memory database, columnar storage and star model optimization. This allows incredible time savings, but has some drawbacks, like not being able to update a table with an index of this type (several workaround are mentioned, but all of them have serious implications). It's up to the optimizer to decide if it will use this kind of index or the traditional query plans.
I'm sure that if you're an Informix user, or someone paying attention to the Informix scene, you have a smile on your face by now... And I would not need to explain why. But for the people who are a bit more distracted, or as I mentioned above live on a closed world, let me explain why Informix people have a smile on their faces at this point in the article:

In 2007 (yes, five years ago), IBM introduced version 11.1, code named Chetah, and one of the features was something called MACH-11. This did not cause half the buzz that we're seeing today, but in very short words, it was the ability to configure a set of Informix instances (where we can have several databases) with a primary server, a "close" secondary server, called HDR (which can by synchronous), and several remote secondary servers (RSS) which receive the logs. The communication between these can be encrypted (there are known customer cases using the Internet to ship their logical logs to the other side of the world). Note that the single node HDR existed since version 7 (can't remember the year, because it was before I joined Informix). The HDR was always readable. The RSS are naturally also readable. In 2009 (if memory serves me well, yes, 3 years ago) IBM introduced version 11.5, called Panther where MACH-11 was extended: We can now configure the secondary servers to accept write instructions which are transparently sent to the primary server. A new piece of software was introduced, called the Connection Manager, that can redirect clients based on pre-defined criteria (or SLAs) which are mapped to the several kinds of servers (or to specific server names). Finally, the statistics gathered on the primary are automatically available on the secondary servers, so there is no need to re-calculate them on the secondary servers. All this works in shared nothing architecture which allows for big geographic dispersion and naturally allows for disaster recover... Oh... and it's terribly simple to setup and use, and it's included even in the free product versions (with some restrictions). And I could continue, writing that in 2010 IBM launched version 11.7 which included the concept of flexible grid, ideal for truly distributed systems that need to dhare data.

Last year, (Q1) IBM announced the addition of new technology to the Informix product line, which targeted BI environments with a star schema design that needed very fast response times for analytical queries. This has the name of Informix Warehouse Accelerator, and combines several technologies: in-memory database, columnar storage and full cluster capabilities (for horizontal scaling). It duplicates the data into the accelerator memory and the Informix optimizer redirects the queries that can be accelerated and that would benefit from that acceleration. You can keep using the base tables as usual, you don't have to change the application layer, and you can control through SQL if you want to use the most recent data (stored in the traditional row storage) (slower) or the possibly older data stored in the accelerator (faster). Note that this creates full star (fact tables and dimensions) data duplication for full power acceleration.

So, although as expectable there are a few technical differences that can make us prefer one or the other, the biggest difference between some of these features seems to be the year they were launched... And yes, the buzz generated around them... and that with Informix you have more platform choices. At times like this I tend to agree with people that complain about IBM (they used to complain about Informix) marketing... Maybe, just maybe, IBM should concentrate on marketing instead of product development... That seems to be what others do with apparently good results... But again, if you're an Informix user, I'm sure you prefer the opposite.



Versão Portuguesa:


Recentemente fui aconselhado sobre como tirar mais partido to Twitter... E assim fiz... Aumentei o número de pessoas ou contas que sigo, e hoje fui inundado de mensagens sobre o lançamento de um dos fornecedores de base de dados concorrentes. Se tem prestado atenção à Internet deve saber a qual me estou a referir... Já tinha visto algumas referências antes e decidi investigar um pouco mais quais eram as novas funcionalidades que causavam toda esta movimentação... Tenho de reconhecer e dar crédito à empresa por detrás do produto, pois não foi difícil obter informação e encontrar vários artigos, papers e pessoas a escrever sobre o assunto.


Tenho para mim que as pessoas na área das TIs tendem a fechar-se sobre aquilo que conhecem melhor. Tenho visto isto em DBAs Oracle, em pessoas que trabalham com Informix (deverá saber que é muito mais difícil encontrar um DBA Informix que um DBA de qualquer outra base de dados, dado que tendemos a usar vários chapéus e desempenhar várias funções) e também em pessoas que trabalham em diferentes ambientes (o mundo dos mainframes é um exemplo clássico). E aparentemente isto também acontece com pessoas que trabalham com SQL Server. Diria que essa é a única explicação para tamanho entusiasmo... Deixem-me explicar porquê, pegando em duas das funcionalidades mais badaladas por esses blogs e twits que se referem a este lançamento (vou usar os termos originais em Inglês, que encontrei na Internet):

  • AlwaysOn
    Acredite-se ou não, parece ser uma forma de replicação que permite colocar várias bases de dados em grupos que usam um servidor principal e um ou mais servidores secundários. A replicação pode ser síncrona ou assíncrona. Os servidores secundários podem aceitar queries só de leitura. Incluí algum tipo de redirecionamento de conexões e algo que não consegui compreender totalmente mas que permite que sejam calculadas estatísticas nos nós secundários que são guardadas em espaços temporários.
  • ColumnStore indexes
    Isto é interessante... Combina várias tecnologias, como base de dados em memória, armazenamento em colunas e optimização de modelos em estrela. Isto permite enormes poupanças de tempo, mas traz algumas desvantagens, como não permitir alterações (INSERTs, UPDATEs, DELETEs) em tabelas com este tipo de índices (são apresentadas várias formas de contornar a limitação, mas todas com sérias implicações). Cabe ao optimizador decidir se deve usar este tipo de índice ou usar os planos de execução tradicionais.
Tenho a certeza que se é um utilizador de Informix, ou se tem estado atento ao mundo Informix, deverá ter um sorriso na cara por esta altura... E não necessitaria de explicar porquê, podendo encerrar já aqui este artigo. Mas para as pessoas mais distraídas, ou como disse acima, para quem viva num mundo mais fechado, vou explicar porque é que os utilizadores de Informix estarão a sorrir neste momento.

  Em 2007 (sim, há cinco anos atrás), a IBM lançou a versão 11.1, com o nome de código Cheetah, e uma das suas funcionalidades foi o MACH-11. Isto não causou metade do alarido de hoje, mas em breves palavras, é a capacidade de configurar um conjunto de instâncias Informix (onde podemos ter várias bases de dados) com um servidor primário, um servidor secundário "próximo", chamado HDR (que pode ser síncrono) e vários servidores secundários remotos (RSS) que recebem os logs. A comunicação entre os servidores pode ser encriptada (há casos conhecidos de clientes que usam a Internet para enviar os logical logs para o outro lado do mundo). Note-se que o nó secundário único (HDR) existia desde a versão 7 (não sei o ano, pois foi antes de entrar na Informix). O HDR sempre disponibilizou acesso para leitura. Os RSS incluíram desde o início a capacidade de aceitar queries para leitura. Em 2009 (se a memória não me falha, sim, há três anos atrás) a IBM introduziu a versão 11.50, chamada Panther onde o MACH-11 foi melhorado. Podemos agora configurar os servidores secundários para aceitar instruções de escrita que são enviadas de forma transparente para o primário. Uma nova peça de software foi introduzida com o nome de Connection Manager, que permite redirecionar os clientes baseado em critérios pré-definidos (ou SLAs) que são mapeados nos vários tipos de servidores (ou em identificadores específicos de cada servidor). Finalmente as estatísticas calculadas no servidor primário estão automaticamente disponíveis nos servidores secundários, por isso não há necessidade de as re-calcular nos secundários. Tudo isto funcionam numa arquitetura shared nothing o que permite uma vasta dispersão geográfica e automaticamente disponibiliza disaster recovery. Ah... E é terrivelmente fácil de criar e usar, e vem incluído até nas versões gratuitas do produto (com algumas restrições). E poderia continuar, dizendo que em 2010 a IBM lançou a versão 11.7 que introduziu o conceito de flexibale grid, algo verdadeiramente pensado para ambientes distribuídos mas com necessidade de partilhar dados.



O ano passado (Q1) a IBM anunciou a adição de nova tecnologia á linha de produtos Informix, cujo alvo foi ambientes de BI com modelos em estrela que precisem de tempos de resposta muito rápidos em queries analíticas. Tem o nome de Informix Warehouse Accelerator e combina várias tecnologias: base de dados em memória, armazenamento de colunas, capacidade de trabalhar em cluster (para crescimento horizontal). Duplica os dados para a memória do acelerador e o optimizador do Informix redirecciona as queries que podem ser aceleradas (se daí vier proveito). Podemos continuar a fazer o uso normal das tabelas de base e não é necessário mexer na camada aplicacional, e podemos controlar através de SQL se queremos ver os dados mais recentes (guardados no formato de linha tradicional) (mais lento) ou a versão possivelmente mais antiga guardada no acelerador (muito mais rápido). Note-se que isto duplica completamente o modelo em estrela (tabela de factos e as dimensões) para atingir uma maior performance (toda a query é resolvida no acelerador).


Portanto, embora como seja expectável, existam algumas diferenças técnicas que nos façam gostar mais de uma abordagem ou da outra, a maior diferença entre algumas destas funcionalidades parece residir mais no ano em que apareceram... E sim, no ruído à volta... e no facto de em Informix podermos escolher a plataforma. Em alturas como esta sinto-me tentado a concordar com quem se queixa do marketing da IBM (já se queixavam da Informix). Talvez, quem sabe, a IBM se devesse concentrar mais no marketing do que no desenvolvimento dos produtos... Parece ser o que outros fazem e com aparentes bons resultado... Por outro lado, se é um utilizador Informix tenho a certeza que prefere o contrário.

Tuesday, March 6, 2012

Sessions for all / Sessões para todos

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

English version:

The session list for the 2012 IIUG user conference is available... And just a quick glimpse at it makes me even more sad for not being present. If you still have the chance to go, don't hesitate.
I was just looking at the session list to decide a few sessions to highlight, but I can't! Honestly... I could point a few that don't raise my interest, mainly because I have the idea I know enough about that particular topic. But the vast majority makes me recall my biggest problem when I attended the conference: decide between two or three simultaneous sessions which one should I attend...

So, as usual, this looks YAYOGVFM (Yet Another Year Of Great Value For Money) :)
All the hot topics are covered:
  • TimeSeries
  • DataWarehouse the the Informix Warehouse Accelerator
  • Security
  • Mobile apps
And of course, this year's bonus: It's not in Kansas City! :)
This was my attempt of a joke. Kansas City is a good place for a conference since it's next door with the Lenexa Labs. I just hope that the traveling won't prevent the I&D team members and product architects to be present on the conference.

So, in short, if you can don't miss it!

Versão Portuguesea:

A lista de sessões para a conferência de utilizadores de 2012 do IIUG já está disponível... E basta uma vista de olhos rápida para que lamente não poder estar presente. Se ainda tem hipótese de ir não hesite.
Estava a ver a lista de sessões para destacar algumas, mas não consigo. Sinceramente... Poderia indicar algumas que não me despertam muito interesse, essencialmente porque tenho a ideia que são tópicos sobre os quais sei o suficiente. Mas a grande maioria faz-me recordar o maior problema que tive quando assisti á conferência: Decidir a que sessão assistir das três ou quatro simultâneas que existiam...

Portanto, como vem sendo hábito, parece MUADEVPD (Mais Um Ano De Excelente Valor Pelo Dinheiro) :)
Todos os tópicos mais badalados estão cobertos:
  • Timeseries
  • Datawarehouse e o Informix Warehouse Accelerator
  • Segurança
  • Aplicações móveis
 E claro, há ainda o bónus deste ano: Não é em Kansas City! :)
Isto foi uma tentativa de piada. Kansas City é um bom sítio para uma conferência dado que fica perto do laboratório de Lenexa. Espero muito sinceramente que a necessidade de viajar não impeça os membros da equipa de desenvolvimento e os arquitetos do produto de estarem presentes na conferência.

Em resumo, se puder não perca esta oportunidade!



Thursday, February 16, 2012

4GL WHENEVER ERROR

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

English version:

I believe this is the first time I cover a 4GL topic here. But a recent customer situation motivated me to write this. Hopefully most of the readers will just say "Yeah... Everybody knows that...", but it's not the first time I see people making the confusion I'm going to describe, and in my opinion that happens because it's not intuitive. Although it's perfectly documented, I suppose many people just follow the intuitive approach and fall into the problem.

I'm talking about the WHENEVER statement. It is used to define the behavior of the program when(ever) a defined condition (ERROR, SQLERROR, WARNING, SQLWARNING or  NOTFOUND) happens. The behavior can be CONTINUE, STOP, GOTO or CALL function. Seems pretty simple and handy... so why am I writing this? The usual confusion relates to the scope of the WHENEVER statement. At first glance you could think this was a program instruction, and the effect or scope of it would be until the program flow reached another WHENEVER statement. And this is the confusion many people make.
In reality it's not a program instruction, but instead it's a compiler directive. As stated in the documentation the scope is local to the module where it appears. If the module contains only function definitions, then all this functions will behave accordingly to the conditions used in the WHENEVER statement. The program flow is irrelevant.
This is better shown with a practical example (line numbers added for clarity)
 1  DATABASE sysmaster
2 MAIN
3 DEFINE v INTEGER
4 CALL func_1() RETURNING v
5 END MAIN
6
7 FUNCTION func_0()
8 WHENEVER ERROR CONTINUE
9 END FUNCTION
10
11 FUNCTION func_1()
12 SELECT no_column FROM no_table
13 RETURN 1,2
14 END FUNCTION

Ok, this is a very simple 4GL program that uses sysmaster database. It has two functions:
  1. func_0
    Is never called, but contains a WHENEVER ERROR CONTINUE which tells 4GL to continue execution when it encounters an error
  2. func_1
    This contains two errors. It tries to access a table that doesn't exist and returns two values (and the CALL on line 4 expect only one)
If we compile and execute it we get:
cheetah@pacman.onlinedomus.net:fnunes-> c4gl -o test.4ge test.4gl;./test.4ge
Program stopped at "test.4gl", line number 4.
FORMS statement error number -1320.
A function has not returned the correct number of values
expected by the calling function.
cheetah@pacman.onlinedomus.net:fnunes->

What happened? The first error to expect would be the SQL error... But 4GL ignores it and raises an error on the CALL line. Why? Because from line 8 onward the code ignores errors. But the CALL on line 4 is not protected against error. If we comment the func_0 function, and repeat we get:
cheetah@pacman.onlinedomus.net:fnunes-> c4gl -o test.4ge test.4gl;./test.4ge
Program stopped at "test.4gl", line number 12.
SQL statement error number -206.
The specified table (no_table) is not in the database.
SYSTEM error number -111.
ISAM error: no record found.
cheetah@pacman.onlinedomus.net:fnunes->
This is the expected behavior, but note that we didn't change the program flow.

So, to be clear, WHENEVER ERROR acts at compile time, and means that from it's occurrence downwards, all the code within the module will be protected until another WHENEVER statement is reached. And it affects only the current module. It has nothing to do with the program execution map.

Just one last reminder: Never use WHENEVER without introducing code to test for errors (for CONTINUE condition). Otherwise your program may do unexpected things


Versão Portuguesa

Julgo que esta é a primeira vez que escrevo aqui de 4GL. A motivação deriva mais uma vez de uma situação vivida num cliente. Com sorte, muitos dos leitores dirão apenas "Sim... toda a gente sabe isso...", mas já não é a primeira vez que encontro a confusão que vou relatar, e na minha opinião tal acontece porque é algo pouco intuitivo. Apesar de estar claramente documentado, suponho que muitas pessoas apenas seguem o instinto e daí cairem no problema.

Estou a falar da instrução WHENEVER. É usada para definir o comportamento do programa sempre que uma determinada condição (ERROR, SQLERROR, WARNING, SQLWARNING ou  NOTFOUND) acontece. O comportamento pode ser CONTINUE, STOP, GOTO ou CALL de uma função. Parece muito simples e prático... Sendo assim porque estou a escrever sobre isto? A confusão habitual relaciona-se com o raio de acção da instrução. À primeira vista parece uma normal instrução de programa, e a sua acção extender-se-ia até que o fluxo de execução do programa encontrasse outro WHENEVER. E isto parece ser assumido por muita gente.
Mas na realidade não é uma instrução de programa, mas sim uma directiva de compilação ou compilador. Como é referido na documentação a sua influência restringe-se ao módulo onde é usada. Se um módulo só contém definições de funções, então essas funções comportar-se-ão conforme a instrução ditar. O fluxo de execução do programa é irrelevante.
É melhor mostrar isto com um exemplo (números de linhas adicionados por clareza):
 1  DATABASE sysmaster
2 MAIN
3 DEFINE v INTEGER
4 CALL func_1() RETURNING v
5 END MAIN
6
7 FUNCTION func_0()
8 WHENEVER ERROR CONTINUE
9 END FUNCTION
10
11 FUNCTION func_1()
12 SELECT uma_coluna FROM nao_existe
13 RETURN 1,2
14 END FUNCTION

Isto é um programa 4GL extremamente simples. Usa a base de dados sysmaster e contém duas funções:
  1. func_0
    Nunca é chamada, mas contém um WHENEVER ERROR CONTINUE que força o 4GL a continuar a execução caso encontre algum erro
  2. func_1
    Esta função contém dois erros. Tenta aceder a uma tabela que não existe e retorna dois valores (a chamada CALL na linha 4 apenas espera um)
Se compilarmos e executarmos obtemos isto:
cheetah@pacman.onlinedomus.net:fnunes-> c4gl -o teste.4ge teste.4gl;./teste.4ge
Program stopped at "teste.4gl", line number 4.
FORMS statement error number -1320.
A function has not returned the correct number of values
expected by the calling function.
cheetah@pacman.onlinedomus.net:fnunes->

O que aconteceu? O primeiro erro que esperávamos era o erro de SQL... Mas foi ignorado e o erro do CALL foi despoletado. Porquê? Porque da linha 8 em diante o código ignora os erros. Mas o CALL da linha 4 não está protegido. Se comentarmos a função func_0 e repetirmos obtemos:
cheetah@pacman.onlinedomus.net:fnunes-> c4gl -o teste.4ge teste.4gl;./teste.4ge
Program stopped at "teste.4gl", line number 12.
SQL statement error number -206.
The specified table (nao_existe) is not in the database.
SYSTEM error number -111.
ISAM error: no record found.
cheetah@pacman.onlinedomus.net:fnunes->
Isto é o esperado, mas repare-se que não mudámos o fluxo de execução do programa.

Portanto, clarificando, WHENEVER ERROR actua na fase de compilação, e significa que daí em diante, todo o código desse módulo, ficará protegido contra erros até ao aparecimento de outro WHENEVER. Não tem portanto nada que ver com o fluxo de execução do programa

Apenas uma nota final: Nunca utilize o WHENEVER (com a opção de CONTINUE) sem introduzir código que teste os erros. Caso contrário o seu programa pode fazer coisas inesperadas

Saturday, February 11, 2012

Informix on AIX patch levels

IBM has recently publish a document stating the OS levels required  and other known issues:

http://www-01.ibm.com/support/docview.wss?uid=swg21579767

If you're already running Informix on AIX, or are planning to, then you should took a quick look at it

Wednesday, February 8, 2012

Sweet CRM / Doce CRM

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

English Version:

A recent press release by Oninit is being echoed across the Internet. Oninit have completed the port of SugarCRM, an open-source CRM system to Informix.
This gives SugarCRM users the opportunity to use Informix as the underlying database to their CRM system, effectively taking advantage of all the features we all know and love (performance, scalability, high availability features, complete platform options, simplicity etc.).

But there is even more to this.. Accordingly to SugarCRM site there were already a number of points connnecting SugarCRM and IBM (Cognos and SugarCRM working together, Lotus integration, IBM systems etc.).
It's also important to note that from a cost reduction point of view, the free Informix versions or the ones with lower costs can be an excellent companion for SugarCRM.
You can start small... Informix will grow as your business.

This is another great news about integration of Informix with many products (MediaWiki, XWiki, iBatis, Hibernate, Drupal, Alfresco and others).
Congratulations to Oninit for another great job (following several TimeSeries activities)

Versão Portuguesa:

Um comunicado de imprensa recente, pela Oninit está a ter eco na Internet. A Oninit completou a adaptação para que o CRM open-source SugarCRM passe a trabalhar também com Informix.
Isto permite aos utilizadores de SugarCRM usar o Informixx como base de dados de suporte do seu sistema de CRM, aproveitando assim as funcionalidades que conhecemos e apreciamos (rapidez, capacidade de escalar, alta-disponibilidade, disponibilidade de várias plataformas, simplicidade etc.).


Mas há ainda mais sobre isto... Segundo o site do SugarCRM já existiam um número de pontos de contacto entr o SugarCRM e a IBM (ligação entre SugarCRM e Cognos, integração com Lotus. sistemas IBM etc.).
Há ainda que referir que numa óptica de poupança de custos, as versões gratuitas ou de menor custo do Informix podem ser uma excelente companhia para o SugarCRM.
Pode começar pequeno... O Informix acompanhará o crescimento do seu negócio.


Isto é mais uma excelente notícia sobre a integração de Informix com muitos outros produtos (MediaWiki, XWiki, iBatis, Hibernate, Drupal, Alfresco e outros).
Parabéns à Oninit pela excelente iniciativa (depois de várias atividades relacionadas com Informix Timeseries)

Sunday, February 5, 2012

Procedures / Procedimentos Owner vs Restricted

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

English version:

Introduction

This article focus on a little known aspect of stored procedures or functions. That probably explains why it was the less voted in a recent poll I've conducted. Nonetheless it's (from my point of view) a very interesting topic. During this article I'll be referring to procedures, but I could use the term functions.
If we take a look at the sysprocedures table we'll see a field called mode. This field is just one character and the values it can contain are:
  • D or d
    DBA
  • O or o
    Owner
  • P or p
    Protected
  • R or r
    Restricted
  • T or t
    Trigger
I'm not interested in all of these, but the lower case letters mean "protected" (created by the system), D is for DBA procedures. P is an old nomenclature for protected procedures. T is used for procedures defined as Trigger procedures. And then we have O and R. O for owner mode and R for restricted mode. What is the difference between them? Assume you're using informix user and you run:
CREATE PROCEDURE test()
END PROCEDURE
You'll have an OWNER mode procedure, owned by informix user. But if instead you run:
CREATE PROCEDURE myuser.test()
END PROCEDURE
You'll have a RESTRICTED mode procedure owned by myuser.
You need to have DBA privilege to create a procedure on behalfwith another user name.

Why RESTRICTED?

The reasons why the restricted mode procedures/functions were created are based on security. Let's imagine the following scenario:
  1. You have two databases called db1 and db2
  2. You have a user myuser with connect privileges on db1 and db2 and another user mydba with DBA privileges on db1
  3. User myuser needs to be connected to db1 and run a distributed query to db2
  4. The db2's DBA grants the required privileges on db2 to user myuser
Now, without the RESTRICTED mode procedures, mydba could create a local db1 procedure on behalf of myuser, and with that it could remotely access the data on db2. Note that the db2 DBA did not intend to give the privileges to anyone else beside myuser. So a local DBA could use it's privileges to abuse some of the remote privileges granted to some of the local users.
This is why the RESTRICTED mode was created. Every time we create a procedure on behalf of another user, it will be created as a RESTRICTED mode procedure. And as such any remote operation will be done using the currently logged user and not with the identity of the procedure owner (as it happens with OWNER mode procedures).

Other implications

So, the reasons for the creation of this new mode are explained and are good reasons. But there can be another implication. Note that I'll be referencing a product issue, but it's highly probable that you'd never notice it. But the fix for that bug introduced new limits and a new error so it can be interesting to dig a bit deeper on this.
Whenever we make a remote connection inside a statement we need to open a new database. And we need to keep a record of the current opened ones. The structure of the opened databases used to be an array of "only" 8 positions. And in certain conditions we could wrap around it without raising an error. And this could lead to a nasty situation where the "current" database was not the one it should be. I noticed this on a customer environment when we started to get error -674 (procedure not found) on a procedure called from a trigger. Why is this related to the restricted vs owner mode procedures? Because with the mixed use of restricted and owner mode procedures we raise the possibility of having the same database opened with different users (the owners and our current user).
Please don't be scared with this problem. The situation I got involved around 60 objects (tables and procedures) linked together by a complex sequence of triggers that called procedures, that made INSERTs/UPDATEs/DELETEs which in turn called other procedures etc..
This sequence was started by a simple INSERT. And it involved 5 databases. The array I mentioned earlier had 8 positions.
Since then, we fixed several things and now (11.50.xC9 and 11.70.xC3):
  1. The array was increased to 32 positions
  2. If we still achieve that limit a proper error will be raised (-26600)
  3. The documentation was improved (it didn't mention any limit and it still mentions 8, but it should be fixed soon)
Versão Portuguesa:

Introdução  

Este artigo foca um aspecto pouco conhecido das stored procedures (ou funções). O facto de ser desconhecido deve ajudar a explicar porque foi o menos votado para artigos num inquérito que realizei há pouco tempo. Apesar disso, é um assunto interessante (do meu ponto de vista). Durante este artigo irei referir na maior parte das vezes "procedimentos". Mas podemos assumir "funções".
Se dermos uma vista de olhos à tabela sysprocedures podemos reparar que contém uma coluna com o nome mode. É apenas um caracter e os valores que pode conter são:
  • D or d
    DBA
  • O or o
    Owner
  • P or p
    Protected
  • R or r
    Restricted
  • T or t
    Trigger
Não estou interessado nestes todos, mas para melhor entendimento, as letras minúsculas significam que o prodedimento (ou função) é "protegido" (criado pelo sistema). D é para procedimentos DBA. P é uma nomenclatura antiga para procedimentos protegidos. T é usado para procedimentos definidos como Trigger procedures. E depois temos os O e R. O para modo owner e R para modo restricted. Qual é a diferença entre ambos? Assuma que estamos a usar o utilizador informix e corremos:
CREATE PROCEDURE teste()
END PROCEDURE
Ficaremos com um procedimento em modo OWNER, cujo dono é o informix. Mas se em vez disso fizermos:
CREATE PROCEDURE myuser.teste()
END PROCEDURE
Ficaremos com um procedimento em modo RESTRICTED cujo dono é o myuser.
É necessário ter privilégios de DBA para criar procedimentos em nome de outro utilizador.

Porquê RESTRICTED?

As razões que levaram à criação do modo RESTRICTED para funções e procedimentos prendem-se com segurança. Vamos imaginar o seguinte cenário:
  1. Temos duas bases de dados chamadas bd1 e bd2
  2. Temos um utilizador myuser com privilégios de CONNECT em bd1 e bd2 e outro utilizador mydba com privilégios de DBA na bd1
  3. O utilizador myuser necessita de, estando conectado à bd1, correr uma query distribuída à bd2
  4. O DBA da bd2 faz o GRANT dos privilégios necessários na bd2 ao utilizador myuser
Ora, sem o modo RESTRICTED dos procedimentos, o utilizador mydba poderia criar um procedimento na bd1, em nome do utilizador myuser e nesse procedimento poderia aceder à bd2 usando a identidade do myuser (que tem privilégios na bd2). Note-se que o DBA da bd2 não tencionava dar os privilégios a mais ninguém que não o myuser. Portanto um DBA local poderia usar os seus privilégios para usufruir de privilégios remotos dados a utilizadores da sua base de dados.
Esta foi a razão que levou à criação deste novo modo. Em termos práticos, um procedimento criado como RESTRICTED executa todas as operações remotas com a identidade do utilizador que a está a executar e não com a identidade do utilizador que está definido como dono (que pode ser diferente de quem a criou).

Outras implicações

Portanto, as razões para a introdução deste novo modo estão apresentadas e são boas razões. Mas podem existir outras implicações. De seguida irei referir um bug do produto, mas é altamente improvável que venha a encontrá-lo. Mas a correcção introduziu algumas alterações que são dignas de nota e que valerão a pena gastar algum tempo com elas.
Cada vez que fazemos uma conexão remota, dentro de uma instrução SQL, temos de abrir a base de dados remota. E necessitamos de manter um registo das bases de dados abertas em cada momento. A estrutura que mantém essa informação era um array de "apenas" 8 posições. E em determinadas situações poderíamos "dar a volta" sem despoletar um erro apropriado. E isto poderia dar origem a uma situação onde a base de dados "actual" não era a que deveria ser (devido à forma como eram abertas e fechadas as ligações durante a execução de uma instrução SQL). Deparei-me com isto num ambiente de um cliente onde começamos a obter o erro -674 (procedure not found) num procedimento despoletado por um trigger. Como é que isto se relaciona com o tema deste artigo? Porque o uso misto de procedimentos em modo RESTRICTED e OWNER potencia um maior número de bases de dados abertas em simultâneo (cada conexão tem um utilizador específico associado que conforme o modo pode ser o dono dos procedimentos ou o utilizador da sessão).
Não fique assustado com este problema. Para melhor enquadrar, na situação que encontrei existiam cerca de 60 objectos (tabelas e procedimentos) ligados por uma complexa teia de triggers e procedimentos (triggers que chamavam procedimentos que fazia INSERTs, UPDATEs e DELETEs, que por sua vez faziam disparar outros triggers e assim sucessivamente).
A sequência era despoletada por um simples INSERT e envolvia 5 bases de dados distintas. O array mencionado anteriormente tinha apenas 8 posições.
Isto levou a várias correcções e agora (11.50.xC9 e 11.70.xC3):
  1. O array foi incrementado para 32 posições
  2. Se alguma vez atingirmos este limite (espero sinceramente que não) um erro apropriado será retornado (-26600)
  3. A documentação foi melhorada (não mencionada qualquer limite, sendo que de momento ainda refere 8... Deve ser corrigido brevemente)

Tuesday, January 31, 2012

UDRs: ROWNUM in Informix / ROWNUM em Informix


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

English version

ROWNUM again?!

This is more or less a FAQ. If you search the Internet for "rownum informix" you'll get a lot of links and several possible answers. I don't plan to give you a final answer, but I'll take advantage of this frequent topic to go back to something I do enjoy: User Defined functions in C language.
Most of the questions regarding ROWNUM appear in the form "does Informix support XPTO database system's ROWNUM", or "does Informix support ROW_NUMBER like database XPTO?" or even "does Informix allow the retrieval of the top N rows?" So, if you ever need something that resembles ROWNUM, the first thing you should do it to establish a clear understanding of what you need. Because the above three questions can represent three different needs, and for each need there may be a different answer. Let's see:
  1. Does Informix support ROWNUM?
    Quick answer would be no. But usually ROWNUM referes to an Oracle "magic" column that's added to the result set and that represents the position of the row in the result set. Note that this number is associated before doing ORDER BY and other clauses. So the result may not be very intuitive. Many times the purpose of using it is just to restrict the number of rows returned. Something that in ANSI (2008) SQL would be done by using the FETCH FIRST n ROWS clause. And this can be done in Informix very simply by puting a "FIRST n" before the select list:

    SELECT FIRST n * FROM customer

    But if you want to associate an incremental number to each row we can implement other solutions.

  2. Does Informix allow the retrieval of the top N rows?
    Yes. I just showed how to do it in the previous section. Just use the FIRST n clause. Also note that you can also use the SKIP n clause. This options are applied after all the other clauses in the SELECT. So you could use them for pagination (although it would require running the same query several times, which is not efficient).

  3. Does Informix support row_number()?
    No. And there's not much we could do. The row_number clause or function is a complex construct that can associate an incremental number to a result set, but with very flexible options that allows the numbering to restart on specific conditions etc. In order to achieve this we would need to be able to change how the query is solved. And we can't. But read on...
So, the initial purpose of this article is to explain how we can create a simple function that will generate an incremental number for each row of the result set. Sometimes this can be useful, and you'll possibly be amazed by how simple it is. There are a few challenges though... And you must be aware of how it works, or the results may surprise you.

The implementation in C

You probably heard that we can create functions in C, Java and SPL (Stored Procedure Language), but most of us only used SPL. Informix extensibility through the user defined routines (UDR) is one of it's greatest strengths, but unfortunately it's also one of the least used features. This is unfortunate not only because we're wasting a lot of potential, but also because a greater usage would probably lead to greater improvement. I'm taking this opportunity to show you how simple it can be to create a function.
In order to do it, we must follow some rules, and we should know a bit about the available API. A good place to start understanding how we can create UDRs is the User's Defined Routines and Datatypes Developer's Guide. This explains the generic concepts and the kind of UDRs we can create. Then we can check the Datablade API Programer's Guide. This has a more technical description of several aspects (like memory management, processing input and output etc.). Finally we have the Datablade API Function Reference, for specific function help and description. But of course.... reading all this without practice is more or less useless. We could use a list of examples to get us going...

So, what I propose is to create a very simple C function that can be embedded in the engine. It's use is as easy as if it was a native function. And it's creation is really simple. The language used will be C. SPL is less flexible (although it can be very handy, useful and quick). Java doesn't have easy access to the internal API, but can eventually be even more flexible for certain tasks, although a bit more complex and slower. But it really depends on your background and needs.

Before we start I should mention a few points which are in fact the hardest part of the process:
  1. IBM bundles a few scripts with the engine that are necessary to get us started. Inside $INFORMIXDIR/incl/dbdk there are a few scripts that are simple makefiles. We may need to adapt these to the platform or compiler we're using. In my system I have:
    • makeinc.gen
    • This is a generic cross platform makefile used by the next one
    • makeinc.linux
    • This is a makefile specific for your platform which includes the previous one
    • makeinc.rules
    • This is a makefile containing basic compilation rules These scripts can and should be used by your own makefile
  2. The function code needs to include some files and follow several rules.
  3. After you create the function code, we need to compile it to object code and generate a dynamic loadable library. This is the way we make it available for the engine
  4. After installing the library we have to create the function, telling the engine where it is available
Let's start by creating the code. As this is a crash course, I'll try to keep explanations to a minimum...:
1    /*
2 ------------------------------------------
3 include section
4 ------------------------------------------
5 */
6 #include <stdio.h>
7 #include <milib.h>
8 #include <sqlhdr.h>
9 #include <value.h>
10
11 mi_integer ix_rownum( MI_FPARAM *fp)
12 {
13 mi_integer *my_udr_state, ret;
14
15 /*
16 ----------------------------------------------------------
17 check to see if we've been called before on this statement
18 ----------------------------------------------------------
19 */
20 my_udr_state = (mi_integer *) mi_fp_funcstate(fp);
21 if ( my_udr_state == NULL )
22 {
23 // No... we haven't... Let's create the persistent structure
24 my_udr_state = (mi_integer *)mi_dalloc(sizeof(mi_integer),PER_STMT_EXEC);
25 if ( my_udr_state == (mi_integer *) NULL)
26 {
27 ret = mi_db_error_raise (NULL, MI_EXCEPTION, "Error in ix_rownum: Out of memory");
28 return(ret);
29 }
30 // We created it, so let's register it and initialize it to 1
31 mi_fp_setfuncstate(fp, (void *)my_udr_state);
32 (*my_udr_state)=1;
33 }
34 else
35 {
36 // If it's not the first time, then just increment the counter...
37 (*my_udr_state)++;
38 }
39 // return the counter...
40 return(*my_udr_state);
41 }
The important points are:
  • Lines 1-10 are just the normal and required includes
  • Line 11 is the function header. We define it as returning an mi_integer (on this functions we should use the mi_* datatypes). We accept one parameter which is a pointer to a function context
  • Line 13 where we define auxiliary variables
  • Line 20, we try to retrieve the previous value we kept stored in a persistent memory area. For that we use a datablade API function called mi_fp_funcstate
  • Lines 21-30, if the previous call returned a NULL pointer we try to allocate (mi_dalloc) memory for keeping the counter. This may be one of the most important steps. We define that the persistence criteria is PER_STMT_EXEC. This means we're keeping the context only while we're executing the same statement. We test the result and raise an error if the allocation fails
  • Line 31 we register the memory we have allocated as the function automatic parameter by calling mi_fp_setfuncstate()
  • Line 32 is the counter initialization. On the first call we define it as 1
  • Line 37 is the just the case for all the calls except the first. And in the generic case we just increment the counter
  • Line 40 is just the return of the value after initializing or incrementing it
Except for some strange but powerful functions, the code is trivial. But how do we make it available to the SQL layer? First we need to compile it and generate the dinamic library. For that I created a simple makefile:
include $(INFORMIXDIR)/incl/dbdk/makeinc.linux

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

all: ix_rownum

clean:
rm *.udr *.o

ix_rownum: ix_rownum.udr
@echo "Library genaration done"

ix_rownum.o: ix_rownum.c
@echo "Compiling..."
$(CC) $(CFLAGS) -o $@ -c $?

ix_rownum.udr: ix_rownum.o
@echo "Creating the library..."
$(SHLIBLOD) $(LINKFLAGS) -o $@ $?

Note the inclusion of the Linux makefile I mentioned earlier. If everything goes well, after I run make I will have a dynamic loadable library called ix_rownum.udr
panther@pacman.onlinedomus.com:informix-> make
Compiling...
cc -DMI_SERVBUILD -fpic -I/usr/informix/srvr1170uc4/incl/public -g -o ix_rownum.o -c ix_rownum.c
Creating the library...
gcc -shared -Bsymbolic -o ix_rownum.udr ix_rownum.o
Library genaration done
panther@pacman.onlinedomus.com:informix->
Having done this we need to create the funcion using SQL, as an external function. The syntax can be:
CREATE FUNCTION rownum() RETURNING INTEGER
WITH (VARIANT)
EXTERNAL NAME '/home/informix/udr_tests/ix_rownum.udr(ix_rownum)'
LANGUAGE C;
We're telling the engine to create a function called rownum, which does not receive any parameter and returns an INTEGER. We specify that it's written in C and the location. Note that I'm giving it the full dynamic library path (/home/informix/udr_tests/ix_rownum.udr) and the function name inside that library (a single library can contain more than one function). And I left the explanation for "WITH(VARIANT)" for last... The external function creation allows us to specify several properties for the functions. This one, VARIANT, tells the engine that the function may return different values when called with the same parameters. This is critical since we're not passing any parameters. If we told the engine it was NOT VARIANT it would only call it once. After that it would assume the return value was 1. This is an optimization, but in our case we don't want it, since it would break the function logic. VARIANT is the default and I just include it for clarity. You can find more about the function properties here.

Working with it

Well, after the above we are able to use ROWNUM() in SQL. Let's see a few examples:
-- Example 1:
SELECT customer_num, rownum() row_num
FROM customer;
customer_num row_num
101 1
102 2
103 3
104 4
105 5
106 6
107 7
108 8
109 9
[...]
-- Example 2
SELECT customer_num, rownum()
FROM customer
WHERE rownum() < 5;
customer_num row_num

101 1
102 2
103 3
104 4

-- Example 3
SELECT
FIRST 4
customer_num, lname
FROM
customer
ORDER BY lname DESC;

customer_num lname

106 Watson
121 Wallack
105 Vector
117 Sipes

-- Example 4
SELECT
customer_num , lname, rownum() row_num
FROM
customer
WHERE rownum() < 5
ORDER BY lname DESC;

customer_num lname row_num

102 Sadler 2
101 Pauli 1
104 Higgins 4
103 Currie 3

-- Example 5
SELECT
t1.*, rownum() row_num
FROM (SELECT customer_num, lname FROM customer ORDER BY lname DESC) as t1
WHERE rownum() <5;

customer_num lname row_num

106 Watson 1
121 Wallack 2
105 Vector 3
117 Sipes 4

Let's comment the above examples. There are very important aspects to consider.

  • Example 1
    This is the simplest example. And works as expected
  • Example 2
    Here we are using ROWNUM() also as a filter for the WHERE clause.
  • Example 3
    This is a auxiliary example to show a possible problem. It's just a select of customer_num and lname ordered by this in a decremental order.
  • Example 4
    Here we're trying the same thing, but using ROWNUM() to limit the number of rows. Note that this alters the result set. Why? Because ROWNUM() is applied immediately on the full table scan. The first 4 rows are retrieved and then the ORDER BY is applied. So using ROWNUM changes the the result set, because it's applied (in the WHERE clause) before the ORDER BY.
  • Example 5
    If we wanted to reproduce the result set from example 3, but still add a row number we could use the syntax presented here
I mentioned above that we could not reproduce the functionality of the ROW_NUMBER() construct of the SQL standard. This allows us to specify an ORDER BY clause and a PARTITION BY clause.
The ORDER BY inside the ROW_NUMBER() tells the database to order the sequence numbers by the specified filed(s). The PARTITION BY tells it to "restart" the count each time the field specified changes (similar to the effect of GROUP BY and aggregate functions). Note that this ORDER BY does not influence the order of the result set.
If you're wondering if we could implement the same functionality using functions, the answer is "sort of..." But I'll leave that for another article.


Versão Portuguesa

ROWNUM outra vez?!

Isto é uma questão que pode fazer parte dos FAQs. Se pesquisar na Internet por "rownum informix" vai obter uma série de links e algumas possíveis respostas. Não espero dar uma resposta definitiva, mas vou aproveitar este tema para voltar a um assunto que me agrada bastante: Funções definidas pelo utilizador em C.
Muitas das questões em torno do ROWNUM parecem numa das formas "o Informix suporta o ROWNUM tal como a base de dados XPTO?", ou "o Informix suporta o ROW_NUM como a base de dados XPTO?" ou ainda "o Informix suporta obter apenas as N primeiras linhas de uma query?". Assim se as suas necessidades parecem ir ao encontro do ROWNUM a primeira coisa a fazer é perceber exactamente o que se pretende. Necessidades diferentes podem ter soluções diferentes. Vamos ver:

  1. O Informix suporta o ROWNUM?
    A resposta rápida seria não. Mas habitualmente a referência a ROWNUM diz respeita a uma coluna "mágica" do Oracle, que é adicionada ao conjunto de resultados e que representa a posição de cada linha nesse mesmo conjunto. Note que este número é adicionado antes do processamento do ORDER BY e outras cláusulas e isso pode tornar o resultado pouco intuitivo. Muitas vezes é usado apenas para limitar o número de linhas obtido. Algo que em ANSI (2008) SQL seria feito com a cláusula FETCH FIRST n ROWS. E isto pode ser feito em Informix muito simplesmente com um FIRST n antes da lista de colunas:

    SELECT FIRST n * FROM customer

    Mas se o que pretende é associar um número incremental a cada linha podemos implementar outras soluções.

  2.  O Informix suporta obter apenas as primeiras N linhas?
    Sim. Mostrei como no parágrafo anterior. Basta usar a cláusula FIRST N. Note-se que podemos também usar a cláusula SKIP n. Estas opções são aplicadas após todas as outras cláusulas do SELECT, nomeadamente o ORDER BY. Podem portanto ser usadas para paginação de resultados, embora isso leve à execução da mesma query várias vezes, o que não será muito eficiente

  3. O Informix suporta ROW_NUMBER?
    Não. E não há muito que possamos fazer. A cláusula ROW_NUMBER é complexa. Permite associar um sequência incremental de valores a um conjunto de resultados, mas com opções muito flexíveis que permitem ordenar a sequência segundo um critério e recomeçar do valor 1 sempre que certas colunas mudam. Para conseguir fazer isto teríamos de conseguir controlar a forma como o motor resolve as queries. E tal não é possível... Mas já vamos ver o que se pode fazer...
Sendo assim, o proprósito inicial deste artigo é explicar como criar uma função simples que gera un número sequencial para cada linha do conjunto de resultados. Isto pode ser útil e possivelmente ficará admirado com a simplicidade de o fazer. No entanto existem alguns desafios.... E tem de estar atento à forma como funciona, ou os resultados podem parecer inesperados.

A implementação em C

Já deve ter ouvido ou lido que podemos criar funções em C, Java e SPL (Stored Procedure Language), mas a maioria de nós apenas lidou com SPL. A capacidade de extensão do Informix através das funções definidas pelo utilizador (UDRs) é uma das suas melhores vantagens, mas infelizmente é também uma das menos usadas. Isto é mau não só porque estamos a desperdiçar muito potencial, mas também porque uma maior utilização levaria certamente a mais melhorias e desenvolvimentos. Vou aproveitar esta oportunidade para mostrar o quão simples pode ser criar uma função.
Para o fazer, temos de seguir algumas regras e devemos saber alguma coisa sobre a API disponível. Um bom sítio para começar a entender como podemos criar UDRs é o User's Defined Routines and Datatypes Developer's Guide. Isto explica os conceitos genéricos e os tipos de UDRs que podemos criar. Depois podemos consultar o Datablade API Programer's Guide. Este contém uma descrição mais técnica sobre vários aspectos (como gestão de memória, processamento de input e output etc.). Por último temos o Datablade API Function Reference, para informação e ajuda em funções específicas. Mas claro... ler isto tudo sem praticar é mais ou menos inútil. Seria bom termos uma lista de exemplos que nos permitissem arrancar....

Assim o que proponho é criar uma função muito simples em C que possa ser embebida no motor. O seu uso é tão fácil como se fosse uma função nativa do motor. E a sua criação é bastante simples. A linguagem usada será C, pois SPL é menos fléxivel (embora possa ser bastante prática, útil e rápida). O Java não tem o acesso tão fácil às funções da API interna, mas pode ainda ser mais fléxivel para algumas tarefas, embora possa ser mais complexo e lento. Mas a escolha deverá depender sempre das nossas necessidades e mesmo do nosso background com cada uma das linguagens.

Antes de começar devo referir alguns pontos que na verdade serão os mais difícieis do processo:
  1. A IBM inclui alguns scripts no motor que são necessários para arrancarmos. Dentro de $INFORMIXDIR/incl/dbdk existem alguns makefiles simples. Poderá ser necessário adaptá-los à plataforma e/ou compilador que vamos usar.No meu sistema tenho:
    • makeinc.gen
    • Um makefile genérico (várias paltaformas) usado pelo próximo
    • makeinc.linux
    • Makefile específico para Linux que referencia o anterior
    • makeinc.rules
    • Makefile com regras genéricas de compilação Estes scripts podem e devem ser usados pelo nosso próprio makefile
  2. O código da função tem de incluir alguns ficheiros e seguir determinadas regras
  3. Depois de criarmos o código da função temos de a compilar para código objecto e a partir deste gerar uma biblioteca dinâmica. Esta será a forma de disponibilizar a função ao motor
  4. Depois de instalar a biblioteca temos de usar SQL para criar a função indicando ao motor onde a mesma se encontra
Vamos começar por criar o código. Como isto é um algo do tipo "mãos na massa", vou tentar manter as explicações no mínimo...:
1    /*
2 ------------------------------------------
3 secao de includes
4 ------------------------------------------
5 */
6 #include <stdio.h>
7 #include <milib.h>
8 #include <sqlhdr.h>
9 #include <value.h>
10
11 mi_integer ix_rownum( MI_FPARAM *fp)
12 {
13 mi_integer *my_udr_state, ret;
14
15 /*
16 ----------------------------------------------------------
17 Ver se já fomos chamados antes nesta instrução SQL
18 ----------------------------------------------------------
19 */
20 my_udr_state = (mi_integer *) mi_fp_funcstate(fp);
21 if ( my_udr_state == NULL )
22 {
23 // Não... não fomos... Vamos criar a estrutura persistente
24 my_udr_state = (mi_integer *)mi_dalloc(sizeof(mi_integer),PER_STMT_EXEC);
25 if ( my_udr_state == (mi_integer *) NULL)
26 {
27 ret = mi_db_error_raise (NULL, MI_EXCEPTION, "Erro em ix_rownum: Memória insuficiente");
28 return(ret);
29 }
30 // Já criámos, portanto vamos registar e inicializar a 1
31 mi_fp_setfuncstate(fp, (void *)my_udr_state);
32 (*my_udr_state)=1;
33 }
34 else
35 {
36 // Se não é a primeira vez vamos incrementar o contador...
37 (*my_udr_state)++;
38 }
39 // retornamos o contador...
40 return(*my_udr_state);
41 }
Os pontos importantes são:
  • Linhas 1-10 são os includes normais e necessários
  • Linha 11 é o cabeçalho da função. Definimos como retornando um mi_integer (nestas funções devemos usar os tipos de dados mi_*). Aceitamos um parâmetro que será um ponteiro para uma estrutura de contexto da função. Este parâmetro não será visível na assinatura "externa" da função (ao nível do SQL)
  • Linha 13 onde definimos variáveis auxiliares
  • Linha 20, tentamos obter o valor anterior que mantivemos na estrutura persistente de memória. Para isso usamos uma função da API dos datblades chamada mi_fp_funcstate
  • Linhas 21-30, se a chamada anterior devolver um ponteiro NULL, tentamos alocar (mi_dalloc) memória para manter o contador. Este será um dos passos mais importantes. Definimos que o critério de persistência é PER_STMT_EXEC. Isto significa que mantemos o contexto apenas durante a execução da mesma instrução SQL. Testamos o resultado e criamos uma excepção de a alocação falhar.
  • Linha 31 registamos a memória alocada anteriormente como o parâmetro automático da função através da chamada mi_fp_setfuncstate()
  • Linha 32 é a inicialização do contador. Na primeira chamada definimo-lo como 1
  • Linha 37 é apenas o caso geral, para todas as chamadas excepto a primeira. E no caso geral apenas incrementamos o contador
  • Linha 40 é o retorno da função, ou seja o valor do contador após inicialização ou incremento
À excepção de algumas funções estranhas, mas poderosas, o código é trivial. Mas como o disponibilizamos à camada de SQL? Antes de mais necessitamos de o compilar e gerar a biblioteca dinâmica. Para isso criamos um makefile simples:
include $(INFORMIXDIR)/incl/dbdk/makeinc.linux

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

all: ix_rownum

clean:
rm *.udr *.o

ix_rownum: ix_rownum.udr
@echo "Geração da biblioteca completa..."

ix_rownum.o: ix_rownum.c
@echo "Compilando..."
$(CC) $(CFLAGS) -o $@ -c $?

ix_rownum.udr: ix_rownum.o
@echo "Creando a biblioteca..."
$(SHLIBLOD) $(LINKFLAGS) -o $@ $?

Note-se a inclusão do makefile Linux que mencionei anteriormente. Se tudo correr bem, após corrermos make teremos uma biblioteca dinâmica chamada  ix_rownum.udr
panther@pacman.onlinedomus.com:informix-> make
Compilando...
cc -DMI_SERVBUILD -fpic -I/usr/informix/srvr1170uc4/incl/public -g -o ix_rownum.o -c ix_rownum.c
Creando a biblioteca...
gcc -shared -Bsymbolic -o ix_rownum.udr ix_rownum.o
Geração da biblioteca completa...
panther@pacman.onlinedomus.com:informix->
Após termos feito isto, necessitamos de criar a função usando SQL, como uma função externa. A sintaxe será:
CREATE FUNCTION rownum() RETURNING INTEGER
WITH (VARIANT)
EXTERNAL NAME '/home/informix/udr_tests/ix_rownum.udr(ix_rownum)'
LANGUAGE C;
Estamos a dizer ao motor para criar uma função chamada rownum, a qual não recebe nenhum parâmetro, e retorna um INTEGER. Especificamos que é escrita em C e qual a localização. Note-se que estou a dar o caminho completo da biblioteca (/home/informix/udr_tests/ix_rownum.udr) e o nome da função dentro da biblioteca (uma única biblioteca pode conter mais que uma função). Deixei a explicação para "WITH(VARIANT)" para último lugar... A criação de uma função externa permite-nos especificar várias propriedades para as funções. Esta, VARIANT, indica ao motor que a função pode retornar valores diferentes quando chamada duas ou mais vezes com os mesmos parâmetros. Isto é critico dado que não estamos a passar nenhum parâmetro. Se indicássemos ao motor que a função era NOT VARIANT apenas a chamaria uma vez. Depois disso assumiria que o valor de retorno era 1. Isto é uma optimização, mas no nosso caso não queremos que tal aconteça, dado que quebraria a lógica da função. VARIANT é o valor pré-definido, e apenas o incluí por clareza. Pode aprender mais sobre as propriedades das funções aqui:

Trabalhando com a função

Depois do exposto acima, podemos usar ROWNUM() no SQL. Vejamos alguns exemplos:
-- Exemplo 1:
SELECT customer_num, rownum() row_num
FROM customer;
customer_num row_num
101 1
102 2
103 3
104 4
105 5
106 6
107 7
108 8
109 9
[...]
-- Exemplo 2
SELECT customer_num, rownum()
FROM customer
WHERE rownum() < 5;
customer_num row_num

101 1
102 2
103 3
104 4

-- Exemplo 3
SELECT
FIRST 4
customer_num, lname
FROM
customer
ORDER BY lname DESC;

customer_num lname

106 Watson
121 Wallack
105 Vector
117 Sipes

-- Exemplo 4
SELECT
customer_num , lname, rownum() row_num
FROM
customer
WHERE rownum() < 5
ORDER BY lname DESC;

customer_num lname row_num

102 Sadler 2
101 Pauli 1
104 Higgins 4
103 Currie 3

-- Exemplo 5
SELECT
t1.*, rownum() row_num
FROM (SELECT customer_num, lname FROM customer ORDER BY lname DESC) as t1
WHERE rownum() <5;

customer_num lname row_num

106 Watson 1
121 Wallack 2
105 Vector 3
117 Sipes 4

Vamos comentar os exemplos acima. Há aspectos muito importantes a considerar:

  • Exemplo1
    Este é o exemplo mais simples. Funciona como se esperaria
  • Exemplo 2
    Aqui estamos a usar o ROWNUM() também como filtro da cláusula WHERE
  • Exemplo 3
    Este é um exemplo auxiliar para ajudar a demonstrar um possível problema. É apenas um SELECT do customer_num e lname ordenado por este de forma descrescente
  • Exemplo 4
    Aqui estamos a tentar a mesma coisa, mas usando o ROWNUM() para limitar o número de linhas. Note-se que isto altera o conjunto de resultados. Porquê? Porque o ROWNUM() é aplicado imediatamente durante o full table scan que é feito para resolver a query. As primeiras quatro linhas são obtidas, e depois o ORDER BY é aplicado. Portanto o uso do ROWNUM() altera o resultado porque é aplicado (na cláusula WHERE) antes do ORDER BY
  • Exemplo 5
    Se quiséssemos reprodudir o resultado do exemplo 3, mas ainda assim adicionar um número de linha a cada elemento dos resultados, poderíamos usar a sintaxe apresentada aqui
Referi acima que não poderíamos reproduzir a funcionalidade da instrução ROW_NUMBER(), do standard SQL. Esta permite-nos especificar uma cláusula de ORDER BY e outra de PARTITION BY.
O ORDER BY dentro do ROW_NUMBER() indica ao motor que deve ordenar a sequência de números pelos campos indicados. O PARTITION BY diz-lhe para recomeçar a numeração cada vez que o campo indicado mudar de valor (semelhante ao efeito de um GROUP BY em agregados). Note-se que este ORDER BY não afecta a ordem dos resultados apresentados.
Se está a imaginar se não poderíamos implementar uma funcionalidade semelhante usando funções, a resposta é "mais ou menos...". Mas vou deixar isso para outro possível artigo.