SSMS SMO Objects: Get query results
Asked Answered
A

1

4

I came across this tutorial to understand how to execute SQL scripts with GO statements.
Now I want to know what can I get the output of the messages TAB.

With several GO statements, the output would be like this:
1 rows affected
912 rows affected
...

But server.ConnectionContext.ExecuteNonQuery() can return only an int, while I need all the text. In case there is some error in some part of query, it should put that also in the output. Any help would be appreciated.

Antibody answered 28/8, 2012 at 7:0 Comment(0)
T
4

The easiest thing is possibly to just print the number you get back for ExecuteNonQuery:

int rowsAffected = server.ConnectionContext.ExecuteNonQuery(/* ... */);
if (rowsAffected != -1)
{
     Console.WriteLine("{0} rows affected.", rowsAffected);
}

This should work, but will not honor the SET NOCOUNT setting of the current session/scope.

Otherwise you would do it like you would do with "plain" ADO.NET. Don't use the ServerConnection.ExecuteNonQuery() method, but create an SqlCommand object by accessing the underlying SqlConnection object. On that subscribe to the StatementCompleted event.

using (SqlCommand command = server.ConnectionContext.SqlConnectionObject.CreateCommand())
{
    // Set other properties for "command", like StatementText, etc.

    command.StatementCompleted += (s, e) => {
         Console.WriteLine("{0} row(s) affected.", e.RecordCount);
    };

    command.ExecuteNonQuery();
}

Using StatementCompleted (instead, say, manually printing the value that ExecuteNonQuery() returned) has the benefit that it works exactly like SSMS or SQLCMD.EXE would:

  • For commands that do not have a ROWCOUNT it will not be called at all (e.g. GO, USE).
  • If SET NOCOUNT ON was set, it will not be called at all.
  • If SET NOCOUNT OFF was set, it will be called for every statement inside a batch.

(Sidebar: it looks like StatementCompleted is exactly what the TDS protocol talks about when DONE_IN_PROC event is mentioned; see Remarks of the SET NOCOUNT command on MSDN.)

Personally, I have used this approach with success in my own "clone" of SQLCMD.EXE.

UPDATE: It should be noted, that this approach (of course) requires you to manually split the input script/statements at the GO separator, because you're back to using SqlCommand.Execute*() which cannot handle multiple batches at a time. For this, there are multiple options:

  • Manually split the input on lines starting with GO (caveat: GO can be called like GO 5, for example, to execute the previous batch 5 times).
  • Use the ManagedBatchParser class/library to help you split the input into single batches, especially implement ICommandExecutor.ProcessBatch with the code above (or something resembling it).

I choose the later option, which was quite some work, given that it is not pretty well documented and examples are rare (google a bit, you'll find some stuff, or use reflector to see how the SMO-Assemblies use that class).

The benefit (and maybe burden) of using the ManagedBatchParser is, that it will also parse all other constructs of T-SQL scripts (intended for SQLCMD.EXE) for you. Including: :setvar, :connect, :quit, etc. You don't have to implement the respective ICommandExecutor members, if your scripts don't use them, of course. But mind you that you'll may not be able to execute "arbitrary" scripts.

Well, were did that put you. From the "simple question" of how to print "... rows affected" to the fact that it is not trivial to do in a robust and general manner (given the background work required). YMMV, good luck.

Update on ManagedBatchParser Usage

There seems to be no good documenation or example about how to implement IBatchSource, here is what I went with.

internal abstract class BatchSource : IBatchSource
{
    private string m_content;

    public void Populate()
    {
        m_content = GetContent();
    }

    public void Reset()
    {
        m_content = null;
    }

    protected abstract string GetContent();

    public ParserAction GetMoreData(ref string str)
    {
        str = null;

        if (m_content != null)
        {
            str = m_content;
            m_content = null;
        }

        return ParserAction.Continue;
    }
}

internal class FileBatchSource : BatchSource
{
    private readonly string m_fileName;

    public FileBatchSource(string fileName)
    {
        m_fileName = fileName;
    }

    protected override string GetContent()
    {
        return File.ReadAllText(m_fileName);
    }
}

internal class StatementBatchSource : BatchSource
{
    private readonly string m_statement;

    public StatementBatchSource(string statement)
    {
        m_statement = statement;
    }

    protected override string GetContent()
    {
        return m_statement;
    }
}

And this is how you would use it:

var source = new StatementBatchSource("SELECT GETUTCDATE()");
source.Populate();

var parser = new Parser(); 
parser.SetBatchSource(source);
/* other parser.Set*() calls */

parser.Parse();

Note that both implementations, either for direct statements (StatementBatchSource) or for a file (FileBatchSource) have the problem that they read the complete text at once into memory. I had one case where that blew up, having a huge(!) script with gazillions of generated INSERT statements. Even though I don't think that is a practical issue, SQLCMD.EXE could handle it. But for the life of me, I couldn't figure out how exactly, you would need to form the chunks returned for IBatchParser.GetContent() so that the parser can still work with them (it looks like they would need to be complete statements, which would sort of defeat the purpose of the parse in the first place...).

Tripp answered 28/8, 2012 at 7:8 Comment(6)
Its is good but there are GO keywords in the SQL Script and if I use plain ADO.NET it will show it as error, I can split them using GO as string but still. Is there any way possible to do it the way I want ?Antibody
The problem is that "the way you want" might not be correct (in all situations). See my updates at the top of the answer.Tripp
@Tripp where did you find information or documentation on using the ManagedBatchParser ? I can't find an implementation of IBatchSource. Does it require you make your own?Senzer
@Senzer At the time, I couldn't find any substantial information either. I did the following: spelunking around with dotPeek in assemblies that I knew, must use the parser (parts of SSMS for example) and 2nd read the documentation and made tests with SQLCMD.EXE. The later especially for behavior concerning the Ed, ListVar, ServerList, etc. methods of ICommandExecutor and the methods of IVariableResolver (which resemble :setvar, etc. from SQLCMD.EXE. Concerning the Implementation of IBatchSource, I'll update my answer.Tripp
@Tripp thanks for the great pointers. Seems like Microsoft did have an implementation of IBatchSource! Check out Microsoft.SqlServer.BatchParserClient in the GAC from either SQL 2008 or 2012! Figured I better canonize this before you do. How do you parse large SQL scripts into batches?Senzer
@Senzer Good finding. However I have my doubts about the "large" part. I left a comment over at your answer.Tripp

© 2022 - 2024 — McMap. All rights reserved.