Получить номер строки в файле после сообщения об ошибке TSQL

Рассмотрим следующий sql script

:ON ERROR EXIT

PRINT 'Line 3'
GO

PRINT 'Line 6'
GO

SELECT * FROM NonExistingTable
GO

PRINT 'Line 12'
GO

При запуске с SQLCMD

> sqlcmd -i MyScript.sql
Line 3
Line 6
Msg 208, Level 16, State 1, Server MyServer, Line 2
Invalid object name 'NonExistingTable'.

При запуске в SQL Server Management Studio с включенным режимом SQLCMD вы получаете

Line 3
Line 6
Msg 208, Level 16, State 1, Server MyServer, Line 2
Invalid object name 'NonExistingTable'.
** An error was encountered during execution of batch. Exiting.

Но когда вы дважды щелкните по строке ошибки, редактор запросов перейдет к проблемной строке.

Сообщается Строка 2 означает номер строки относительно партии. Пакеты разделяются операцией GO. Мы хотим получить реальный ответ Line 9.

Я также пробовал PowerShell Invoke-Sqlcmd, но это еще хуже, поскольку он вообще не обнаруживает такие ошибки (Обнаружение ошибок из Powershell Invoke -Sqlcmd не всегда работает?).

Есть ли простой способ обернуть наш sql script некоторыми помощниками, чтобы получить желаемые реальные строки ошибок?

UPD: я изменил ошибку script, чтобы убедиться, что она не сработает наверняка...

Ответы

Ответ 1

Вот решение, с которым я столкнулся: https://github.com/mnaoumov/Invoke-SqlcmdEx

А теперь

> .\Invoke-SqlcmdEx.ps1 -InputFile .\MyScript.sql
Line 3
Line 6
Msg 208, Level 16, State 1, Server MyServer, Script .\MyScript.ps1, Line 9
Invalid object name 'NonExistingTable'.

sqlcmd failed for script .\MyScript.ps1 with exit code 1
At C:\Dev\Invoke-SqlcmdEx\Invoke-SqlcmdEx.ps1:77 char:18
+             throw <<<<  "sqlcmd failed for script $InputFile with exit code $LASTEXITCODE"
    + CategoryInfo          : OperationStopped: (sqlcmd failed f...ith exit code 1:String) [], RuntimeException
    + FullyQualifiedErrorId : sqlcmd failed for script .\MyScript.ps1 with exit code 1

И он имеет правильную строку 9 output

На всякий случай я включил script здесь. script может показаться излишним, но он написан так, чтобы полностью поддерживать все функции SQLCMD script и правильно обрабатывать транзакции

Invoke-SqlcmdEx.ps1

#requires -version 2.0

[CmdletBinding()]
param
(
    [string] $ServerInstance = ".",
    [string] $Database = "master",
    [string] $User,
    [string] $Password,

    [Parameter(Mandatory = $true)]
    [string] $InputFile
)

$script:ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
function PSScriptRoot { $MyInvocation.ScriptName | Split-Path }

trap { throw $Error[0] }

function Main
{
    if (-not (Get-Command -Name sqlcmd.exe -ErrorAction SilentlyContinue))
    {
        throw "sqlcmd.exe not found"
    }

    $scriptLines = Get-Content -Path $InputFile
    $extendedLines = @()

    $offset = 0
    foreach ($line in $scriptLines)
    {
        $offset++
        if ($line -match "^\s*GO\s*$")
        {
            $extendedLines += `
                @(
                    "GO",
                    "PRINT '~~~ Invoke-SqlcmdEx Helper - Offset $offset'"
                )
        }

        $extendedLines += $line
    }

    $tempFile = [System.IO.Path]::GetTempFileName()

    try
    {
        $extendedLines > $tempFile

        $sqlCmdArguments = Get-SqlCmdArguments

        $ErrorActionPreference = "Continue"
        $result = sqlcmd.exe $sqlCmdArguments -i $tempFile 2>&1
        $ErrorActionPreference = "Stop"

        $offset = 0
        $result | ForEach-Object -Process `
            {
                $line = "$_"
                if ($line -match "~~~ Invoke-SqlcmdEx Helper - Offset (?<Offset>\d+)")
                {
                    $offset = [int] $Matches.Offset
                }
                elseif (($_ -is [System.Management.Automation.ErrorRecord]) -and ($line -match "Line (?<ErrorLine>\d+)$"))
                {
                    $errorLine = [int] $Matches.ErrorLine
                    $realErrorLine = $offset + $errorLine
                    $line -replace "Line \d+$", "Script $InputFile, Line $realErrorLine"
                }
                else
                {
                    $line
                }
            }

        if ($LASTEXITCODE -ne 0)
        {
            throw "sqlcmd failed for script $InputFile with exit code $LASTEXITCODE"
        }
    }
    finally
    {
        Remove-Item -Path $tempFile -ErrorAction SilentlyContinue
    }
}

function Get-SqlCmdArguments
{
    $sqlCmdArguments = `
        @(
            "-S",
            $ServerInstance,
            "-d",
            $Database,
            "-b",
            "-r",
            0
        )

    if ($User)
    {
        $sqlCmdArguments += `
            @(
                "-U",
                $User,
                "-P",
                $Password
            )
    }
    else
    {
        $sqlCmdArguments += "-E"
    }

    $sqlCmdArguments
}

Main

UPD: @MartinSmith обеспечил опрятную идею использования LINENO aproach.

Вот версия, которая использует этот подход: https://github.com/mnaoumov/Invoke-SqlcmdEx/blob/LINENO/Invoke-SqlcmdEx.ps1 В основном он вставляет LINENO [номер соответствующей строки] после каждого оператора GO.

Но если мы рассмотрим следующий script

IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID('dbo.MyFunction') AND type = 'FN')
    EXEC sp_executesql N'CREATE FUNCTION dbo.MyFunction() RETURNS int AS BEGIN RETURN 0 END'
GO
LINENO 3

ALTER FUNCTION dbo.MyFunction()
RETURNS int
AS
BEGIN
    RETURN 42
END
GO

Сбой будет

> sqlcmd -i MyScript.sql
Msg 111, Level 15, State 1, Server MyServer, Line 5
'ALTER FUNCTION' must be the first statement in a query batch.
Msg 178, Level 15, State 1, Server MyServer, Line 9
A RETURN statement with a return value cannot be used in this context.

Таким образом, подход LINENO не будет работать для операторов, которые должны быть первыми в пакете запросов. Вот список таких утверждений: http://msdn.microsoft.com/en-us/library/ms175502.aspx: СОЗДАТЬ ПО УМОЛЧАНИЮ, СОЗДАТЬ ФУНКЦИЮ, СОЗДАТЬ ПРОЦЕДУРУ, СОЗДАТЬ ПРАВИЛО, СОЗДАТЬ СХЕМА, СОЗДАТЬ ТРИГГЕР и СОЗДАТЬ ВИД. Заявления ALTER не упоминаются, но я думаю, что правило применяется и для них, а