16

I'm building a custom db deployment utility, I need to read text files containing sql scripts and execute them against the database.

Pretty easy stuff, so far so good.

However I've encountered a snag, the contents of the file are read successfully and entirely, but once passed into the SqlCommand and then executed with SqlCommand.ExecuteNonQuery only part of the script is executed.

I fired up Profiler and confirmed that my code is not passing all of the script.

    private void ExecuteScript(string cmd, SqlConnection sqlConn, SqlTransaction trans)
    {

        SqlCommand sqlCmd = new SqlCommand(cmd, sqlConn, trans);
        sqlCmd.CommandType = CommandType.Text;
        sqlCmd.CommandTimeout = 9000000; // for testing
        sqlCmd.ExecuteNonQuery();

    }

    // I call it like this, readDMLScript contains 543 lines of T-SQL
    string readDMLScript = ReadFile(dmlFile);
    ExecuteScript(readDMLScript, sqlConn, trans);
skaffman
  • 398,947
  • 96
  • 818
  • 769
H. Abraham Chavez
  • 828
  • 2
  • 8
  • 20
  • 1
    What character is the script truncated at? – MikeWyatt Mar 16 '10 at 17:21
  • 1
    how does the `ReadFile` method work? Are you 200% sure it's not skipping a few characters, maybe?? Why not just use `System.IO.File.ReadAllText(filename)` ?? – marc_s Mar 16 '10 at 17:24
  • how *much* text are you reading from the file, in bytes? – marc_s Mar 16 '10 at 17:32
  • Thanks for the help Marc, I was using a Stream reader. I'll switch to System.IO.File.ReadAllText(filename) for the sake of clarity. Now onto the issue, I suspected that the script was messed up because when executing it using the SqlCommand exceptions relating to the syntax of the script were raise while it ran fine in Sql Management Studio. It looks like I had some head trauma this morning it turns out that the script is not truncated. – H. Abraham Chavez Mar 16 '10 at 17:53
  • The exceptions are still holding me back though. Do you know why something like this : System.Data.SqlClient.SqlException: Incorrect syntax near 'GO'. Incorrect syntax near 'GO'. Incorrect syntax near 'GO'. Would be visible under the SqlCommand but not under Sql Management Studio when executing a script? – H. Abraham Chavez Mar 16 '10 at 17:53

6 Answers6

35

Yep, everyone hits this snag the first time they start sending the contents of SQL script files to the database.

GO is not a T-SQL command. It's the end-of-batch marker recognised by all the Microsoft interactive SQL tools (Management Studio, isql, osql). In order to handle it, you will have to write your own parser to break out every block of text in the file between GO statements and feed them to the database as separate commands.

How you implement your parser is up to you. It could be simple (read in each line at a time, detect lines that consist of nothing but GO and whitespace) or complex (tokenising all the statements and working out whether a GO is a genuine statement or a bit of text inside a string or multi-line comment).

Personally I went with the first option. It handles 99% of all SQL files you are ever likely to encounter with no fuss. If you want to go the whole hog and write a tokeniser, I'm sure lots of people have done one already, just Google for it.

Example:

using(var reader = new SqlBatchReader(new StreamReader(dmlFile))) {
    string batch;
    while((batch = reader.ReadBatch()) != null) {
        var cmd = new SqlCommand(batch, conn, trans) { CommandType = CommandType.Text };
        cmd.ExecuteNonQuery();
    }
}

class SqlBatchReader : IDisposable {
    private TextReader _reader;
    public SqlBatchReader(TextReader reader) {
        _reader = reader;
    }
    /// <summary>
    /// Return the next command batch in the file, or null if end-of-file reached.
    /// </summary>
    public string ReadBatch() {
        // TODO: Implement your parsing logic here.
    }
}
Christian Hayter
  • 30,581
  • 6
  • 72
  • 99
  • Unless your script file starts with 'SET IDENTITY_INSERT ON'. Is there no good solution to export data to a file for .NET to then import? – MStodd May 29 '15 at 15:59
  • @MStodd: `SET IDENTITY_INSERT` is intended for use in data import scripts, so it is a good solution. Personally, though, I would use SSIS to import data. – Christian Hayter Jun 01 '15 at 07:36
  • The problem I have now is if I export a script from SSMS starting with 'SET IDENTITY_INSERT ON', it won't be enough to use your code will it? Won't I need to execute that command before every insert? – MStodd Jun 01 '15 at 17:00
  • @MStodd: `SET IDENTITY_INSERT` stays set until you turn it off, you only have to set it once. What problem are you having with your script exactly? – Christian Hayter Jun 02 '15 at 07:19
  • I'm having this issue: http://stackoverflow.com/questions/7714812/why-doesnt-set-identity-insert-off-work-for-me-in-ef-4-1 – MStodd Jun 02 '15 at 18:27
  • @MStodd: I see. However, that will only be an issue if the connection is reset between commands. If you keep it open, as you would with my code above or with SSIS, then it should work fine. – Christian Hayter Jun 03 '15 at 08:35
5

I found the code below while searching for an answer to this issue:

http://blogs.msdn.com/b/onoj/archive/2008/02/26/incorrect-syntax-near-go-sqlcommand-executenonquery.aspx

Pros: It's short and simple to understand and worked perfectly for my needs.

Cons: It is less efficient than Stream based solutions and is case sensitive (i.e "GO" not "go").

string[] commands = sql.Split(new string[]{"GO\r\n", "GO ", "GO\t"}, StringSplitOptions.RemoveEmptyEntries );
foreach (string c in commands)
{
    var command = new SqlCommand(c, masterConnection);
    command.ExecuteNonQuery();
}
Bill Staib
  • 83
  • 2
  • 4
2

ExecuteNonQuery in SMO works with batches:

http://msdn.microsoft.com/en-us/library/microsoft.sqlserver.management.smo.database.executenonquery.aspx

Adam Butler
  • 3,023
  • 5
  • 35
  • 40
1

Answer based on comments under the original post:

GO is a marker for Management Studio / osql / isql. It tells to send a batch of commands to SQL Server. In your utility, you should split the input data using GO as a delimiter and send each element individually (without the GO command)

Timores
  • 14,439
  • 3
  • 46
  • 46
0

This is what we use :)

public static class ExtensionMethodsSqlCommand
{
    #region Public

    private static bool IsGo(string psCommandLine)
    {
        if (psCommandLine == null)
            return false;
        psCommandLine = psCommandLine.Trim();
        if (string.Compare(psCommandLine, "GO", StringComparison.OrdinalIgnoreCase) == 0)
            return true;
        if (psCommandLine.StartsWith("GO", StringComparison.OrdinalIgnoreCase))
        {
            psCommandLine = (psCommandLine + "--").Substring(2).Trim();
            if (psCommandLine.StartsWith("--"))
                return true;
        }
        return false;
    }

    [System.Diagnostics.DebuggerHidden]
    public static void ExecuteNonQueryWithGos(this SqlCommand poSqlCommand)
    {
        string sCommandLong = poSqlCommand.CommandText;
        using (StringReader oStringReader = new StringReader(sCommandLong))
        {
            string sCommandLine;
            string sCommandShort = string.Empty;
            while ((sCommandLine = oStringReader.ReadLine()) != null)
                if (ExtensionMethodsSqlCommand.IsGo(sCommandLine))
                {
                    if (sCommandShort.IsNullOrWhiteSpace() == false)
                    {
                        if ((poSqlCommand.Connection.State & ConnectionState.Open) == 0)
                            poSqlCommand.Connection.Open();
                        using (SqlCommand oSqlCommand = new SqlCommand(sCommandShort, poSqlCommand.Connection))
                            oSqlCommand.ExecuteNonQuery();
                    }
                    sCommandShort = string.Empty;
                }
                else
                    sCommandShort += sCommandLine + "\r\n";
        }
    }

    #endregion Public
}
  • Why don't you add some description of the code example you have posted? It's easier to read the description and understand that solution work or doesn't work for you than spend some time to analyze what the code is doing. – Artemix Jul 22 '13 at 09:48
0

I ended up writing an implementation of a StringReader to do this.

It handles:

  1. Skipping past GO contained in dash dash comments
  2. Skipping past GO contained in slash star comments
  3. Skipping past GO contained in literals (i.e single quotes)
  4. Skipping past GO contained in column names etc.

Therefore it will only detect the keyword GO when used as a batch seperator. This means it splits the SQL text correctly.

It also handles if you have appended a sql terminator (semicolon) to the word GO

You can find the code for it here:

You use it like so:

using (var reader = new SqlCommandReader(scriptContents))
       {
            var commands = new List<string>();
            reader.ReadAllCommands(c => commands.Add(c));
            // commands now contains each seperated sql batch.
        }
Darrell
  • 1,905
  • 23
  • 31