Выполнение пакета SQL, содержащего операторы GO в C#

Я пытаюсь построить программу, которая выполняет операторы SQL в пакетном режиме с обработкой ошибок (поэтому я не использую SMO).

проблема в том, что GO не является частью SQL, и при использовании.NET для выполнения операторов возникает ошибка (SMO обрабатывает ее, но не дает никаких указаний на то, что выполнение не удалось).

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

использование вышеприведенных строк не решает мою проблему из-за того, что ключевое слово GO также может входить в комментарий (я не хочу удалять комментарии из утверждений), а комментарии могут входить в /**/ или после двух тире -
например, я не хотел бы, чтобы следующий код был проанализирован:

/*
GO
*/

(конечно, я погуглил, но там не было решения)

4 ответа

Решение

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

СкриптДом (обновлено в 2023 г.)

Очередь редактирования для ответа @meziantou заполнена (и при размере 500 предложений не похоже, что кто-либо с привилегиями редактирования когда-либо будет проходить через нее), поэтому я добавляю этот обновленный ответ как вики сообщества. . ScriptDom теперь живет на nuget, github

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

Да, GoSM - это то, что 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"]

И да, как говорится в комментарии, ваш реальный ответ - написать парсер. Даже с тем, что вы говорите о комментариях, вы все равно можете иметь /* а также */ встроенный внутри STRING внутри insert, так...

Разделять только тогда, когда "GO" стоит на одинокой линии или с пробелами, например так:

Regex.Split(statements, @"^\s+GO\s+$");
Другие вопросы по тегам