asfernandes
This blog has a new home: https://asfernandes.github.io

A linguagem procedural do Firebird, a PSQL, foi incrementada com novos comandos e outras melhorias. A primeira melhoria que irei apresentar se trata do uso de subqueries como expressões PSQL. Antes, o desenvolvedor era obrigado a criar uma variável e fazer um SELECT INTO para obter um valor e usá-lo posteriormente em outro comando. Agora as subqueries fazem parte do conjunto de expressões PSQL e podem ser usadas da mesma maneira que já podiam ser usadas em comandos SQL. A listagem 15 mostra o uso de uma subquery em uma comparação e em seguida na atribuição de um valor retornado a uma variável.


Listagem 15. Usando subqueries como expressões PSQL.
SET TERM !;
EXECUTE BLOCK RETURNS (COD VARCHAR(10)) AS
BEGIN
 IF ((SELECT COUNT(*) FROM DOCUMENTOS) > 3) THEN
 BEGIN
    COD = (SELECT MAX(CODIGO) FROM DOCUMENTOS);
    SUSPEND;
 END
END!
SET TERM ;!


Na listagem 15 o parâmetro de retorno COD foi definido com o tipo VARCHAR(10) e recebe um valor lido da coluna CODIGO da tabela DOCUMENTOS. Uma alteração do tipo desta coluna poderia deixar este bloco de código inconsistente ou inválido. No Firebird 2.1 foi adicionado o suporte ao uso de domains em PSQL. Neste caso, o desenvolvedor precisaria criar um domínio e usá-lo na definição da tabela e na definição do parâmetro de retorno do EXECUTE BLOCK para ter um código consistente. Dependendo da situação, a criação de domínios para todas as colunas pode se tornar uma tarefa extremamente burocrática. Pensando nisso, o Firebird 2.5 adiciona a cláusula TYPE OF COLUMN. Com essa cláusula é possível declarar parâmetros e variáveis ou fazer casts usando o tipo definido de uma coluna. Ao alterar o tipo da coluna, a alteração se reflete automaticamente em todas as stored procedures e triggers que façam uso de seu tipo. A listagem 16 mostra o mesmo código da listagem 15 mas usando TYPE OF COLUMN para o parâmetro COD.


Listagem 16. Usando a cláusula TYPE OF COLUMN.
EXECUTE BLOCK RETURNS (COD TYPE OF COLUMN DOCUMENTOS.CODIGO) AS
BEGIN
 IF ((SELECT COUNT(*) FROM DOCUMENTOS) > 3) THEN
 BEGIN
    COD = (SELECT MAX(CODIGO) FROM DOCUMENTOS);
    SUSPEND;
 END
END!


No Firebird 2.1 foram acrescentados os triggers de eventos de banco de dados, como o evento ON CONNECT. Uma das funcionalidades que podiam ser feitas com este tipo de trigger era a rejeição de uma conexão, permitindo que os desenvolvedores implementassem um sistema de segurança dentro do banco de dados. Para rejeitar uma conexão é necessário lançar uma exceção dentro do trigger. Nota: este tipo de segurança não impede que o usuário SYSDBA tenha acesso ao banco, pois os administradores podem desativar os triggers de banco de dados conectando-se usando a opção -nodbtriggers do ISQL. O problema é que alguns desenvolvedores gostariam de gravar uma entrada em uma tabela de log registrando o evento, mas a exceção causava o rollback da criação deste registro. Esta e outras situações (como comunicação com outros sistemas através de UDFs) agora podem se beneficiar de um novo comando, o IN AUTONOMOUS TRANSACTION. Assim como um IF ou WHILE, este comando aceita um subcomando (ou vários, com o uso de BEGIN END). No início da execução do IN AUTONOMOUS TRANSACTION é aberta uma nova transação e o subcomando é executado dentro desta transação. Caso o subcomando execute por completo, é feito um COMMIT na transação criada. Caso ocorra uma exceção, é feito um rollback. A listagem 17 apresenta um trigger ON CONNECT que aceita apenas conexões dos usuários SYSDBA e ADMIN e grava o nome dos outros usuários que tiverem a conexão rejeitada. Note que a exceção usada para rejeitar a conexão é lançada fora do bloco IN AUTONOMOUS TRANSACTION, que já havia terminado e, consequentemente, inserido o registro e finalizado a transação com COMMIT.


Listagem 17. Usando o comando IN AUTONOMOUS TRANSACTION.
CREATE TABLE CONN_LOG (USUARIO VARCHAR(128), DATA TIMESTAMP);
CREATE EXCEPTION E_CONN 'Acesso negado.';
SET TERM !;
CREATE TRIGGER T_CONN ON CONNECT AS
BEGIN
 IF (CURRENT_USER <> 'SYSDBA' AND CURRENT_USER <> 'ADMIN') THEN
 BEGIN
    IN AUTONOMOUS TRANSACTION DO
BEGIN
       INSERT INTO CONN_LOG
VALUES (CURRENT_USER, CURRENT_TIMESTAMP);
END

    EXCEPTION E_CONN;
 END
END!
SET TERM ;!


O comando EXECUTE STATEMENT ganha algumas novidades importantes na versão 2.5. Uma delas é uma das funcionalidades mais pedidas pelos usuários: ler e atualizar dados em outros bancos de dados. Através da cláusula ON EXTERNAL DATA SOURCE é possível especificar o banco de dados em que o comando executará. É permitido também que o desenvolvedor especifique o nome, senha e role do usuário para fazer a conexão, e o funcionamento da transação iniciada para rodar o comando no banco de dados externo. Com WITH COMMON TRANSACTION (opção default), a transação da conexão externa é agrupada à transação interna atual e o comando COMMIT ou ROLLBACK executado na transação atual é propagado às transações externas. Com WITH AUTONOMOUS TRANSACTION o funcionamento se torna similar as transações autônomas, sendo feito o COMMIT ou ROLLBACK logo após a execução sucedida ou não do comando externo. A listagem 18 mostra o uso deste comando para ler e gravar dados em outro banco de dados.


Listagem 18. Executando comandos em outros bancos de dados com EXECUTE STATEMENT.
SET TERM !;
EXECUTE BLOCK RETURNS (NOME VARCHAR(60)) AS
 DECLARE BANCO VARCHAR(60) = 'localhost:/db/pessoas.fdb';
 DECLARE USUARIO VARCHAR(10) = 'SYSDBA';
 DECLARE SENHA VARCHAR(10) = 'masterke';
 DECLARE NOME_INSERIR VARCHAR(20) = 'Sicrano';
BEGIN
 FOR EXECUTE STATEMENT 'SELECT NOME FROM PESSOAS'
     ON EXTERNAL DATA SOURCE BANCO
     AS USER USUARIO PASSWORD SENHA
     INTO NOME
 DO
 BEGIN
     SUSPEND;
 END


 EXECUTE STATEMENT
   'INSERT INTO PESSOAS (NOME) VALUES (''' || NOME_INSERIR || ''')'
   ON EXTERNAL DATA SOURCE BANCO
   AS USER USUARIO PASSWORD SENHA
   WITH AUTONOMOUS TRANSACTION;
END!
SET TERM ;!


A listagem 18 mostra como era feita a criação de strings de SQL dinâmico, usando concatenação de strings. A criação de comandos com concatenação é insegura, pois um usuário mal-intencionado pode gerar comandos não previstos pelo desenvolvedor, causando o ataque conhecido como injeção de SQL [1]. No novo EXECUTE STATEMENT é possível passar parâmetros de duas formas diferentes. A listagem 19 mostra o uso de parâmetros anônimos, com o símbolo de interrogação.

Listagem 19. Usando EXECUTE STATEMENT com parâmetros anônimos.
SET TERM !;
EXECUTE BLOCK AS
 DECLARE NOME VARCHAR(20) = 'Sicrano';
 DECLARE DATA_NASCIMENTO DATE = DATE '2000-01-01';
BEGIN
 EXECUTE STATEMENT
   ('INSERT INTO PESSOAS (NOME, DATA_NASCIMENTO) VALUES (?, ?)')
   (NOME, DATA_NASCIMENTO);
END!
SET TERM ;!


Além da passagem de parâmetros anônimos, o comando EXECUTE STATEMENT agora suporta o uso de parâmetros nomeados. As vantagens dos parâmetros nomeados são que o desenvolvedor não precisa se preocupar com a ordem e também pode usar um mesmo parâmetro mais de uma vez sem a necessidade de criação de variáveis. O uso dos parâmetros dentro da string é similar a passagem de parâmetros feita com o símbolo dois-pontos em bibliotecas Delphi, como expliquei na parte referente à construção “? IS NULL”. Os valores são associados aos nomes dos parâmetros usando-se o operador “:=” (dois pontos, igual). A listagem 20 mostra o uso de parâmetros nomeados.


Listagem 20. Usando EXECUTE STATEMENT com parâmetros nomeados.
SET TERM !;
EXECUTE BLOCK AS
 DECLARE NOME VARCHAR(20) = 'Sicrano';
 DECLARE DATA_NASCIMENTO DATE = DATE '2000-01-01';
BEGIN
 EXECUTE STATEMENT
   ('INSERT INTO PESSOAS (NOME, DATA_NASCIMENTO)
       VALUES (:P_NOME, :P_DATA_NASCIMENTO)')
   (P_DATA_NASCIMENTO := DATA_NASCIMENTO, P_NOME := NOME);
END!
SET TERM ;!

Nota

[1] Injeção de SQL é o nome de um ataque em que o usuário mal-intencionado preenche dados de um formulário de forma que um comando gerado no servidor através de concatenação de strings seja alterado. Geralmente o ataque é feito usando-se aspas simples e um restante de código que termine o comando iniciado pelo desenvolvedor, seguindo com outro comando completo e o símbolo de início de comentário (--) para invalidar o restante do comando.

 

C++: Argument-dependent name lookup

Posted In: . By Adriano

Sometimes C++ still surprises me. Consider this code:

namespace ns1
{
  class C1
  {
  };

  void f1(const C1&)
  {
  }
}


int main()
{
  ns1::C1 c1;
  f1(c1);

  return 0;
}

If you think it's wrong and causes a compiler error, you're wrong.

The function "f1" is not in the scope of "main", but its argument has a type of the same namespace, causing the function to be located.

This rule is called Argument-dependent name lookup (or Koenig lookup) and it is also very necessary in relation to operators.

 

Na parte de expressões SQL, o Firebird 2.5 acrescenta melhorias e novidades. A função agregada LIST (presente desde a versão 2.1) agora aceita qualquer expressão em seu segundo parâmetro, onde o desenvolvedor pode especificar a string de separação dos elementos retornados. Até a versão 2.1.3 era possível apenas o uso de strings constantes nesse parâmetro. Nota: a versão 2.1.4 também deve ser liberada com esta novidade. A listagem 10 mostra a utilidade deste parâmetro, principalmente no desenvolvimento de relatórios, agrupando várias mensagens referentes a um documento em um mesmo registro e mostrando cada mensagem em uma linha.

Listagem 10. Usando a função LIST com expressões no segundo parâmetro.

SELECT LIST(MENSAGEM, ASCII_CHAR(13) || ASCII_CHAR(10))
 FROM MENSAGENS_DOCUMENTO WHERE DOCUMENTO = 10;

Referente a parâmetros de comandos SQL, o Firebird suporta apenas o uso de parâmetros anônimos com o símbolo “?” (ponto de interrogação). Algumas bibliotecas (inclusive de Delphi) aceitam o uso de parâmetros nomeados usando “:” (dois pontos). Quando o desenvolvedor tentava escrever queries usando o padrão [WHERE :CODIGO IS NULL OR CODIGO = :CODIGO] para trazer todos os registros quando o parâmetro não fosse informado, o Firebird retornava um erro. Isto porque este comando é traduzido pelas bibliotecas para [WHERE ? IS NULL OR CODIGO = ?] e o Firebird não aceitava o uso de parâmetro com o predicado IS NULL, pois o tipo do parâmetro era considerado como desconhecido. Na versão 2.5 foi adicionado à API o tipo SQL_NULL. As bibliotecas de acesso precisam entender este novo tipo e apenas passar se o valor do parâmetro é ou não NULL, permitindo assim o uso deste padrão de comando.

Outra novidade referente a expressões é o novo predicado SIMILAR TO. Esta expressão é usada para fazer comparações usando expressões regulares de acordo com o padrão SQL. A listagem 11 mostra uma verificação de números de telefones cadastrados fora do padrão (NN) NNNN-NNNN. O caractere de escape funciona exatamente como no comando LIKE, considerando o próximo caractere como um valor literal, ao invés de usá-lo como um operador.

Listagem 11. Verificando números de telefones cadastrados fora do padrão.
SELECT * FROM PESSOAS
 WHERE TELEFONE NOT SIMILAR TO
   '\([0-9]{2}\) [0-9]{4}\-[0-9]{4}' escape '\'
A tabela 1 mostra os operadores permitidos em expressões regulares e a tabela 2 mostra as classes de caracteres que podem ser usadas com o operador [[:CLASSE:]].


Operador
Descrição
X{2}
Duas ocorrências de X.
X{2,}
Duas ou mais ocorrências de X.
X{4,6}
De quatro a seis ocorrências de X.
X?
Zero ou uma ocorrência de X.
X*
Zero ou mais ocorrências de X.
X+
Uma ou mais ocorrências de X.
X|Y
X ou Y.
_
Qualquer caractere - como no LIKE.
%
Qualquer sequência de caracteres - como no LIKE.
(X)
Agrupa X para ser tratado pelo operador subsequente.
[XYZ]
Qualquer caractere igual a X, Y ou Z.
[^XYZ]
Qualquer caractere diferente de X, Y ou Z.
[X-Z]
Qualquer caractere entre X e Z.
[[:CLASSE:]]
Qualquer caractere de uma certa classe, conforme tabela 2.
Tabela 1. Operadores de expressões regulares.


Classe
Descrição
ALPHA
Qualquer caractere entre A e Z.
UPPER
Qualquer caractere maiúsculo.
LOWER
Qualquer caractere minúsculo.
DIGIT
Dígitos de 0 a 9.
SPACE
Espaço: caractere (ASCII_CHAR) 32.
WHITESPACE
Todo tipo de espaço: caracteres 9, 10, 11, 12, 13 e 32.
Tabela 2. Classes de caracteres para expressões regulares.


O Firebird 2.5 também suporta novos formatos de valores literais (constantes) para números e strings. Agora é possível escrever números inteiros no formato hexadecimal, usando o prefixo 0x. Quando o número possuir até 8 dígitos hexadecimais após o 0x, como em 0xFFFFFFFF, o número adquire o tipo INTEGER (32 bits com sinal). Quando possuir mais de 8 dígitos, como em 0x0FFFFFFFF, o tipo adquirido é o BIGINT (64 bits com sinal). Desta forma, estas duas constantes retornam valores diferentes, pois os números negativos são armazenados na notação complemento de dois [1].

Além de números, também é possível a criação de strings binárias (character set OCTETS) usando a notação hexadecimal x''. Em ambos os casos a letra X pode ser escrita em maiúscula ou minúscula. A listagem 12 mostra o uso destes novos tipos de valores literais. Note que cada par de caracteres hexadecimais se transforma em um byte na string resultante.

Listagem 12. Novos formatos de expressões literais.
SELECT 0xDEADBEEF, x'DEADBEEF' FROM RDB$DATABASE;
SELECT OCTET_LENGTH(x'DEADBEEF') FROM RDB$DATABASE;
-- Resultado de OCTET_LENGTH: 4, e não 8

A nova função BIN_NOT se junta ao grupo de funções binárias adicionadas na versão 2.1, BIN_AND e BIN_OR. Junto com as constantes hexadecimais, essa família de funções facilita o uso de máscaras de valores binários gravados em um único campo, técnica comumente utilizada em aplicações e agora facilitada no banco de dados. A listagem 13 mostra o uso da função BIN_NOT, que inverte todos os bits de um valor inteiro.

Listagem 13. Exemplo de uso da função BIN_NOT.
SELECT BIN_NOT(0xFFFFFFFF) FROM RDB$DATABASE;  -– Resultado: 0

Na versão 2.1 foi adicionada a função GEN_UUID, que retorna um UUID [2] como um valor do tipo CHAR(16) CHARACTER SET OCTETS. Este tipo de retorno foi escolhido pois é a representação mais compacta para ser usado em índices, porém requer suporte das aplicações que desejam exibir estes UUIDs aos usuários. Na nova versão foram adicionadas funções para conversão entre as representações binária e texto [CHAR(36) CHARACTER SET ASCII] de UUIDs, as funções CHAR_TO_UUID e UUID_TO_CHAR. O formato texto de UUID aceito e retornado por estas funções é 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'. A listagem 14 mostra o uso destas funções.


Listagem 14. Exemplo de uso das funções UUID_TO_CHAR e CHAR_TO_UUID.
SELECT UUID_TO_CHAR(UUID), DESCRICAO FROM OBJETOS

 WHERE UUID = CHAR_TO_UUID(?);


Notas

[1] - Complemento de dois é a notação mais comum usada para representar números inteiros com sinal em sistemas computacionais. Nesta notação o bit mais significativo de um número positivo ou do número zero é representado como 0. Os números negativos são representados com os bits invertidos e somado o valor 1. Desta forma, em um número de 32 bits o 1 é representado com trinta e um bits 0 seguido por um bit 1, enquanto que o número -1 é representado por trinta e dois bits 1.

[2] - UUID (Universally Unique Identifier) é uma sequência aleatória de 16 bytes que, independente do local e momento que seja gerada, é única. Uma das utilidades dos UUIDs é criar chaves em sistemas distribuídos, como filiais de uma empresa com bancos de dados separados e que tenham os dados agregados em um banco principal através de replicação.

 

Além do comando CREATE COLLATION, mostrado na seção anterior, o Firebird 2.5 acrescenta comandos DDL que aliviam limitações anteriores e outros comandos totalmente novos. Duas limitações existentes nas versões anteriores eram relacionadas à impossibilidade de alteração de colunas COMPUTED BY e views. Era preciso eliminar estes objetos e recriá-los com as alterações. O problema é que o Firebird não permite que um objeto seja eliminado quando este está sendo usado por outro objeto. Algumas ferramentas (como o Flamerobin, por exemplo) geram scripts para eliminar e recriar objetos durante a alteração de um objeto que não era permitida, porém esta não é a solução ideal.

A listagem 5 mostra o comando usado para alteração de uma expressão COMPUTED no Firebird 2.5 e a listagem 6 mostra um exemplo do comando ALTER VIEW.

Listagem 5. Exemplo de alteração de expressão COMPUTED BY.
-- Criação da tabela com erro na expressão da coluna IDADE.
CREATE TABLE PESSOAS (
 NOME VARCHAR(60),
 DATA_NASCIMENTO DATE,
 IDADE COMPUTED BY (DATEDIFF(YEAR, CURRENT_DATE, DATA_NASCIMENTO)));
INSERT INTO PESSOAS VALUES ('Fulano', '2000-01-01');
INSERT INTO PESSOAS VALUES ('Beltrano', '1950-05-10');
SELECT * FROM PESSOAS;  -- Lista a idade como negativa.
-- Correção da expressão da coluna IDADE.
ALTER TABLE PESSOAS
 ALTER IDADE COMPUTED BY (DATEDIFF(YEAR, DATA_NASCIMENTO, CURRENT_DATE));
SELECT * FROM PESSOAS;

Listagem 6. Exemplo do comando ALTER VIEW.
-- View criada incorretamente sem a expressão WHERE.
CREATE VIEW PESSOAS_MAIORES AS
 SELECT * FROM PESSOAS;
-- Correção da view.
ALTER VIEW PESSOAS_MAIORES AS
 SELECT * FROM PESSOAS WHERE IDADE >= 18;

Assim como em outros comandos DDL, também é permitido o uso dos comandos RECREATE VIEW e CREATE OR ALTER VIEW. Além dos novos comandos relacionados a views, agora é permitido o uso de stored procedures na cláusula FROM de uma view, o que não era permitido nas versões anteriores. A listagem 7 mostra um exemplo.


Listagem 7. Exemplo de uso de stored procedure em uma view.
SET TERM !;
CREATE PROCEDURE PESSOAS_SP RETURNS (NOME VARCHAR(60), IDADE INTEGER) AS
BEGIN
 FOR SELECT NOME, IDADE FROM PESSOAS INTO NOME, IDADE DO
     SUSPEND;
END!
SET TERM ;!
CREATE VIEW PESSOAS_SP_MAIORES AS
 SELECT * FROM PESSOAS_SP WHERE IDADE >= 18;

Na nova versão também foram incluídos comandos DDL para manuseio do banco de dados de usuários. Estes comandos são: CREATE USER, ALTER USER e DROP USER. Estes comandos podem ser executados quando conectado a qualquer banco de dados, mas sempre atualizam os dados do banco de dados geral de usuários (security2.fdb). Os comandos CREATE USER e DROP USER podem ser usados apenas por usuários com privilégio de administrador. O comando ALTER USER pode ser usado por qualquer usuário, desde que usado para alterar apenas suas próprias informações (a senha, por exemplo). Já os administradores podem usar ALTER USER para alterar informações de qualquer usuário. Além da senha (PASSWORD), é possível registrar os nomes (FIRSTNAME, MIDDLENAME e LASTNAME) e definir se um usuário é ou não um administrador com a cláusula GRANT/REVOKE ADMIN ROLE. A listagem 8 mostra exemplos de criação, alteração e remoção de usuários.


Listagem 8. Manuseio do banco de dados de segurança com comandos SQL.
CREATE USER FULANO PASSWORD 'altereja'
 FIRSTNAME 'Fulano' LASTNAME 'da Silva';
ALTER USER SYSDBA PASSWORD 'masterkey';
DROP USER BELTRANO;
CREATE USER ADMIN_ADJUNTO PASSWORD 'masterkey' GRANT ADMIN ROLE;
GRANT RDB$ADMIN TO ADMIN_ADJUNTO;

Para um usuário tornar-se efetivamente um administrador, um usuário que já seja administrador (como o SYSDBA) precisa, além de colocar GRANT ADMIN ROLE no CREATE USER ou ALTER USER, conceder a role RDB$ADMIN ao usuário com o comando “GRANT RDB$ADMIN TO Usuario” em cada banco de dados que o usuário poderá atuar como administrador. O usuário também precisará conectar-se ao banco de dados usando a ROLE RDB$ADMIN. A listagem 9 mostra o usuário ADMIN_ADJUNTO atuando como SYSDBA.


Listagem 9. Conectando-se com privilégios de administrador a um banco de dados.
CONNECT 'TEST.FDB' USER ADMIN_ADJUNTO
 PASSWORD 'masterkey' ROLE RDB$ADMIN;

DROP USER FULANO;