terça-feira, 9 de outubro de 2012

Bug na compilação do T-SQL

Hoje eu estava codificando uma função e encontrei um bug na compilação do SQL Server! Vamos ver se você encontra? No meu caso eram centenas de linhas de código, então para simplificar sua vida eu escrevi uma função e enchi um pouco de linguiça...
USE tempdb
GO

IF (OBJECT_ID('dbo.fn_Teste', 'FN') IS NOT NULL)
      DROP FUNCTION dbo.fn_Teste
go

CREATE FUNCTION dbo.fn_Teste(@P1 INT, @P2 INT)
RETURNS INT
AS
BEGIN

      DECLARE @Retorno INT

      IF (@P1 = 1)
      BEGIN
            SELECT @Retorno = COUNT(*)
            FROM sys.objects
            WHERE object_id > @P2
      END
 
      IF (@P1 = 2)
      BEGIN
            SELECT @Retorno = COUNT(*)
            FROM sys.indexes AS S
            WHERE S.index_id < @P2
      END
           
      IF (@P1 = 3)
      BEGIN
            SELECT @Retorno = COUNT(*)
            FROM sys.columns
            WHERE system_type_id = dbo.fn_Teste(1)
      END

      IF (@P1 = 4)
      BEGIN
            SELECT @Retorno = COUNT(*)
            FROM sys.allocation_units AS S
            WHERE S.total_pages > @P2
      END
     
      RETURN @Retorno
END
go

SELECT dbo.fn_Teste(1, 4)

Na hora que você executar o SELECT a consulta vai receber a seguinte mensagem de erro: “Msg 313, Level 16, State 2, Line 1 - An insufficient number of arguments were supplied for the procedure or function dbo.fn_Teste.“.

Oxi, número de parâmetros errados? Ou estou muito doidão ou estou certo e coloquei os 2 parâmetros na chamada da função, e pau! Já viu o problema? Se sim, ótimo, caso contrário continuemos... Na dúvida eu fui fazer outro teste, apago a função original, coloco um 2 no nome e executo o script abaixo.

IF (OBJECT_ID('dbo.fn_Teste', 'FN') IS NOT NULL)
      DROP FUNCTION dbo.fn_Teste
go

IF (OBJECT_ID('dbo.fn_Teste2', 'FN') IS NOT NULL)
      DROP FUNCTION dbo.fn_Teste2
go

CREATE FUNCTION dbo.fn_Teste2(@P1 INT, @P2 INT)
RETURNS INT
AS
BEGIN

      DECLARE @Retorno INT

      IF (@P1 = 1)
      BEGIN
            SELECT @Retorno = COUNT(*)
            FROM sys.objects
            WHERE object_id > @P2
      END
 
      IF (@P1 = 2)
      BEGIN
            SELECT @Retorno = COUNT(*)
            FROM sys.indexes AS S
            WHERE S.index_id < @P2
      END
           
      IF (@P1 = 3)
      BEGIN
            SELECT @Retorno = COUNT(*)
            FROM sys.columns
            WHERE system_type_id = dbo.fn_Teste(1)
      END

      IF (@P1 = 4)
      BEGIN
            SELECT @Retorno = COUNT(*)
            FROM sys.allocation_units AS S
            WHERE S.total_pages > @P2
      END
     
      RETURN @Retorno
END
go

SELECT dbo.fn_Teste2(1, 4)

Resultado: funcionou! Ihhh, lascou tudo. Por que a fn_Teste2 funcionou e a primeira não? Já sabe? Então te dou um tempinho para pensar antes de continuar lendo...
(Olha o tempo galera! Agora no placar Leitor 1 x 0 SQL Server...)

Vamos lá...

Isso não é BUG do SQL Server coisa nenhuma, só foi uma brincadeira da minha parte. O que estávamos vendo aqui é a resolução deferida do SQL Server em ação.
Quando criamos a função fn_Teste se você reparar no código da função eu estou chamando a função fn_Teste novamente (no IF = 3), só que com a parametrização errada (1 parâmetro somente). Na criação da fn_Teste esse objeto efetivamente ainda não foi criado, então o corpo da função referencia outra função que ainda não existe (ela mesma!), portanto a validação é deferida.
No momento que executamos a chamada a fn_Teste, o problema com os parâmetros não está na sua chamada (são 2 parâmetros, ok!), mas sim na chamada da função no meio do T-SQL, pois já que o objeto existe o SQL Server faz o binding e verifica o problema.
O curioso fica por conta da fn_Teste2, no script de propósito eu apaguei a fn_Teste para temos uma validação deferida. E na hora que a função foi executada a fn_Teste também não existia, então não tendo como validar a fn_Teste, a função executou com sucesso. Se você tentar executar a consulta “SELECT dbo.fn_Teste2(3, 4)”, aí vai receber um erro, pois sua chamada vai entrar no IF que precisa da função inexistente.
Curioso é o seguinte, quando você invocar fn_Teste2(1, 4) e depois consultar o plan cache vai ver a entrada da função, usecount sendo incrementado, porém o plano não está em cache! Nesse momento se você criar fn_Teste(), mesmo com o corpo do fn_Teste2 estando com a chamada errada para fn_Teste (somente um parâmetro), a chamada para fn_Teste2(1, 4) ainda vai funcionar!
Para ver o erro você vai precisar chamar DBCC FREEPROCCACHE e depois fn_Teste2(1, 4), aí nessa compilação como o fn_Teste existe, a operação não será deferida e o número incorreto de parâmetros será detectado.
Brincadeiras com o T-SQL, eu me diverti...
Abraços
sr. Nimbus Serviços em Tecnologia - www.srnimbus.com.br

Um comentário: