TSQL - Como usar GO dentro de um bloco BEGIN .. END?

96

Estou gerando um script para migrar automaticamente as alterações de vários bancos de dados de desenvolvimento para teste / produção. Basicamente, ele pega um monte de scripts de mudança e os mescla em um único script, envolvendo cada script em uma IF whatever BEGIN ... ENDinstrução.

No entanto, alguns dos scripts requerem uma GOinstrução para que, por exemplo, o analisador SQL saiba sobre uma nova coluna após sua criação.

ALTER TABLE dbo.EMPLOYEE 
ADD COLUMN EMP_IS_ADMIN BIT NOT NULL
GO -- Necessary, or next line will generate "Unknown column:  EMP_IS_ADMIN"
UPDATE dbo.EMPLOYEE SET EMP_IS_ADMIN = whatever

No entanto, depois de embrulhar isso em um IFbloco:

IF whatever
BEGIN
    ALTER TABLE dbo.EMPLOYEE ADD COLUMN EMP_IS_ADMIN BIT NOT NULL
    GO
    UPDATE dbo.EMPLOYEE SET EMP_IS_ADMIN = whatever
END

Falha porque estou enviando um BEGINsem correspondência END. Porém, se eu remover o GOele reclamará novamente de uma coluna desconhecida.

Existe alguma maneira de criar e atualizar a mesma coluna em um único IFbloco?

BlueRaja - Danny Pflughoeft
fonte
2
Consulte stackoverflow.com/questions/4855537/… por favor
gbn
2
@gbn: Sim, eu entendo por que isso acontece (veja o segundo parágrafo) ; mas não tenho ideia de como contornar isso - realmente preciso transformar cada consulta em um monte de strings !?
BlueRaja - Danny Pflughoeft
@BlueRaja: Qual é a preocupação? Se funcionar, isso é tudo que importa no final do dia. Se houver um problema comercial legítimo com a solução fornecida, expresse isso. Existe algo especificamente desconcertante em converter cada consulta em um monte de strings?
mellamokb
1
@mellamokb: Sim, há um problema; se a palavra GO for usada em qualquer outro contexto (como um comentário ou uma string), o script não funcionará. Além disso, perdemos os números de linha úteis em mensagens de erro no caso de algo dar errado. Não há como fazer isso com transações? Ou tente / pegar?
BlueRaja - Danny Pflughoeft
@BlueRaja: 1) Acredito que deva GOestar em uma linha por si só, então você pode pesquisar apenas esse caso, e não todas as ocorrências da palavra GO. 2) Você sempre pode registrar quais declarações foram concluídas com êxito. Ou você poderia embrulhar tudo em um try / catch e usar seus próprios números de linha usando alguma variável, como @lineNo, que você acompanha e relata o erro. Como você os está gerando automaticamente, fazer alterações como essa deve ser muito fácil. Parece que você simplesmente não quer explorar essa rota, quando penso que há soluções a serem encontradas para todas as suas preocupações.
mellamokb

Respostas:

44

Tive o mesmo problema e finalmente consegui resolvê-lo usando SET NOEXEC .

IF not whatever
BEGIN
    SET NOEXEC ON; 
END

ALTER TABLE dbo.EMPLOYEE ADD COLUMN EMP_IS_ADMIN BIT NOT NULL
GO
UPDATE dbo.EMPLOYEE SET EMP_IS_ADMIN = whatever

SET NOEXEC OFF; 
Mina Jacob
fonte
2
Esta é uma otima soluçao!
Bazinga de
+1! Esta é a ÚNICA resposta prática até agora para uso em um SQLCMDscript de modo SS (ou seja, um script de implantação mestre) que chama (via :rcomando) outros scripts SS (ou seja, scripts de sub-implantação) com algumas dessas chamadas dentro de ifinstruções. As respostas de Oded, mellamokb e Andy Joiner de incluir todas essas declarações em execChamadas / begin- endnão são iniciais. Além disso, o método begin- endnão funcionará se houver uma createdeclaração (por exemplo, requer um explícito goantes dela). Mas, cara, "Santo duplo negativo, Batman!" ;)
Tom
Para facilitar a leitura (por exemplo, para ajudar a superar os duplos negativos e deixar mais claro que ele está simulando um bloco virtual if ), prefixaria o bloco com um -- If whatevercomentário, indentaria o bloco e pós-fixaria o bloco com um --end If whatevercomentário.
Tom
Você salvou meu bacon! Eu estava executando declarações de mesclagem e aqueles GO idiotas não gostam de estar dentro de um IF BEGIN END ELSE
Omzig
Hm, estou recebendo um erro na atualização de alguma forma depois que set noexec on foi executado? (erro de que o nome da coluna a ser atualizado é inválido) Executando em MSSQL 2014 no editor de consultas. Funciona bem se a condição se tornar falsa (portanto, noexec permanece desativado)
Jerry
43

GO não é SQL - é simplesmente um separador de lote usado em algumas ferramentas MS SQL.

Se você não usar isso, precisará garantir que as instruções sejam executadas separadamente - em lotes diferentes ou usando SQL dinâmico para a população (obrigado @gbn):

IF whatever
BEGIN
    ALTER TABLE dbo.EMPLOYEE ADD COLUMN EMP_IS_ADMIN BIT NOT NULL;

    EXEC ('UPDATE dbo.EMPLOYEE SET EMP_IS_ADMIN = whatever')
END
Oded
fonte
8
Sim eu entendo isso. Isso não responde à pergunta - eu preciso criar e atualizar uma coluna no mesmo IFbloco.
BlueRaja - Danny Pflughoeft
@Oded: Será que a ;ajuda aqui? - Você acabou de editar sua resposta: o)
Neil Knight
@Neil - esse é o meu pensamento, sim.
Oded de
;também não funciona - o analisador ainda está fornecendo "Nome de coluna inválido 'EMP_IS_ADMIN'."
BlueRaja - Danny Pflughoeft
Quando o lote é compilado, EMP_IS_ADMIN não existe. stackoverflow.com/questions/4855537/…
gbn
16

Você pode tentar sp_executesql, dividir o conteúdo entre cada GOinstrução em uma string separada para ser executada, conforme demonstrado no exemplo abaixo. Além disso, há uma variável @statementNo para rastrear qual instrução está sendo executada para facilitar a depuração onde ocorreu uma exceção. Os números das linhas serão relativos ao início do número da instrução relevante que causou o erro.

BEGIN TRAN

DECLARE @statementNo INT
BEGIN TRY
    IF 1=1
    BEGIN
        SET @statementNo = 1
        EXEC sp_executesql
            N'  ALTER TABLE dbo.EMPLOYEE
                    ADD COLUMN EMP_IS_ADMIN BIT NOT NULL'

        SET @statementNo = 2
        EXEC sp_executesql
            N'  UPDATE dbo.EMPLOYEE
                    SET EMP_IS_ADMIN = 1'

        SET @statementNo = 3
        EXEC sp_executesql
            N'  UPDATE dbo.EMPLOYEE
                    SET EMP_IS_ADMIN = 1x'
    END
END TRY
BEGIN CATCH
    PRINT 'Error occurred on line ' + cast(ERROR_LINE() as varchar(10)) 
       + ' of ' + 'statement # ' + cast(@statementNo as varchar(10)) 
       + ': ' + ERROR_MESSAGE()
    -- error occurred, so rollback the transaction
    ROLLBACK
END CATCH
-- if we were successful, we should still have a transaction, so commit it
IF @@TRANCOUNT > 0
    COMMIT

Você também pode executar facilmente instruções de várias linhas, conforme demonstrado no exemplo acima, simplesmente colocando-as entre aspas simples ( '). Não se esqueça de deixar de lado as aspas simples contidas na string com aspas simples duplas ( '') ao gerar os scripts.

mellamokb
fonte
Não acho que isso funcionaria para comandos divididos em várias linhas, certo?
BlueRaja - Danny Pflughoeft
@BlueRaja: Eu atualizei o exemplo para mostrar como funcionaria. Essas strings podem ser multilinhas, desde que as aspas simples (') contidas dentro dele sejam escapadas usando aspas simples duplas (' ')
mellamokb
1
@mellamokb: estritamente falando, apenas o UPDATE precisa de sp_executesql ... stackoverflow.com/questions/4855537/…
gbn
1
@gbn: Verdadeiro. Mas se você vai automatizar isso para centenas de declarações, será mais fácil apenas aplicá-lo cegamente em todas as declarações, em vez de decidir quando e onde você precisa.
mellamokb de
@gbn @mellamokb: Eu quis dizer declarações como SELECT * <newline> FROM whatever. Se eu executar cada linha com sua própria instrução EXEC, haverá falha. Ou você está sugerindo que eu interrompa a cada GOafirmação?
BlueRaja - Danny Pflughoeft
9

Por fim, consegui fazê-lo funcionar substituindo cada instância de GOem sua própria linha por

END
GO

---Automatic replacement of GO keyword, need to recheck IF conditional:
IF whatever
BEGIN

Isso é muito preferível a envolver cada grupo de instruções em uma string, mas ainda está longe do ideal. Se alguém encontrar uma solução melhor, poste e eu aceitarei.

BlueRaja - Danny Pflughoeft
fonte
6
Se a primeira condicional for "se esta coluna não existe", a primeira declaração no bloco é "adicionar esta coluna", então a segunda verificação da condicional encontrará a coluna e não executará a segunda declaração,
Damien_The_Unbeliever
@Damien: True; felizmente, isso nunca acontecerá no meu caso (a condicional é sempre uma verificação de um valor específico em uma tabela específica, que sempre é adicionada como a última instrução do IFbloco). Parece que não há uma boa maneira de fazer isso no SQL.
BlueRaja - Danny Pflughoeft
A set noexecresposta de Mina Jacob é a ÚNICA resposta prática até agora para uso em um SQLCMDscript de modo SS (ou seja, um script de implantação mestre) que chama (via :rcomando) outros scripts SS (ou seja, scripts de sub-implantação) com algumas dessas chamadas dentro de ifinstruções. As respostas de Oded, mellamokb e Andy Joiner de incluir todas essas declarações em execChamadas / begin- endnão são iniciais. Além disso, o método begin- endnão funcionará se houver uma createDeclaração (por exemplo, requer um explícito goantes dela).
Tom
8

Você pode incluir as instruções em BEGIN e END em vez de GO no meio

IF COL_LENGTH('Employees','EMP_IS_ADMIN') IS NULL --Column does not exist
BEGIN
    BEGIN
        ALTER TABLE dbo.Employees ADD EMP_IS_ADMIN BIT
    END

    BEGIN
        UPDATE EMPLOYEES SET EMP_IS_ADMIN = 0
    END
END

(Testado no banco de dados Northwind)

Editar: (Provavelmente testado em SQL2012)

Andy Joiner
fonte
1
Por favor, dê a razão para -1
Andy Joiner
1
Não sei por que foi rejeitado ... funciona como um encanto para mim.
Thorarin
10
Usando o SQL Server 2008 R2, isso não parece funcionar para mim, ainda recebo um erro 'Nome de coluna inválido' EMP_IS_ADMIN '.' na linha UPDATE.
MerickOWA
O batch BEGIN-END funcionou para mim usando o SQL Server 2016. IMO, esta é a sintaxe mais limpa.
Uber Schnoz
A set noexecresposta de Mina Jacob é a ÚNICA resposta prática até agora para uso em um SQLCMDscript de modo SS (ou seja, um script de implantação mestre) que chama (via :rcomando) outros scripts SS (ou seja, scripts de subdesenvolvimento) com algumas dessas chamadas dentro de ifinstruções. As respostas de Oded, mellamokb e Andy Joiner de incluir todas essas declarações em execChamadas / begin- endnão são iniciais. Além disso, o método begin- endnão funcionará se houver uma createDeclaração (por exemplo, requer um explícito goantes dela).
Tom
1

Você pode tentar esta solução:

if exists(
SELECT...
)
BEGIN
PRINT 'NOT RUN'
RETURN
END

--if upper code not true

ALTER...
GO
UPDATE...
GO
Luk
fonte
1
Não é muito útil se você tiver vários blocos if-else, um após o outro, certo?
Jerry
0

Eu tenho usado RAISERRORno passado para este

IF NOT whatever BEGIN
    RAISERROR('YOU''RE ALL SET, and sorry for the error!', 20, -1) WITH LOG
END

ALTER TABLE dbo.EMPLOYEE ADD COLUMN EMP_IS_ADMIN BIT NOT NULL
GO
UPDATE dbo.EMPLOYEE SET EMP_IS_ADMIN = whatever
kavun
fonte
-1

Você pode incorporar as instruções GOTOe LABELpara ignorar o código, deixando assim as GOpalavras - chave intactas.

Jim a
fonte
5
Parece que os LABELs não podem ser referenciados nas instruções GO, pois eles não estão no lote enviado para SQL
berkeleybross