ServerConnection.ExecuteNonQuery in SQLCMD Mode
Asked Answered
P

2

14

I am using the Microsoft Data-Tier Application framework to create a deployment script based on a DacPackage object. I am attempting to use the Microsoft.SqlServer.Management.Smo.Server class to execute this script...

SqlConnection deployConnection = new SqlConnection(connBuilder.ToString());
deployConnection.Open();
Server server = new Server(new ServerConnection(deployConnection));
server.ConnectionContext.ExecuteNonQuery(deployScript);

However, this errors out with...

Unhandled Exception: Microsoft.SqlServer.Management.Common.ExecutionFailureException:
  An exception occurred while executing a Transact-SQL statement or batch. --->
  System.Data.SqlClient.SqlException: Incorrect syntax near ':'.

I know that the answer to this problem is that I need to be in SQLCMD mode, but I don't know how to tell my ServerConnection to execute in said mode.

I guess my problem isn't as specific as what I state in the title. What I really need to be able to do is execute the script generated from the DacPackage via the .Net framework. Can anyone help me with this?

Portent answered 28/10, 2014 at 19:49 Comment(0)
W
17

SQLCMD mode commands are not T-SQL commands; they only work in SQL Server Management Studio (SSMS) / Visual Studio (VS) and SQLCMD.EXE. SQLCMD-mode is inherently how SQLCMD.EXE works and can be manually enabled in SSMS / VS; it is a part of those applications and not something that can be done via a provider.

Those applications interpret the SQLCMD-mode commands and do not pass them through to SQL Server. SQLCMD-mode commands are parsed/executed first (which is how they are able to affect the SQL that is about to be submitted) and then the final version of the SQL is submitted to SQL Server.

Hence, the deployment SQL scripts generated by SQL Server Data Tools (SSDT) / Visual Studio need to be run via one of these three programs.

Since you have a .dacpac file already, Microsoft provides a few ways to publish those that you should check out:

You can also create a publish SQL script via DacServices.GenerateDeployScript(), but this won't change the situation as stated above since the publish / deploy SQL script, whether generated from Visual Studio "Publish {project_name}" or GenerateDeployScript(), is the same script. Meaning, it will have the SQLCMD-mode colon-commands such as :setvar and :on error exit as well as the SQLCMD-mode variables, which at the very least will be $(DatabaseName) which is used in the following line:

USE [$(DatabaseName)];

While it is possible to comment out the initial :setvar lines by setting the DacDeployOptions property of CommentOutSetVarDeclarations to true, that will still leave the :on error exit line as well as a line for :setvar __IsSqlCmdEnabled "True" that is used to detect whether or not SQLCMD-mode has been enabled. Just above this particular :setvar line is a comment stating:

/*
Detect SQLCMD mode and disable script execution if SQLCMD mode is not supported.
To re-enable the script after enabling SQLCMD mode, execute the following:
SET NOEXEC OFF; 
*/

So they really do intend that this script is only run via SQLCMD, whether through DOS -> SQLCMD.EXE or PowerShell -> Invoke-SqlCMD.

Technically, it is possible to generate a string of the deploy script contents (rather than to a stream) and manipulate that string by a) removing any colon-commands, and b) replacing "$(DatabaseName)" with whatever database you intend on deploying to. However, I have not tried this, I am not recommending this, and I am not sure it would work in all situations of what deployment scripts could be generated by SQL Server Data Tools. But it does seem like an option.

Also, minorly related: you don't need SMO to run SQL Scripts. SMO is means of interacting with SQL Server via objects rather than directly through T-SQL commands.

EDIT:
Links where others have tried this and found it did not work:

Possibilities for getting the generated publish SQL script to work programmaticaly:

Wigwam answered 28/10, 2014 at 20:7 Comment(12)
So there is no way to programmatically execute the generated SQLCMD scripts from the .Net framework - is that what you're saying?Portent
@DanForbes - pretty much, yes. Nothing outside of those 3 programs has any clue as to what those colon-commands are. BUT, that does not mean you cannot start a process in C# to call the command-line for "SQLCMD.EXE -S server " and supply the script name and appropriate flag for passing in a SQL script.Wigwam
@DanForbes : I know you already accepted this (thanks) but I did find some extra info that I am adding now to my answer.Wigwam
Excellent! I will explore those suggestions. Thank you very much again, @srutzky!Portent
So, I've been looking at the MSDN docs for the DacServices class, and I'm noticing that the GenerateDeployScript method specifies the following return value: "String of Transact-SQL script used to create or update database schema based on the supplied DacPackage." But, that seems directly at odds with the behavior I'm seeing and what you're saying above. Is that just a typo on Microsoft's behalf?Portent
@DanForbes Thanks for pointing out the DacServices class, I hadn't seen that before but very cool. The issue here is that you are confusing two things as interchangable: SQL publish scripts and DacPac files. You will publish one or the other. The "GenerateDeployScript" method takes a ".dacpac" file and creates a SQL publish script from it. But that does not do the publishing and you already have a SQL script. So, pick which type of file you want to deploy (".sql" or ".dacpac"). If ".sql" then use SQLCMD.EXE or PowerShell as noted. Else use DacServices.Deploy. I will add this to the answer.Wigwam
Right, I understand that, but above you say, "SQLCMD mode commands are not T-SQL commands," but then the documentation seems to indicate the returned string is a T-SQL script. That's my misunderstanding. Incidentally, what I'm trying to do right now is profile the performance of deploying a DACPAC directly via the DacServices class versus executing the deploy script that the DacServices class can generate.Portent
@DanForbes , are you saying that you have a) generated a SQL deploy script via GenerateDeployScript(), and b) that SQL scripts does contain the ":setvar", etc SQLCMD-mode commands?Wigwam
Yes, that's exactly what I'm saying.Portent
I also see :on error exit. I think that may be it (in addition to the :setvars) but it's kind of hard to tell.Portent
Let us continue this discussion in chat.Wigwam
@DanForbes : I just found something interesting, an open-source library (fairly small) that handles SQLCMD-specific SQL scripts. I added a new bullet to the final section (the middle / 2nd one) with the details.Wigwam
M
0

I was solving the task to execute a script, generated by SqlPackage tool programmatically, to set up the database for integration tests. After thorough investigation, I still have not found any standard .NET library to parse or execute SQLCMD mode queries. So I have considered the following options:

  1. Call sqlcmd.exe command line tool.
  2. Call Invoke-SqlCmd PowerShell command.
  3. Use one of the third-party libraries to run SQLCMD mode script.

None of the options were satisfactory for me after deeper consideration and experimenting. On the other hand, I have noticed that probably the support of all language features of SQLCMD language is not required for me. More specifically, I only need variables substitution and ignoring some directives. So I have decided to implement my own limited SQLCMD mode parser based on regular expressions:

internal static class SqlCmdScriptParser
{
    private static readonly Command[] ControlCommands =
    {
        new()
        {
            Id = "setvar",
            Pattern = new(@":(?<command>setvar)\s+(?<name>\w+)\s+""(?<value>.*)"""),
            IsFullLine = true,
            MatchSubstitution =
                (match, variables) =>
                {
                    variables.Add(match.Groups["name"].Value, match.Groups["value"].Value);
                    return string.Empty;
                },
        },
        new()
        {
            Id = "on error",
            Pattern = new(@":(?<command>on error)\s+(?<value>exit|ignore)"),
            IsFullLine = true,
            MatchSubstitution = (_, _) => string.Empty,
        },
        new()
        {
            Id = "$",
            Pattern = new(@"(?<command>\$)\((?<name>\w+)\)"),
            IsFullLine = false,
            MatchSubstitution =
                (match, variables) =>
                    variables.GetValueOrDefault(match.Groups["name"].Value) ?? string.Empty,
        },
    };

    private static readonly IReadOnlyDictionary<string, Command> ControlCommandsMap =
        ControlCommands.ToDictionary(c => c.Id, StringComparer.OrdinalIgnoreCase);

    private static readonly Regex ControlCommandsPattern = GetControlCommandsPattern();

    private static readonly Regex BatchSeparatorPattern = new Regex("GO").ToFullLine();

    public static IReadOnlyCollection<string> Parse(
        string input, IReadOnlyDictionary<string, string>? variables = null) =>
        input
            .SubstituteControlCommands(variables)
            .SplitBatch();

    private static Regex GetControlCommandsPattern()
    {
        var patterns = ControlCommands
            .Select(c => c.IsFullLine ? c.Pattern.ToFullLine() : c.Pattern)
            .Select(p => $"({p})")
            .ToList();
        var combinedPattern = string.Join("|", patterns);
        return new Regex(combinedPattern, RegexOptions.Multiline | RegexOptions.Compiled);
    }

    private static Regex ToFullLine(this Regex source) =>
        new($@"^\s*{source}\s*$\n?", RegexOptions.Multiline | RegexOptions.Compiled);

    private static string SubstituteControlCommands(
        this string input, IReadOnlyDictionary<string, string>? variables)
    {
        var establishedVariables = new Dictionary<string, string>(
            variables ?? new Dictionary<string, string>(), StringComparer.OrdinalIgnoreCase);
        return ControlCommandsPattern
            .Replace(input, match => SubstituteControlCommandMatch(match, establishedVariables));
    }

    private static string SubstituteControlCommandMatch(
        Match match, Dictionary<string, string> variables)
    {
        var commandId = match.Groups["command"].Value;
        var command = ControlCommandsMap.GetValueOrDefault(commandId)
            ?? throw new InvalidOperationException($"Unknown command: {commandId}");
        return command.MatchSubstitution(match, variables);
    }

    private static IReadOnlyCollection<string> SplitBatch(this string input) =>
        BatchSeparatorPattern.Split(input)
            .Where(s => !string.IsNullOrEmpty(s))
            .ToList();

    private sealed class Command
    {
        public string Id { get; init; } = string.Empty;

        public Regex Pattern { get; init; } = new(string.Empty);

        public bool IsFullLine { get; init; }

        public Func<Match, Dictionary<string, string>, string> MatchSubstitution { get; init; } =
            (_, _) => string.Empty;
    }
}

Usage:

var inputScript = '-- Set your input SQLCMD mode script here';

// Set your variables here
var variables = new Dictionary<string, string>();

var batches = SqlCmdScriptParser.Parse(inputScript, variables);

As output, it generates the collection of SQL commands ready to be executed.

This implementation is currently limited, but it is easily extensible, since supported commands can be added in declarative way. In case the commands should be precisely implemented (not just ignored), some parser state can be introduced, which can result in form settings, associated with each generated SQL command. These settings can be used during the execution of generated SQL commands.

Example in .NET Fiddle

Maine answered 20/8, 2023 at 13:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.