【问题标题】:Executing SQL batch containing GO statements in C#在 C# 中执行包含 GO 语句的 SQL 批处理
【发布时间】:2014-10-23 04:56:15
【问题描述】:

我正在尝试构建一个通过错误处理批量执行 sql 语句的程序 (因此我没有使用 SMO)。

问题是 GO 不是 SQL 的一部分,当使用 .NET 执行语句时,它最终会出现错误(SMO 会处理它,但没有给出任何指示是否执行失败)。

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

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

/*
GO
*/

(我用谷歌搜索了它,但那里没有解决方案)

【问题讨论】:

  • 如果满足您的要求,您可以查看此答案stackoverflow.com/questions/12431591/…
  • 是否每个 GO 语句都换行?
  • 链接问题的答案并没有解决Go出现在/**/里面的情况例如:/* GO / where第一行是/ 第二行是GO第三行是 */
  • 我明白了,那么您需要一个更复杂的算法。解析和所有的麻烦。您应该根据所需的复杂性权衡您的选择(和时间限制)
  • 您也忽略了GO 可以轻松出现在字符串中的事实。

标签: c# sql .net sql-server


【解决方案1】:

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 运行时

CodeFluent Runtime Database 有一个小的 sql 文件解析器。它不处理复杂的情况,但例如支持 cmets。

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?是这样的吗
  • 感谢您报告此错误。我修好了。
  • 这是一个很酷的解决方案。我可能以前尝试过,它看起来很熟悉。也许没有使用它,因为我生成的文件是健全的(例如 SSMS 生成的。)对于其他人,您需要安装什么才能使这些库可用,它们是否在 GAC 中,等等。
  • 库在 GAC 中或可以在“C:\Program Files (x86)\Microsoft Visual Studio 10.0\VSTSDB”中找到
  • 应该运行的脚本可以包含存储过程,在此解决方案中使用 alter 会导致所有 cmets 从存储过程中删除
【解决方案2】:

是的,Go 是 SSMS 必须允许您分解的东西。正如您所提到的,它不是 sql 的一部分。 SSMS 使用 SMO 来完成它的工作,这就是它在那里工作的原因。

正如您的评论表明的那样,但问题很混乱,您需要在处理之前删除所有评论块。如果您不想这样做,则需要将文件作为流处理并从/* 开始忽略并在*/ 停止...可能还有--\n|\r\n

您还可以使用正则表达式将其拆分(如果您将其作为文本 blob 读入,尚未按行拆分):

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

是的,正如评论所说,您真正的答案是编写一个解析器。即使按照您所说的 cmets,您仍然可以将 /**/ 嵌入到 insert 内的 STRING 中。所以……

【讨论】:

  • 忘记继续了。此外,我可能会在某个时候将内部执行批处理和“最后一行”的测试打包成一行。这是丑陋的代码:D
  • 怎么没有。我已经编写了数十次这样的代码来处理具有数十万个插入语句的文件。也许你可以澄清你的问题?
  • 澄清一下,如果语句包含在第一行 /* 第二行 GO 和第三行 */ 上面的答案将尝试拆分语句
  • 正如我告诉过你的,Nadav,这个问题需要使用至少一个 15/20 行递归函数进行实际解析。尝试实现它,如果您有任何问题,请在另一个问题中发布您的代码。就目前而言,已经给出的答案是快速修复,真正的答案是:不可能,写一个解析器
  • 那你需要对文件进行预解析,去掉注释块。但是你正在进入非常不标准的领域。
【解决方案3】:

仅当“GO”位于单行或带有空格时才拆分,如下所示:

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

【讨论】:

  • 出现在 /* */ 中的 GO 语句仍然会导致它被拆分,并且在它之后很快就会出现错误
  • @NadavStern 是的,这个问题将存在于基于字符串操作的 ALL 解决方案中。所以这里发布的所有答案都是一样的。要消除最后一个问题,您必须解释文本并生成语句、字符串和 cmets 的实际语义。您将需要一个真正的解析器,可能是递归的,因此您会发现它对于这样的任务是不可行的。我看到的唯一解决方案是按约定更改输入:强制用户编写单独的文件而不是使用GO
  • Regex.Split(statements, @"^\s*GO\s*$", RegexOptions.Multiline | RegexOptions.IgnoreCase) 为我工作。
猜你喜欢
  • 2015-10-25
  • 2013-01-18
  • 2015-07-30
  • 1970-01-01
  • 1970-01-01
  • 2022-06-14
  • 1970-01-01
  • 1970-01-01
  • 2014-01-09
相关资源
最近更新 更多