16

Consider the following sql script

:ON ERROR EXIT

PRINT 'Line 3'
GO

PRINT 'Line 6'
GO

SELECT * FROM NonExistingTable
GO

PRINT 'Line 12'
GO

When you run with SQLCMD

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

When you run in SQL Server Management Studio with SQLCMD Mode enabled you get

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.

But when you double click on the error line the query editor will jump to the problematic line.

Reported Line 2 means a line number relative to the batch. Batches are separated by GO statement. We want to get a real Line 9 answer.

I've also tried PowerShell's Invoke-Sqlcmd but it is even worse, since it does not detect such errors at all (Error detection from Powershell Invoke-Sqlcmd not always working?).

Is there a simple way to wrap our sql script with some helpers to get the desired real error lines?

UPD: I've changed the error script to make sure it would fail for sure...

Community
  • 1
  • 1
mnaoumov
  • 2,146
  • 2
  • 22
  • 31
  • What version of SQL Server are you using? I'm using SQL 2014 ENT and I got the right line number, I even tried a SQL 2012 ENT db from SQL 2014 management studio, and that worked as well – Daniel Gimenez Jan 05 '15 at 20:29
  • Interesting. I am trying on SQL Server 2008 R2 – mnaoumov Jan 05 '15 at 20:33
  • I've got super wierdness: If I connect to a SQL DEV 2012 machine from SQL Enterprise Mgr 2014 I don't get the error, if I do it from SQL Enterprise Mgr 2012 I do! Why would Enterprise Manager be the difference? (I triple checked that I was in SQLCMD mode.) – Daniel Gimenez Jan 05 '15 at 20:37
  • I've just checked on SQL DEV 2012, it is reporting the wrong line from both SSMS and from SQLCMD.exe. – mnaoumov Jan 05 '15 at 20:38
  • If I use SQLCMD and connect to SQL 2014 ENT I get line 2. So somehow SQL 2014 management studio is doing something different to get the right line number. – Daniel Gimenez Jan 05 '15 at 21:10
  • Perhaps you could keep track of the current batch in [Context_Info](http://msdn.microsoft.com/en-us/library/ms180125.aspx) (which maintains its value across batches on a connection) and then use an error handler to output both your batch identification and SQL Server's line number. (The batch identification could be your line number for the first line in the batch.) – HABO Jan 05 '15 at 21:32
  • 3
    I can't find documentation on it, but IIRC this issue was fixed in SSMS 2014. The fix will not be back-ported to 2012 never mind 2008 or 2008 R2, and I don't think there will be any fix possible for `sqlcmd` either. – Aaron Bertrand Jan 05 '15 at 21:32
  • 1
    [There is a way of doing it if you can pre-process the script probably](http://stackoverflow.com/q/4054511/73226) – Martin Smith Jan 06 '15 at 13:13
  • @MartinSmith, thanks for your idea, but it is not fully working, unfortunately. It is not enough space to explain it in the comment so I update my answer to address your idea – mnaoumov Jan 06 '15 at 18:32
  • @mnaoumov - Thanks. Will be interested to see what the issues are. – Martin Smith Jan 06 '15 at 18:35

1 Answers1

3

Here is the solution I came up with: https://github.com/mnaoumov/Invoke-SqlcmdEx

And now

> .\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

And it has a proper Line 9 output

Just in case I inline the script here as well. The script may look like an overkill but it is written like that in order to fully support all SQLCMD script features and deal correctly with transactions

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 provided a neat idea to use LINENO aproach.

Here is the version which uses this approach: https://github.com/mnaoumov/Invoke-SqlcmdEx/blob/LINENO/Invoke-SqlcmdEx.ps1 It basically inserts LINENO [corresponding-line-number] after each GO statement.

But if we consider the following 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

It will fail with

> 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.

So LINENO approach won't work for the statements that have to be the first in a query batch. Here is the list of such statements: http://msdn.microsoft.com/en-us/library/ms175502.aspx: CREATE DEFAULT, CREATE FUNCTION, CREATE PROCEDURE, CREATE RULE, CREATE SCHEMA, CREATE TRIGGER, and CREATE VIEW. ALTER statements are not mentioned but I think the rule is applied for them as well

mnaoumov
  • 2,146
  • 2
  • 22
  • 31
  • 1
    This was very useful, but as we had a very large SQL input file to debug (4,3 GB), the script struggled a bit to read the contents of it. For anyone else with the same scenario, split your .sql into smaller chunks. – Eirik H Oct 25 '16 at 08:31
  • 1
    It works well but the added PRINT statement messes with any @@ROWCOUNT used after GO. – pdube Mar 28 '21 at 13:49