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