Clique aqui para ler esse artigo em PDF. imagem_pdf.jpg

capnet43.jpg

Clique aqui para ler todos os artigos desta edição

Um Tradutor de Triggers

 

Há diversos motivos para que se queira portar triggers entre bancos de dados diferentes. Um motivo comum hoje em dia são as políticas de consolidação, centralização, ou padronização de SGBDs, em curso em diversas empresas e que impõem a migração de um sistema feito em um BD para outro. Embora a migração do esquema e dos dados possa oferecer alguma dificuldade por conta dos tipos de campos e restrições de integridade, já existem ferramentas que auxiliam esta atividade, como por exemplo o IBDataPump (www.clevercomponents.com). No entanto, a migração dos triggers é quase sempre realizada de forma manual, sendo uma tarefa custosa e principalmente entediante, propensa portanto à introdução de bugs. Se este passo for automatizado, o custo de migração pode ser reduzido e, principalmente, a qualidade melhorada. Outras motivações para a migração são a atualização de aplicações que usam SGBDs que caíram em desuso, ou cujas licenças se tornaram muito caras, ou ainda a migração para software livre.

Neste artigo apresentaremos uma solução automatizada para um problema muito comum na área de banco de dados: portar triggers entre sistemas gerenciadores de bancos de dados (SGBD). Mais precisamente, iremos mostrar como construir um programa tradutor que reescreva na linguagem do Firebird 1.5 (PSQL) triggers originalmente escritos em PL/SQL Oracle. A técnica a ser utilizada é a mesma empregada na construção de compiladores, e pode ser facilmente adaptada para outros SGBDs. No entanto, é exigido que o programador tenha um mínimo de conhecimentos teóricos sobre construção de compiladores (normalmente ensinado em qualquer curso de graduação em informática). Faremos um breve apanhado da teoria necessária, mostrando o uso das ferramentas mais comuns disponíveis: o lex e o yacc.

 

Lex e Yacc

O processo de um compilador ou interpretador de uma linguagem de programação é geralmente decomposto em 2 tarefas:

 

1) Ler o programa fonte e descobrir sua estrutura;

2) Processar sua estrutura para gerar o programa final.

 

O Lex e o Yacc podem ajudar na primeira tarefa que pode ser subdividida em mais 2 partes:

 

1) Dividir o código em tokens (função do Lex);

2) Descobrir a hierarquia da estrutura do programa (Yacc).

 

Lex e yacc são dois programas de linha de comando disponíveis em quase todas as distribuições do Unix e do Linux. O lex deve ser chamado recebendo como entrada um parâmetro que é o nome do arquivo contendo os comandos a serem processados. Por exemplo:

 

$bash> lex entrada.l

 

         A saída do lex será um programa em C (lex.yy.c) contendo rotinas que serão chamadas pelo nosso compilador.

         O yacc deve ser chamado recebendo como parâmetro o nome do arquivo que contém as regras de tradução para o nosso compilador, e produz como saída o código source do compilador (y.tab.c). Esse código fonte contém chamadas para as funções geradas pelo lex no arquivo lex.yy.c (dessa forma, um include é usado para referenciar o arquivo lex.yy.c). Depois basta usar o gcc para compilar o arquivo y.tab.c e assim gerar o executável do nosso compilador.

         O programa compilado gerado pelo yacc é um executável que lê um arquivo na entrada padrão e, através das regras de tradução especificadas, produz o arquivo compilado, ou seja, o código traduzido, na saída padrão.

 

Semelhanças entre as linguagens

Para que seja possível automatizar a tradução de um trigger de uma linguagem para outra é necessário que haja um mínimo de compatibilidade entre suas estruturas. Linguagens que apresentem construções muito específicas irão dificultar ou mesmo tornar inviável o mapeamento. Por sorte, as construções muito específicas em geral não são muito utilizadas, o que torna o tradutor eficaz na maioria dos casos. Ou seja, podemos construir rapidamente um tradutor que trate a maioria dos triggers, e deixe uns poucos casos para serem manualmente traduzidos.

Um passo importante na hora de construir um tradutor desse tipo é decidir quais comandos e construções da linguagem serão tratados e quais serão deixados de fora. No nosso exemplo iremos construir um tradutor de triggers escritos em PL/SQL Oracle para a linguagem do Firebird 1.5 (uma versão melhorada da linguagem disponível no Interbase 6.0 e no FB 1.0). Passemos a analisar agora as principais semelhanças e diferenças entre estas duas linguagens.

O primeiro ponto a ser analisado diz respeito aos eventos que ativam os triggers. Nesse quesito temos uma incompatibilidade difícil de ser superada: o Oracle possui um tipo especial de triggers, chamado de statement level (vide artigo de Stored Procedures e Triggers no Oracle publicado na SQL Magazine nº 4). Não há como criar diretamente um trigger equivalente no Firebird. No entanto, conforme afirmamos antes, isto não chega a ser um empecilho grave pois esse tipo de trigger raramente é utilizado. De resto, os triggers do Oracle são ativados pelos mesmos seis eventos que o Firebird: after ou before insert, after ou before update e after ou before delete, chamados uma vez para cada linha afetada na tabela.

Ainda sobre a ativação de um trigger, o Oracle permite definir que um trigger de update seja acionado somente se determinados campos forem alterados. É possível também definir uma pré-condição (cláusula WHEN) para que um trigger seja acionado. Essas funcionalidades não estão presentes no Firebird, mas veremos que é fácil simulá-las com o uso de alguns comandos IF-THEN.

O segundo ponto diz respeito aos tipos de dados. Para os tipos básicos de dados não teremos muitos problemas, pois os tipos do Firebird são compatíveis com os do Oracle. Já para os cursores, arrays (vetores) e tabelas temporárias as semelhanças serão suficientes para evitar soluções complicadas, mas teremos de restringir um pouco a tradução nesse ponto.

No geral, embora haja diferenças entre as linguagens analisadas, grande parte delas são diferenças de sintaxe, ou seja, existem comandos com funções semelhantes mas com nomes ou construções diferentes em cada linguagem. Assim, será possível construir um tradutor que irá abranger a maioria dos triggers comumente implementados.

Revisão: compiladores, gramáticas, lex, yacc, atributos

Iremos agora fazer uma pequena revisão prática do processo de tradução e as principais ferramentas utilizadas – o lex e o yacc. Nosso ambiente de desenvolvimento foi o Linux Conectiva 7.0, Kernel 2.4 e gcc 2.95.3. O lex e o yacc são instalados por default nessa distribuição.

Lex e yacc são ferramentas complementares para a construção de compiladores. Ambas possuem uma linguagem de programação própria, que quando processada gera como saída um programa em C padrão, que ao ser compilado pelo gcc gera um executável – o nosso tradutor.

A técnica de construção de compiladores que iremos utilizar divide o compilador em três etapas de análise e uma de síntese: análises léxica, sintática e semântica, e geração de código. Por se tratar de um tradutor simples, não iremos abordar nenhum tipo de otimização de código nem representações intermediárias. O lex serve para construir analisadores léxicos, e o yacc para construir analisadores sintáticos. O analisador semântico e a geração de código serão manualmente escritos por nós de uma forma predeterminada para ser integrada ao lex e ao yacc.

Gramáticas

Para se construir um compilador é necessário conhecer o conceito de Gramática Livre de Contexto (GLC). Uma GLC é um conjunto de regras de substituição que podem ser aplicadas para que determinada sentença seja produzida em uma linguagem.

Uma GLC é formada por: um conjunto de símbolos terminais (que correspondem a símbolos da linguagem e são também chamados de tokens), um conjunto de símbolos não-terminais ou variáveis (que representam passos intermediários), um conjunto de regras de produção (que mostram como podemos substituir os símbolos), e um símbolo inicial. Como exemplo, veja a gramática da Listagem 1.

 

Listagem 1. Exemplo de GLC

E ? E + E

E ? E - E

E ? E * E

E ? E / E

E ? ( E )

E ? id

E ? num

 

Essa gramática mostra como podem ser geradas expressões matemáticas. Por exemplo, a produção “E ? E + E” nos mostra que a variável “E” pode ser substituída por “E + E”. Assim, para construir uma expressão aritmética qualquer envolvendo identificadores e números podemos ir aplicando as regras de substituição acima. Esse processo é chamado de derivação. Por exemplo, para derivar a expressão “id * ( id + num )” podemos usar as seguintes regras de produção:

 

E ? E * E ? id * E ? id * ( E ) ? id * ( E + E ) ? id * ( id + E ) ? id * ( id + num )

 

O que o lex e o yacc fazem é criar um programa tradutor que, recebendo como entrada um programa em uma linguagem, encontra uma derivação que produza essa entrada de acordo com determinada GLC.

Lex

O lex cuida de reconhecer os símbolos terminais: identificadores, números, palavras reservadas, operadores, parênteses etc.

Na Listagem 2 temos um fragmento de um arquivo lex (o arquivo completo está no site da revista). O símbolo “%%” é um separador de seções. Basicamente, o arquivo lex irá descrever, entre o primeiro e o segundo “%%”, o que será retornado para cada padrão que for encontrado. Assim, iremos retornar uma constante “_CREATE” quando encontrarmos a palavra “CREATE” no programa fonte, e assim por diante. Deverá haver uma entrada para cada palavra reservada da linguagem. Já NUM é um padrão mais elaborado, descrito antes do primeiro “%%”, e que é formado por dígitos com ponto opcional. Nesse caso, quando um número for reconhecido, iremos retornar uma constante “_NUM” e iremos armazenar o seu valor na variável global yylval, que é uma string do C++. Finalmente, o ponto significa um caractere simples: os operadores + e – , por exemplo.

 

Listagem 2. Exemplo do arquivo lex

WS       [ \n\t]

LETTER   [a-zA-Z_]

DIGIT    [0-9]

ID       ({LETTER}({LETTER}|{DIGIT})*)

NUM      ({DIGIT}+(\.{DIGIT}+)?)

QQSTR    (["][^"\n]*["])

QSTR     (['][^'\n]*['])

 

%%

{WS}  {};

“CREATE” { return _CREATE; }

“REPLACE” { return _REPLACE; }

“IF” { return _IF; }

“THEN” { return _THEN; }

“ELSE” { return _ELSE; }

“END IF” { return _ENDIF; }

 

{NUM} { yylval = yytext; return _NUM; }

{ID} { yylval = yytext; return _ID; }

.    { return yytext[0]; }

%%

Yacc

O yacc terá a descrição GLC da linguagem a ser traduzida, no nosso caso, o PL/SQL. Junto com as regras de produção no yacc, iremos colocar trechos de código que serão acionados para realizar a tradução de uma estrutura para sua correspondente no Firebird. Dessa forma, sempre que uma determinada produção for utilizada, o fragmento de código associado a ela será executado.

 

Nota

Uma limitação do yacc é que ele não deve ser utilizado com gramáticas ambíguas. Gramáticas ambíguas são as que permitem mais de uma árvore de derivação, ou seja, significa que o compilador pode realizar a derivação de mais de uma forma diferente. O problema é que em geral apenas uma delas é correta.

 

A Listagem 3 apresenta um fragmento de uma GLC no yacc descrevendo o comando IF do PL/SQL. Novamente, os “%%” são separadores, e a gramática irá ficar entre o primeiro e o segundo “%%”. Sempre que tivermos mais de uma possibilidade de substituição (p.e. E?E+E ou E?E–E) iremos agrupá-las através do símbolo “|” (p.e. E?E+E | E–E). No yacc, a “?” é substituída por um “:”; o “;” marca o fim da lista de produções, e o código a ser executado vem entre chaves (o código é ilustrado somente na Listagem 4).

 

Listagem 3. Fragmento do arquivo yacc

#token _ID _NUM _IF _THEN _ELSE _ENDIF

 

%%

 

CMD_IF : _IF EXP _THEN CMDS _ENDIF

       | _IF EXP _THEN CMDS _ELSE CMDS _ENDIF

       ;

 

CMDS : CMD ‘;’ CMDS

     | CMD ‘;’

     ;

 

%%

 

Nesse exemplo não mostramos o restante da gramática para os símbolos EXP e CMD. Note que os símbolos terminais (tokens) são descritos antes do primeiro “%%” e deverão ter o seu padrão descrito no arquivo do lex. Vemos também que CMDS é uma lista de CMD separados por ‘;’: quando um caractere aparece entre plics quer dizer que ele deve aparecer no programa de entrada também.

Essa gramática nos informa que um comando IF do PL/SQL é sempre terminado por um ENDIF, possui um THEN e pode ter um ELSE ou não. Após o THEN e após o ELSE podem vir uma lista de comandos separados por “;”.

Atributos

Para facilitar a programação, o yacc disponibiliza um atributo para cada token encontrado durante a análise sintática. Assim, na produção “A ? X Y Z” teremos quatro atributos. Utilizamos os atributos dos símbolos do lado direito para calcular o valor do atributo do símbolo do lado esquerdo, ou seja, usamos X, Y e Z para calcular A. Finalmente, cada atributo é representado por um $ seguido do número de sua posição: o A é $0, o X é $1, o Y é $2 e o Z é $3. O $0 pode ser escrito também como $$.

A Listagem 4 apresenta, para o exemplo da Listagem 3, a regra de tradução do IF em PL/SQL Oracle para IF em Firebird. É importante atentar que no Firebird não existe ENDIF, mas cada bloco de comandos deve vir entre BEGIN–END.

 

Listagem 4. Fragmento do arquivo yacc

%%

 

CMDS : CMD ‘;’ CMDS { $$ = $1 + “;\n” + $3; }

     | CMD ‘;’ { $$ = $1 + “;\n”; }

     ;

 

CMD : CMD_IF

    | CMD_INSERT

    | CMD_UPATE

    ... // trecho de código suprimido

    ;

 

CMD_IF : _IF EXP _THEN CMDS _ENDIF { $$ = “IF ” + $2 + “ THEN \nBEGIN\n” + $4 +

         “END;\n” }

       | _IF EXP _THEN CMDS _ELSE CMDS _ENDIF { $$ = “IF “ + $2 +

         “ THEN \nBEGIN\n” + $4 + “END\nELSE\nBEGIN\n” + $5 + “END;\n” }

       ;

 

%%

 

Nesta listagem temos a tradução de um IF: (1) tomando os atributos do lado esquerdo e concatenando-os na ordem correta, (2) colocando as palavras BEGIN-END no local apropriado e (3) ignorando o ENDIF.

 Estamos assumindo que cada comando terá a sua própria regra de tradução, como no caso do CMD_IF, CMD_INSERT, CMD_UPDATE etc. Cada uma dessas regras irá calcular o seu atributo $$. A regra de tradução default é $$ = $1, ou seja, se nenhuma tradução for especificada o valor de $1 é copiado para o $$. Na listagem acima, na tradução de “CMD : CMD_IF” teremos o valor de $1 copiado diretamente para $$. Note que a tradução de “CMDS : CMD ‘;’ CMDS” concatena os atributos do lado direito  antes de copiar para $$. Com isso, o compilador irá agir da seguinte forma: cada comando terá uma regra de tradução, semelhante ao código que mostramos para o CMD_IF. O resultado dessa tradução é copiado para CMD, e depois cada CMD é concatenado na  regra CMDS, acumulando todo o código do trigger em CMDS.

Tendo entendido como as coisas funcionam, nosso trabalho agora será criar uma gramática para o PL/SQL Oracle e projetar uma tradução das estruturas de cada comando para seu equivalente no Firebird.

A gramática utilizada

Agora definiremos de forma incremental a gramática. Inicialmente trataremos dos eventos e em seguida do bloco com as declarações de variáveis e comandos. A Listagem 5 ilustra a gramática inicial.

 

Listagem 5. Gramática Inicial

TRG ? _CREATE RPL _TRIGGER TRGNAME ORDER EVENTS _ON TRGTABLE FOREACHROW PLBLOCK

FOREACHROW ? _FOR _EACH _ROW

RPL ? _OR _REPLACE | ?

TRGNAME ? _ID '.' _ID | _ID

ORDER ? _BEFORE | _AFTER

EVENTS ? EVENTS _OR EVENT | EVENT

EVENT ? _INSERT | _UPDATE UPDCOLS | _DELETE

UPDCOLS ? _OF _ID COLS | ?

COLS ? ',' _ID COLS | ?

TRGTABLE ? _ID '.' _ID | _ID

 

Estamos usando a letra grega ? para simbolizar a substituição por vazio, de modo a representar comandos opcionais. Por exemplo, RPL pode ser substituído por “OR REPLACE” ou por vazio, de forma que podemos ter após o “CREATE” a cláusula “OR REPLACE” ou direto o nome do trigger. Além disso, no Firebird o “REPLACE” deve ser trocado por “ALTER”.

Para simular a funcionalidade “UPDATE OF coluna” presente no Oracle e ausente no Firebird iremos gerar um trigger de update começando com um IF que irá testar se alguma das colunas presentes na lista de colunas do update foi alterada. Este teste consiste em comparar o valor novo com o velho: old.coluna <> new.coluna. Devemos apenas tomar cuidado com a lógica do teste para considerar também valores nulos. O teste correto será (para cada coluna C): (new.C <> old.C) or (new.C is null and old.C is not null) or (new.C is not null and new.C is null). Esse mesmo artifício pode ser utilizado para simular a cláusula WHEN. A Listagem 6 mostra o código dessa tradução.

 

Listagem 6. Fragmento de Código do Yacc

string ofColumns;

%%

 

TRG : _CREATE RPL _TRIGGER TRGNAME ORDER EVENTS _ON TRGTABLE FOREACHROW PLBLOCK

      {

        cout<<“SET TERM !! ;”;

        cout<<“CREATE “<<$2<<”TRIGGER “<<$4<<“ FOR “<<$8<