7

I am trying to build a program which execute sql statements in batch with error handling (therefore I am not using SMO).

the problem is that GO is not a part of SQL and when using .NET to execute the statements it ends up with an error (SMO handles it but does not give any indication whether the execution failed).

string statements = File.ReadAllText("c:\\test.sql");
string[] splitted = statements.split("GO");

using the above lines do not solve my problem due to the fact that the GO keyword can also come inside a comment (I don't want to remove comments from the statements) and comments can come inside /**/ or after the two dashes --
for example I wouldn't like the following code to be parsed:

/*
GO
*/

(ofc i googled it but there was no solution over there)

Nadav
  • 2,589
  • 9
  • 44
  • 63
  • 1
    You can check this answer if it satisfies your requirements http://stackoverflow.com/questions/12431591/how-do-i-run-large-sql-scripts-that-contain-many-keywords-including-go-using/12431708#12431708 – Steve Aug 29 '14 at 07:54
  • Is every GO statement on a new line? – shree.pat18 Aug 29 '14 at 07:56
  • the linked question's answer does not solve the situation where Go appears inside /**/ for examle: /* GO */ where the first line is /* the second one is GO and the third line is */ – Nadav Aug 29 '14 at 07:59
  • I see, then you are in need of a more complex algorithm. Parsing and all the hassle. You should weight your options (and time constraints) against the complexity required – Steve Aug 29 '14 at 08:02
  • 2
    You're also ignoring the fact the `GO` can easily appear within strings. – Damien_The_Unbeliever Aug 29 '14 at 08:03
  • @Damien_The_Unbeliever thanks for your comment this is another issue which i need to consider – Nadav Aug 29 '14 at 08:13
  • I think you need to update your question. The answers given will work just fine for what you are doing, as far as anyone can understand it from your question. – Andrew Aug 29 '14 at 08:14
  • FWIW, Microsoft's own Query Analyzer [behaves the same way as your program](http://i.stack.imgur.com/RFppJ.png). –  Aug 29 '14 at 09:14
  • Possible duplicate of [Execute a large SQL script (with GO commands)](http://stackoverflow.com/questions/40814/execute-a-large-sql-script-with-go-commands) – Michael Freidgeim Jun 09 '16 at 02:17

4 Answers4

11

ScriptDom

The easiest solution (and the most robust) is to use a T-SQL parser. The good news is that you don't have to write it, just add reference to:

  • Microsoft.Data.Schema.ScriptDom
  • Microsoft.Data.Schema.ScriptDom.Sql

Then use the code:

static void Main(string[] args)
{
    string sql = @"
/* 
GO
*/ 
SELECT * FROM [table]

GO

SELECT * FROM [table]
SELECT * FROM [table]

GO

SELECT * FROM [table]";

    string[] errors;
    var scriptFragment = Parse(sql, SqlVersion.Sql100, true, out errors);
    if (errors != null)
    {
        foreach (string error in errors)
        {
            Console.WriteLine(error);
            return;
        }
    }

    TSqlScript tsqlScriptFragment = scriptFragment as TSqlScript;
    if (tsqlScriptFragment == null)
        return;

    var options = new SqlScriptGeneratorOptions { SqlVersion = SqlVersion.Sql100, KeywordCasing = KeywordCasing.PascalCase };

    foreach (TSqlBatch batch in tsqlScriptFragment.Batches)
    {
        Console.WriteLine("--");
        string batchText = ToScript(batch, options);
        Console.WriteLine(batchText);                
    }
}

public static TSqlParser GetParser(SqlVersion level, bool quotedIdentifiers)
{
    switch (level)
    {
        case SqlVersion.Sql80:
            return new TSql80Parser(quotedIdentifiers);
        case SqlVersion.Sql90:
            return new TSql90Parser(quotedIdentifiers);
        case SqlVersion.Sql100:
            return new TSql100Parser(quotedIdentifiers);
        case SqlVersion.SqlAzure:
            return new TSqlAzureParser(quotedIdentifiers);
        default:
            throw new ArgumentOutOfRangeException("level");
    }
}

public static IScriptFragment Parse(string sql, SqlVersion level, bool quotedIndentifiers, out string[] errors)
{
    errors = null;
    if (string.IsNullOrWhiteSpace(sql)) return null;
    sql = sql.Trim();
    IScriptFragment scriptFragment;
    IList<ParseError> errorlist;
    using (var sr = new StringReader(sql))
    {
        scriptFragment = GetParser(level, quotedIndentifiers).Parse(sr, out errorlist);
    }
    if (errorlist != null && errorlist.Count > 0)
    {
        errors = errorlist.Select(e => string.Format("Column {0}, Identifier {1}, Line {2}, Offset {3}",
                                                        e.Column, e.Identifier, e.Line, e.Offset) +
                                            Environment.NewLine + e.Message).ToArray();
        return null;
    }
    return scriptFragment;
}

public static SqlScriptGenerator GetScripter(SqlScriptGeneratorOptions options)
{
    if (options == null) return null;
    SqlScriptGenerator generator;
    switch (options.SqlVersion)
    {
        case SqlVersion.Sql80:
            generator = new Sql80ScriptGenerator(options);
            break;
        case SqlVersion.Sql90:
            generator = new Sql90ScriptGenerator(options);
            break;
        case SqlVersion.Sql100:
            generator = new Sql100ScriptGenerator(options);
            break;
        case SqlVersion.SqlAzure:
            generator = new SqlAzureScriptGenerator(options);
            break;
        default:
            throw new ArgumentOutOfRangeException();
    }
    return generator;
}

public static string ToScript(IScriptFragment scriptFragment, SqlScriptGeneratorOptions options)
{
    var scripter = GetScripter(options);
    if (scripter == null) return string.Empty;
    string script;
    scripter.GenerateScript(scriptFragment, out script);
    return script;
}

SQL Server Management Objects

Add references to:

  • Microsoft.SqlServer.Smo
  • Microsoft.SqlServer.ConnectionInfo
  • Microsoft.SqlServer.Management.Sdk.Sfc

You can then use this code:

using (SqlConnection connection = new SqlConnection("Server=(local);Database=Sample;Trusted_Connection=True;"))
{
    ServerConnection svrConnection = new ServerConnection(connection);
    Server server = new Server(svrConnection);
    server.ConnectionContext.ExecuteNonQuery(script);
}

CodeFluent Runtime

CodeFluent Runtime Database has a small sql file parser. It does not handle complex cases but for instance comments are supported.

using (StatementReader statementReader = new CodeFluent.Runtime.Database.Management.StatementReader("GO", Environment.NewLine, inputStream))
{
    Statement statement;
    while ((statement = statementReader.Read(StatementReaderOptions.Default)) != null)
    {
        Console.WriteLine("-- ");
        Console.WriteLine(statement.Command);
    }
}

Or much simplier

new CodeFluent.Runtime.Database.Management.SqlServer.Database("connection string")
      .RunScript("path", StatementReaderOptions.Default);
meziantou
  • 20,589
  • 7
  • 64
  • 83
  • Cool. But take another quick look at your switch statement... `Sql80` => `100` and `100` => `80`? Is that meant to be that way – Andrew Aug 29 '14 at 08:51
  • It's a cool solution. I might have tried it before, it looks familiar. Maybe not used it because my generated files were sane (e.g. SSMS generated.) For others, what do you need to install to have these libraries available, are they in the GAC, etc. – Andrew Aug 29 '14 at 09:01
  • Librairies are in the GAC or can be found in "C:\Program Files (x86)\Microsoft Visual Studio 10.0\VSTSDB" – meziantou Aug 29 '14 at 09:03
  • it seems promising but using your solution seems to delete comments – Nadav Aug 29 '14 at 09:34
  • Ah... thats why... I think the dependencies made it heavy to deploy to boxes without a VS.Net/VSTS setup (e.g. a web farm :D). And we didn't end up needing it, so there is that too. Thanks for the info. – Andrew Aug 29 '14 at 09:36
  • @NadavStern Not sure why thats an issue. He wants to run the script, right, not edit it? So he just wouldn't save the changes. – Andrew Aug 29 '14 at 09:37
  • 1
    the scripts that should run can contain store procedures, using alter with this solution would result with all of the comments being removed from the store procedures – Nadav Aug 29 '14 at 09:45
0

ScriptDom (updated 2023)

The edit queue for the answer by @meziantou is full (and at a size of 500 suggestions it doesn't seem that anybody with edit priviledge is ever going to go through it), so I'm adding this updated answer as a community wiki. ScriptDom now lives at nuget, github

ScriptDom is a T-SQL parser with support for batch files. Just add a reference to the nuget package Microsoft.SqlServer.TransactSql.ScriptDom. Example:

using Microsoft.SqlServer.TransactSql.ScriptDom;

namespace ScriptDomTest;

internal class Program
{
    static void Main(string[] args)
    {
        string sql = @"
    /* 
    GO
    */ 
    SELECT * FROM [table]

    GO

    SELECT * FROM [table]
    SELECT * FROM [table]

    GO

    SELECT * FROM [table]";

        string[]? errors;
        var scriptFragment = Parse(sql, SqlVersion.Sql100, true, out errors);
        if (errors != null)
        {
            foreach (string error in errors)
            {
                Console.WriteLine(error);
                return;
            }
        }

        TSqlScript? tsqlScriptFragment = scriptFragment as TSqlScript;
        if (tsqlScriptFragment == null)
            return;

        var options = new SqlScriptGeneratorOptions { SqlVersion = SqlVersion.Sql100, KeywordCasing = KeywordCasing.PascalCase };

        foreach (TSqlBatch batch in tsqlScriptFragment.Batches)
        {
            Console.WriteLine("--");
            string batchText = ToScript(batch, options);
            Console.WriteLine(batchText);                
        }
    }

    public static TSqlParser GetParser(SqlVersion level, bool quotedIdentifiers)
    {
        return level switch
        {
            SqlVersion.Sql80 => new TSql80Parser(quotedIdentifiers),
            SqlVersion.Sql90 => new TSql90Parser(quotedIdentifiers),
            SqlVersion.Sql100 => new TSql100Parser(quotedIdentifiers),
            SqlVersion.Sql110 => new TSql110Parser(quotedIdentifiers),
            SqlVersion.Sql120 => new TSql120Parser(quotedIdentifiers),
            SqlVersion.Sql130 => new TSql130Parser(quotedIdentifiers),
            SqlVersion.Sql140 => new TSql140Parser(quotedIdentifiers),
            SqlVersion.Sql150 => new TSql150Parser(quotedIdentifiers),
            SqlVersion.Sql160 => new TSql160Parser(quotedIdentifiers),
            _ => throw new ArgumentOutOfRangeException(nameof(level)),
        };
    }

    public static TSqlFragment? Parse(string sql, SqlVersion level, bool quotedIndentifiers, out string[]? errors)
    {
        errors = null;
        if (string.IsNullOrWhiteSpace(sql)) return null;
        sql = sql.Trim();
        TSqlFragment scriptFragment;
        IList<ParseError> errorlist;
        using (var sr = new StringReader(sql))
        {
            scriptFragment = GetParser(level, quotedIndentifiers).Parse(sr, out errorlist);
        }
        if (errorlist != null && errorlist.Count > 0)
        {
            errors = errorlist.Select(e => string.Format("Column {0}, Number {1}, Line {2}, Offset {3}",
                                                            e.Column, e.Number, e.Line, e.Offset) +
                                                Environment.NewLine + e.Message).ToArray();
            return null;
        }
        return scriptFragment;
    }

    public static SqlScriptGenerator? GetScripter(SqlScriptGeneratorOptions options)
    {
        if (options == null) return null;
        return options.SqlVersion switch
        {
            SqlVersion.Sql80 => new Sql80ScriptGenerator(options),
            SqlVersion.Sql90 => new Sql90ScriptGenerator(options),
            SqlVersion.Sql100 => new Sql100ScriptGenerator(options),
            SqlVersion.Sql110 => new Sql110ScriptGenerator(options),
            SqlVersion.Sql120 => new Sql120ScriptGenerator(options),
            SqlVersion.Sql130 => new Sql130ScriptGenerator(options),
            SqlVersion.Sql140 => new Sql140ScriptGenerator(options),
            SqlVersion.Sql150 => new Sql150ScriptGenerator(options),
            SqlVersion.Sql160 => new Sql160ScriptGenerator(options),
            _ => throw new ArgumentOutOfRangeException(nameof(options)),
        };
    }

    public static string ToScript(TSqlFragment scriptFragment, SqlScriptGeneratorOptions options)
    {
        var scripter = GetScripter(options);
        if (scripter == null) return string.Empty;
        string script;
        scripter.GenerateScript(scriptFragment, out script);
        return script;
    }
}
poizan42
  • 1,461
  • 17
  • 22
-1

Yes, go is something SSMS has to allow you to break things up. It isn't part of sql, as you mentioned. SSMS uses SMO to do its work, so thats why it works there.

As your comment makes clear, but the question is muddied, you would need to remove all comment blocks before processing. If you don't want to do that, you would need to process the file as a stream and start ignore at /* and stop at */... and probably also -- and \n|\r\n

You can also uses a regex to split split it up (if you read it in as a text blob, not already broken up by lines):

var text = File.ReadAllText("file.txt")
var cleanedText = Regex.Replace(text, @"/\*.*\*/", "", RegexOptions.Singleline)
var parts = Regex.Split(cleanedText, @"^\s*GO.*$", RegexOptions.Multiline);
for(var part in parts) {
    executeBatch(part);
} 

// but this is getting ugly

var str = "what /*\n the \n\n GO \n*/heck\nGO\nand then";
var cleanedText = Regex.Replace(str, @"/\*.*\*/", "\n", RegexOptions.Singleline)
var split = Regex.Split(cleanedText, @"^\s*GO.*$", RegexOptions.Multiline);
// == ["what\nheck", "\nand then"]

And yes, as the comment says, your real answer is to write a parser. Even with what you say about comments, you can still have /* and */ embedded inside a STRING inside an insert. so...

Andrew
  • 8,322
  • 2
  • 47
  • 70
  • Forgot the continue. Also i would probably wrap up the internal execute batch and the test for "last line" into one line at some point. It's ugly code :D – Andrew Aug 29 '14 at 08:03
  • How does it not. I've written code just like this dozens of times to handle files with hundreds of thousands of insert statements. Perhaps you can clear up your question? – Andrew Aug 29 '14 at 08:13
  • to clear up, if the statement contains in the first line /* in the second line GO and in the third line */ the above answer would try to split the statement – Nadav Aug 29 '14 at 08:17
  • 1
    As I've told you, Nadav, this problem requires actual parsing with at least one 15/20-line recursive function. Try to implement it and if you have any problems post your code in another question. As it is right now, the answers already given are quick fixes and the true answer is: **it's impossible, write a parser**. – pid Aug 29 '14 at 08:20
  • Then you need to pre-parse the file to remove comment blocks. But you are getting into deeply nonstandard territory there. – Andrew Aug 29 '14 at 08:21
  • this fails for nested comments – Ewan Apr 11 '19 at 12:19
-2

Split only when "GO" stands on a lonely line or with whitespaces, like this:

Regex.Split(statements, @"^\s+GO\s+$");
pid
  • 11,472
  • 6
  • 34
  • 63
  • 1
    GO statement which appears inside /* */ would still result with it being split and the error would appear soon after it – Nadav Aug 29 '14 at 08:08
  • 2
    @NadavStern Yes, this problem will stay with **ALL** solutions based on string manipulation. So it's the same for ALL answers posted here. To remove this last problem you have to interpret the text and produce actual semantics of what are statements, strings and comments. You'll need a **true parser**, probably recursive, so you see it's not feasible for such a task. The only solution I see is to change the input by convention: force users to write separate files instead of using `GO`. – pid Aug 29 '14 at 08:16
  • Regex.Split(statements, @"^\s*GO\s*$", RegexOptions.Multiline | RegexOptions.IgnoreCase) worked for me. – xhafan Apr 16 '18 at 09:00