在C#中执行包含GO语句的SQL批处理

7
我正在尝试构建一个程序,批量执行SQL语句并进行错误处理(因此我没有使用SMO)。问题是GO不是SQL的一部分,当使用.NET执行语句时会出现错误(SMO会处理,但不会给出任何指示是否执行失败)。
string statements = File.ReadAllText("c:\\test.sql");
string[] splitted = statements.split("GO");

使用以上代码并不能解决我的问题,因为GO关键字也可能出现在注释中(我不想从语句中删除注释),而注释可以出现在/**/内或两个破折号--之后。
例如,我不希望解析以下代码:

/*
GO
*/

(当然我谷歌过了,但那里没有解决方案)

1
如果你满意的话,可以查看这个回答:http://stackoverflow.com/questions/12431591/how-do-i-run-large-sql-scripts-that-contain-many-keywords-including-go-using/12431708#12431708 - Steve
2
你还忽略了 GO 可以轻松出现在字符串中这一事实。 - Damien_The_Unbeliever
@Damien_The_Unbeliever 感谢您的评论,这是我需要考虑的另一个问题。 - Nadav
我认为你需要更新你的问题。从你的问题中,任何人都可以理解到给出的答案对你正在做的事情完全有效。 - Andrew
可能是 执行带有 GO 命令的大型 SQL 脚本 的重复。 - Michael Freidgeim
显示剩余4条评论
4个回答

11

ScriptDom

最简单且最健壮的解决方案是使用 T-SQL 解析器。好消息是您不必自己编写它,只需添加以下引用:

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

然后使用以下代码:

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管理对象

添加以下引用:

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

你可以使用以下代码:

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具有小型SQL文件解析器。它不能处理复杂情况,但例如支持注释。

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);
    }
}

或者更简单地说

new CodeFluent.Runtime.Database.Management.SqlServer.Database("connection string")
      .RunScript("path", StatementReaderOptions.Default);

酷。但是再快速地看一下你的switch语句... Sql80 => 100100 => 80?这样写是有意义的吗? - Andrew
感谢您报告这个错误。我会修复它。 - meziantou
这是一个很棒的解决方案。我可能之前尝试过它,它看起来很熟悉。也许我没有使用它,因为我的生成文件很健全(例如SSMS生成的)。对于其他人来说,需要安装什么才能使用这些库,它们是否在GAC中等等。 - Andrew
@NadavStern 不确定为什么会有问题。他想运行脚本,对吧,而不是编辑它?所以他只需要不保存更改即可。 - Andrew
1
应该运行的脚本可以包含存储过程,使用此解决方案的alter将导致所有存储过程中的注释被删除。 - Nadav
显示剩余3条评论

0

ScriptDom(2023年更新)

@meziantou的答案编辑队列已满(并且在500个建议的情况下,似乎没有人有编辑权限会去处理它),所以我将这个更新的答案添加为社区wiki。 ScriptDom现在位于nugetgithub

ScriptDom是一个支持批处理文件的T-SQL解析器。只需添加对nuget包Microsoft.SqlServer.TransactSql.ScriptDom的引用即可。示例:

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;
    }
}

-1

是的,Go是SSMS必须允许您拆分事物的内容。正如您所提到的,它不是SQL的一部分。SSMS使用SMO来完成其工作,这就是为什么它在那里起作用的原因。

正如您的评论所清楚表明的那样,但问题变得混乱了,您需要在处理之前删除所有注释块。如果您不想这样做,您需要将文件处理为流,并从/*开始忽略,直到*/...并且可能还有--\n|\r\n

您还可以使用正则表达式将其拆分(如果您将其作为文本块读入,而不是已经按行拆分):

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"]

是的,正如评论所说,你真正的答案是要编写解析器。即使你提到了注释,你仍然可以在insert中的字符串内嵌入/**/。所以...


忘记了 continue。我可能会在某个时候将内部执行批处理和“最后一行”的测试包装成一行。这是丑陋的代码:D - Andrew
这怎么可能不行呢。我已经写过很多次类似的代码,来处理包含成千上万个插入语句的文件。也许您可以澄清一下您的问题? - Andrew
澄清一下,如果语句在第一行包含“/”,第二行包含“GO”,第三行包含“/”,上述答案会尝试拆分该语句。 - Nadav
1
正如我告诉你的那样,Nadav,这个问题需要实际解析,至少需要一个15/20行的递归函数。尝试实现它,如果有任何问题,请在另一个问题中发布您的代码。就目前而言,已经给出的答案只是快速修复,真正的答案是:不可能,编写一个解析器 - pid
然后你需要预解析文件以去除注释块。但是在那里,您要涉足深度非标准领域。 - Andrew
这对于嵌套注释是失败的。 - Ewan

-2

只有当"GO"独占一行或出现在空白处时才拆分,像这样:

Regex.Split(statements, @"^\s+GO\s+$");

1
在 /* */ 中出现的 GO 语句仍然会被分割,错误很快就会出现。 - Nadav
2
@NadavStern 是的,这个问题将影响基于字符串操作的所有解决方案。因此,这对于在此处发布的所有答案都是相同的。要消除此最后一个问题,您需要解释文本并生成语句、字符串和注释的实际语义。您需要一个真正的解析器,可能是递归的,因此您会发现这对于这样的任务不可行。我看到唯一的解决方案是通过约定更改输入:强制用户编写单独的文件,而不是使用 GO - pid
Regex.Split(statements, @"^\sGO\s$", RegexOptions.Multiline | RegexOptions.IgnoreCase) 对我很有用。 - xhafan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接