在 TSQL 错误消息之后获取文件中的行号
Get line number in file following TSQL error message
考虑以下 sql 脚本
: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'.
当您在启用 SQLCMD 模式的 SQL Server Management Studio 中运行时,您会得到
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.
但是当您双击错误行时,查询编辑器将跳转到有问题的行。
Reported 第 2 行 表示与批次相关的行号。批次由 GO 语句分隔。我们想要得到真正的 第 9 行 答案。
我也尝试过 PowerShell 的 Invoke-Sqlcmd 但它更糟糕,因为它根本没有检测到此类错误 (Error detection from Powershell Invoke-Sqlcmd not always working?)。
是否有一种简单的方法可以将我们的 sql 脚本与一些帮助程序包装起来以获得所需的真实错误行?
UPD:我已经更改了错误脚本以确保它肯定会失败...
这是我想出的解决方案: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 行输出
以防万一我也在此处内联脚本。该脚本可能看起来有点矫枉过正,但这样写是为了完全支持所有 SQLCMD 脚本功能并正确处理事务
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 方法的好主意。
这是使用这种方法的版本:https://github.com/mnaoumov/Invoke-SqlcmdEx/blob/LINENO/Invoke-SqlcmdEx.ps1 它基本上在每个 GO[= 之后插入 LINENO [corresponding-line-number] 44=]声明。
但是如果我们考虑下面的脚本
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:CREATE DEFAULT、CREATE FUNCTION、CREATE PROCEDURE、CREATE RULE、CREATE SCHEMA、CREATE TRIGGER 和 CREATE VIEW。未提及 ALTER 语句,但我认为该规则也适用于它们
考虑以下 sql 脚本
: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'.
当您在启用 SQLCMD 模式的 SQL Server Management Studio 中运行时,您会得到
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.
但是当您双击错误行时,查询编辑器将跳转到有问题的行。
Reported 第 2 行 表示与批次相关的行号。批次由 GO 语句分隔。我们想要得到真正的 第 9 行 答案。
我也尝试过 PowerShell 的 Invoke-Sqlcmd 但它更糟糕,因为它根本没有检测到此类错误 (Error detection from Powershell Invoke-Sqlcmd not always working?)。
是否有一种简单的方法可以将我们的 sql 脚本与一些帮助程序包装起来以获得所需的真实错误行?
UPD:我已经更改了错误脚本以确保它肯定会失败...
这是我想出的解决方案: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 行输出
以防万一我也在此处内联脚本。该脚本可能看起来有点矫枉过正,但这样写是为了完全支持所有 SQLCMD 脚本功能并正确处理事务
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 方法的好主意。
这是使用这种方法的版本:https://github.com/mnaoumov/Invoke-SqlcmdEx/blob/LINENO/Invoke-SqlcmdEx.ps1 它基本上在每个 GO[= 之后插入 LINENO [corresponding-line-number] 44=]声明。
但是如果我们考虑下面的脚本
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:CREATE DEFAULT、CREATE FUNCTION、CREATE PROCEDURE、CREATE RULE、CREATE SCHEMA、CREATE TRIGGER 和 CREATE VIEW。未提及 ALTER 语句,但我认为该规则也适用于它们