TSQL - ¿Cómo usar GO dentro de un bloque BEGIN .. END?

96

Estoy generando un script para migrar automáticamente los cambios de varias bases de datos de desarrollo a la etapa de preparación / producción. Básicamente, se necesitan varios scripts de cambio y los fusiona en un solo script, envolviendo cada script en una IF whatever BEGIN ... ENDdeclaración.

Sin embargo, algunos de los scripts requieren una GOdeclaración para que, por ejemplo, el analizador de SQL conozca una nueva columna después de que se cree.

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

Sin embargo, una vez que envuelvo eso en un IFbloque:

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

Falla porque estoy enviando un BEGINmensaje sin coincidencia END. Sin embargo, si elimino el GO, vuelve a quejarse de una columna desconocida.

¿Hay alguna forma de crear y actualizar la misma columna dentro de un solo IFbloque?

BlueRaja - Danny Pflughoeft
fuente
2
Consulte stackoverflow.com/questions/4855537/ ... por favor
gbn
2
@gbn: Sí, me doy cuenta de por qué sucede esto (vea el segundo párrafo) ; pero no tengo idea de cómo solucionarlo, ¿ realmente necesito convertir cada consulta en un montón de cadenas?
BlueRaja - Danny Pflughoeft
@BlueRaja: ¿Cuál es la preocupación? Si funciona, eso es todo lo que importa al final del día. Si existe un problema comercial legítimo con la solución proporcionada, por favor expreselo. ¿Hay algo específicamente desconcertante en convertir cada consulta en un montón de cadenas?
mellamokb
1
@mellamokb: Sí, hay un problema; si la palabra GO se usa en cualquier otro contexto (como un comentario o una cadena), el script no funcionará. Además, perdemos los números de línea útiles en los mensajes de error en caso de que algo salga mal. ¿No hay forma de hacer esto con las transacciones? ¿O intentar / atrapar?
BlueRaja - Danny Pflughoeft
@BlueRaja: 1) Creo que GOdebe estar en una línea por sí solo, por lo que puede buscar solo ese caso, y no todas las instancias de la palabra GO. 2) Siempre puede registrar qué declaraciones se completaron correctamente. O puede envolver todo en un try / catch y usar sus propios números de línea usando alguna variable, como @lineNo, de la que realiza un seguimiento e informa sobre el error. Dado que los está generando automáticamente, realizar cambios como este debería ser muy sencillo. Parece que simplemente no quieres explorar esta ruta cuando creo que hay soluciones para todas tus preocupaciones.
mellamokb

Respuestas:

44

Tuve el mismo problema y finalmente logré resolverlo 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
fuente
2
¡Esta es una gran solución!
Bazinga
+1! Esta es la ÚNICA respuesta práctica hasta ahora para usar en un SQLCMDscript de modo SS (es decir, un script de implementación maestro) que llama (a través de un :rcomando) a otros scripts SS (es decir, scripts de subdespliegue) con algunas de esas llamadas dentro de ifdeclaraciones. Las Respuestas de Oded, Mellamokb y Andy Joiner de incluir todas esas Declaraciones en execLlamadas / begin- endno son para empezar. Además, el método begin- endno funcionará si hay una createinstrucción (por ejemplo, requiere un goprevio explícito ). Pero, hombre, "¡Santos dobles negativos, Batman!" ;)
Tom
Para facilitar la lectura (por ejemplo, para ayudar a superar los dobles negativos y dejar más claro que está simulando un bloque virtual if ), prefijaría el bloque con un -- If whatevercomentario, sangraría el bloque y fijaría el bloque con un --end If whatevercomentario.
Tom
¡Me salvaste el tocino! Estaba ejecutando declaraciones de fusión y a esos GO tontos no les gusta estar dentro de un IF BEGIN END ELSE
Omzig
Hm, recibo un error en la actualización de alguna manera después de que se haya ejecutado set noexec on? (error de que el nombre de la columna para actualizar no es válido) Se ejecuta en MSSQL 2014 en el editor de consultas. Funciona bien si la condición se vuelve falsa (por lo tanto, noexec permanece apagado)
Jerry
43

GO no es SQL, es simplemente un separador de lotes utilizado en algunas herramientas de MS SQL.

Si no lo usa, debe asegurarse de que las declaraciones se ejecuten por separado, ya sea en diferentes lotes o mediante el uso de SQL dinámico para la población (gracias @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
fuente
8
Sí, lo entiendo. Esto no responde a la pregunta: necesito crear y actualizar una columna en el mismo IFbloque.
BlueRaja - Danny Pflughoeft
@Oded: ¿Ayudaría ;aquí? - Acaba de editar su respuesta: o)
Neil Knight
@Neil: este es mi pensamiento, sí.
Oded
;tampoco funciona: el analizador todavía me da "Nombre de columna no válido 'EMP_IS_ADMIN'".
BlueRaja - Danny Pflughoeft
Cuando se compila el lote, EMP_IS_ADMIN no existe. stackoverflow.com/questions/4855537/…
gbn
16

Puede intentar sp_executesqldividir el contenido entre cada GOdeclaración en una cadena separada para que se ejecute, como se muestra en el siguiente ejemplo. Además, hay una variable @statementNo para rastrear qué declaración se está ejecutando para una fácil depuración donde ocurrió una excepción. Los números de línea estarán relacionados con el comienzo del número de declaración relevante que causó el error.

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

También puede ejecutar fácilmente declaraciones de varias líneas, como se demuestra en el ejemplo anterior, simplemente envolviéndolas entre comillas simples ( '). No olvide escapar las comillas simples contenidas dentro de la cadena con una comilla simple doble ( '') al generar los scripts.

mellamokb
fuente
No creo que esto funcione para comandos divididos en varias líneas, ¿verdad?
BlueRaja - Danny Pflughoeft
@BlueRaja: He actualizado el ejemplo para mostrar cómo funcionaría. Esas cadenas pueden ser de varias líneas, siempre que las comillas simples (') contenidas en el interior se
escapen
1
@mellamokb: estrictamente hablando, solo la ACTUALIZACIÓN necesita sp_executesql ... stackoverflow.com/questions/4855537/…
gbn
1
@gbn: Cierto. Pero si va a automatizar esto para cientos de declaraciones, será más fácil aplicarlo ciegamente en todas las declaraciones en lugar de decidir cuándo y dónde lo necesita.
mellamokb
@gbn @mellamokb: Me refería a declaraciones como SELECT * <newline> FROM whatever. Si ejecuto cada línea con su propia declaración EXEC, eso se romperá. ¿O estás sugiriendo que rompa con cada GOdeclaración?
BlueRaja - Danny Pflughoeft
9

Finalmente logré que funcionara reemplazando cada instancia de GOen su propia línea con

END
GO

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

Esto es muy preferible a envolver cada grupo de declaraciones en una cadena, pero aún está lejos de ser ideal. Si alguien encuentra una solución mejor, publíquela y la aceptaré.

BlueRaja - Danny Pflughoeft
fuente
6
Si el primer condicional es "si esta columna no existe", la primera declaración en el bloque es "agregar esta columna", luego la segunda verificación del condicional encontrará la columna y no ejecutará la segunda declaración,
Damien_The_Unbeliever
@Damien: Cierto; afortunadamente, eso nunca sucederá en mi caso (el condicional siempre es una verificación de un valor específico en una tabla específica, que siempre se agrega como la última declaración del IFbloque). Parece que simplemente no hay una buena manera de hacer esto en SQL.
BlueRaja - Danny Pflughoeft
La set noexecrespuesta de Mina Jacob es la ÚNICA respuesta práctica hasta ahora para usar en un SQLCMDscript en modo SS (es decir, un script de implementación maestro) que llama (a través de un :rcomando) a otros scripts SS (es decir, scripts de subdespliegue) con algunas de esas llamadas dentro de ifdeclaraciones. Las Respuestas de Oded, Mellamokb y Andy Joiner de incluir todas esas Declaraciones en execLlamadas / begin- endno son para empezar. Además, el método begin- endno funcionará si hay una createinstrucción (por ejemplo, requiere un goprevio explícito ).
Tom
8

Puede incluir las declaraciones en BEGIN y END en lugar del GO entre

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

(Probado en la base de datos Northwind)

Editar: (Probablemente probado en SQL2012)

Andy Joiner
fuente
1
Por favor, explique la razón de -1
Andy Joiner
1
No sé por qué fue rechazado ... funciona como un encanto para mí.
Thorarin
10
Al usar SQL Server 2008 R2, esto no parece funcionar para mí, todavía obtengo un error 'Nombre de columna no válido' EMP_IS_ADMIN '. en la línea UPDATE.
MerickOWA
El procesamiento por lotes BEGIN-END funcionó para mí usando SQL Server 2016. En mi opinión, esta es la sintaxis más limpia.
Uber Schnoz
La set noexecrespuesta de Mina Jacob es la ÚNICA respuesta práctica hasta ahora para usar en un SQLCMDscript de modo SS (es decir, un script de implementación maestro) que llama (a través de un :rcomando) a otros scripts SS (es decir, scripts de subdespliegue) con algunas de esas llamadas dentro de ifdeclaraciones. Las Respuestas de Oded, Mellamokb y Andy Joiner de incluir todas esas Declaraciones en execLlamadas / begin- endno son para empezar. Además, el método begin- endno funcionará si hay una createinstrucción (por ejemplo, requiere un goprevio explícito ).
Tom
1

Puede probar esta solución:

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

--if upper code not true

ALTER...
GO
UPDATE...
GO
Luk
fuente
1
No es muy útil si tiene varios bloques if-else uno tras otro, ¿verdad?
Jerry
0

He usado RAISERRORen el pasado para esto

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
fuente
-1

Puede incorporar declaraciones GOTOy LABELpara omitir el código, dejando las GOpalabras clave intactas.

Jim a
fuente
5
Parece que las etiquetas no pueden ser referenciados a través de declaraciones GO, ya que no están en el lote enviado a SQL
berkeleybross