New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
(MODULES-6281) Return Errors from T-SQL #263
(MODULES-6281) Return Errors from T-SQL #263
Conversation
@@ -9,7 +9,7 @@ def open_and_run_command(query, config) | |||
open(config) | |||
execute(query) | |||
rescue win32_exception => e |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is => e
still necessary if you're not going to do anything with the specific exception?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably not. I'll get rid of it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey, turns out I do still need it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General rubyism is to prefix with underscore for required, but unused variables _e
@@ -9,7 +9,7 @@ def open_and_run_command(query, config) | |||
open(config) | |||
execute(query) | |||
rescue win32_exception => e | |||
return ResultOutput.new(true, e.message) | |||
return ResultOutput.new(true, @connection.Errors(0).Description) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Am I correct in thinking this will only send back the first error? Is it possible/probable to get 2+ errors, and if so, do I care to retrieve the others?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it will only send back the first error. It seems unlikely enough to receive multiple errors back from a script that it doesn't seem worth the increase in complexity to handle the edge case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To play devils advocate, also not too much to collect all the descriptions:
messages = (0..@connection.Errors.Count { |i| @connection.Errors(i).Description }).join("\n")
6397e6f
to
63abc02
Compare
63abc02
to
45d9721
Compare
288a437
to
4ebc3e2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@glennsarti Interested in your input, because @jpogran said you might have an opinion.
Technically what I've done here could be viewed by some as a change to the behavior of the module. Previously when errors were encountered in this module, unless the error text started with "SQL Server \n", then the exit status would get set, but not error message would be returned, and if it did start that way, only the portion of the string after that phrase was returned. This change got rid of that and started passing back either the full win32 error message, or the error message from the SQL Query. If a user was depending on the previous behavior for some reason, this could be viewed as a breaking change. I don't personally view it as a breaking change, I think this is a bug fix in error messaging and we should go ahead and merge this PR as it stands once I'm done with adhoc and manual testing.
@@ -9,7 +9,7 @@ def open_and_run_command(query, config) | |||
open(config) | |||
execute(query) | |||
rescue win32_exception => e |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey, turns out I do still need it.
@@ -9,7 +9,7 @@ def open_and_run_command(query, config) | |||
open(config) | |||
execute(query) | |||
rescue win32_exception => e |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General rubyism is to prefix with underscore for required, but unused variables _e
end | ||
|
||
if (@error_message.nil?) && (!error_message.nil?) | ||
@error_message = error_message |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
THis is being set twice. Perhaps move it above and add a return statement.
@@ -8,8 +8,11 @@ | |||
|
|||
def stub_connection | |||
@connection = mock() | |||
error = mock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now is not the time, but should really move away from using mocha
for stubs and just using core rspec doubles.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It took me a while to figure out why all of my reading on how to do mocking in rspec was coming up with a different syntax.
In this case I don't view it as a change in behvaiour. For example, on non-English version of windows the error message would be different. (English v Japanese for example). It would probably warrant a minor version bump 0.y.0. One thing that will change will be the acceptance tests (assuming there are any for that class) |
e8bae9b
to
d89351a
Compare
end | ||
end | ||
|
||
unless (!@error_message.nil?) && (error_message.nil?) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The unless
+ negation is a bit confusing to read, especially when also combined with the &&
.
I think your intent here is to set @error_message
ONLY when it's not already set?
If that's correct, I think this can this be simplified like:
@error_message ||= error_message
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was looking for a more elegant want to do this. Changing now.
unless connection.Errors.count == 0 | ||
@error_message = connection.Errors(0).Description | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be simpler as:
@error_message = connection.Errors(0).Description if connection && !connection.Errors.empty?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Errors collection is never a null property, it just throws an error if you try to call an index if it's not storing any errors, which is why I'm calling the count
property first. I could probably get away with @error_message = connection.Errors(0).Description if connection && !connection.Errors.count == 0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The .empty?
I suggested should generally be the same as .count == 0
... unless this is a COM enumerable? In which case using .count == 0
is probably the correct thing to do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Worse than I thought. This object just doesn't respond to .empty?
. It's a OLE object so I'm guess that's why.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's probably not an Enumerable object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's what I was saying @glennsarti ;0
But not only is it not a COM enumerable like IEnumVariant ... which Ruby cannot enumerate ... but the interface doesn't have enumeration methods anyhow.
@@ -90,25 +90,26 @@ def win32_exception | |||
end | |||
|
|||
class ResultOutput | |||
|
|||
attr_reader :exitstatus, :error_message, :raw_error_message |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this class still has a raw_error_message
attribute... but it's not set anymore? Do we still want to keep the concept of what this previously represented?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I'll get rid of it. That was previously in place because the larger error message was being parsed for the sql error, but even in the older version it was never called.
spec/spec_helper.rb
Outdated
@@ -10,4 +10,3 @@ | |||
e.syntax = [:should, :expect] | |||
end | |||
end | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accidental newline deletion
bbc77d7
to
9de69b7
Compare
@error_message = parse_for_error(error_message) | ||
end | ||
|
||
@error_message = connection.Errors(0).Description if connection && !connection.Errors.count == 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One minor nit here... my initial suggestion could have been better:
@error_message = connection.Errors(0).Description if connection && connection.Errors.count > 0
9de69b7
to
75e7e46
Compare
end | ||
|
||
def extract_messages(connection) | ||
error_count = connection.nil? ? -1 : (connection.Errors.count - 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about an early return like this to avoid the nesting?
return nil if connection.nil? || connection.Errors.count == 0
error_count = connection.Errors.count - 1
((0..error_count).map { |i| connection.Errors(i).Description.to_s }).join("\n")
9fda054
to
5923ba0
Compare
Ok, this version incorporates a short circuit in the extract_messages function, it updates the unit tests to work properly with errors contained in the connection object, and it updates the acceptance tests to reflect that the error message returned from T-SQL errors is no longer being parsed and chopped up like it was. |
5923ba0
to
6de16ff
Compare
@@ -9,12 +9,15 @@ def open_and_run_command(query, config) | |||
open(config) | |||
execute(query) | |||
rescue win32_exception => e | |||
return ResultOutput.new(true, e.message) | |||
# require 'pry'; binding.pry; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Errant pry
ensure | ||
close | ||
end | ||
|
||
ResultOutput.new(false, nil) | ||
# require 'pry'; binding.pry; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Errant pry
@error_message = parse_for_error(error_message) | ||
end | ||
|
||
@error_message = (extract_messages(connection) || error_message) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit - extra brackets not required.
def initialize(has_errors, error_message) | ||
attr_reader :exitstatus, :error_message | ||
|
||
def initialize(has_errors, error_message, connection) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Warning, this variable has the same name as a method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does ole_connection sound?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@RandomNoun7 its the @has_errors
bit that should be changed (which predates your code)... connection
is fine
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh. I can change it now, or we can call it out of scope for the ticket. I'm fine either way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Derp.....I should've read more carefully. I was meaning the has_errors
but you didn't change that.
Need more ☕️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can change the name of the method to errors?
and it doesn't seem to break anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will rename or remove in a maint commit.
@@ -20,11 +25,11 @@ def stub_connection | |||
@connection.stubs(:Open).with('Provider=SQLNCLI11;Initial Catalog=master;Application Name=Puppet;Data Source=.;DataTypeComptibility=80;User ID=sa;Password=Pupp3t1@') | |||
end | |||
it 'should not raise an error but populate has_errors with message' do | |||
subject.stubs(:execute).raises(Exception.new("SQL Server\n error has happened")) | |||
@connection.Errors.stubs(:count).returns(1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems an odd stub to add. Should instead it be stubbing a an entire Errors construct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have to have it there, or it never attempts to examine the @connection object for errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This stub is fine... he has to simulate a populated ADODB Errors connection object, as the existence of errors in this collection changes the behavior of the error reporting.
It may be possible to create an instance of the real thing, but that would require Windows...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The need to create and test the real thing is why I modified the acceptance tests to ensure the proper object is examined and just the win32 exception message passed into the rescue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it "feels odd" to me that your stubbing the length of an array, but not setting the content of the Array too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, it's a strange object. Calling count
on the Errors
object isn't just asking for the length of the array as Errors
isn't a plain array object. count
is a real method on the object that has other properties and methods. So in this case I'm stubbing a method call. The content that gets returned is set higher up in the stub_connection
helper at the top of the file. It defaults to zero, but in the case of tests where I actually want to take a look at the error text in the object I have to override that and return 1 as the count.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@glennsarti yeah, higher up he sets error_mock.stubs(:Description).returns("SQL Error in Connection")
... but since the default is 0
items, I agree it should probably be done where the length is stubbed to 1
to make things clearer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The two test cases where this was used have been modified to set the non default count
value and the Description
value inside the test case.
6de16ff
to
78ff9f3
Compare
The sqlserver_tsql resource does not handle errors returned from T-SQL statements properly. The errors come back from the query but unless the phrase 'SQL Server' was included in the error text, the error was not then passed back to the user/logs. This change ensure that errors returned are passed back properly by inspecting the Errors property of the ADO object used to run the query. Note that this may not behave as expected if the error that occurrs is of sufficently low severity that a rowset, even if empty, is also returned. If a rowset is returned by the ADO object, the object will not populate the Errors property, and there is no way to tell a low severity error occurred. For instance 'Select 1/0' returns a low severity division by zero error, and an empty rowset. Because the empty rowset exists, the error cannot be seen. It is up to the user to ensure that the errors they want to catch are of sufficienty severity that execution of the query is halted.
78ff9f3
to
9b8a0fe
Compare
Superceded by updated changes and verbal agreements
subject.stubs(:win32_exception).returns(Exception) | ||
stub_connection | ||
@connection.Errors.stubs(:count).returns(1) | ||
@connection.Errors.stubs(:Description).returns("SQL Error in Connection") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe mocking library makes this magically work... but I read this as supplying a value for:
@connection.Errors.Description
When I think we're interested in:
@connection.Errors(0).Description
The sqlserver_tsql resource does not handle errors returned from T-SQL
statements properly. The errors come back from the query but unless the
phrase 'SQL Server' was included in the error text, the error was not
then passed back to the user/logs. This change ensure that errors
returned are passed back properly by inspecting the Errors property of
the ADO object used to run the query. Note that this may not behave as
expected if the error that occurrs is of sufficently low severity that a
rowset, even if empty, is also returned. If a rowset is returned by the
ADO object, the object will not populate the Errors property, and there
is no way to tell a low severity error occurred. For instance 'Select
1/0' returns a low severity division by zero error, and an empty rowset.
Because the empty rowset exists, the error cannot be seen. It is up to
the user to ensure that the errors they want to catch are of sufficient
severity that execution of the query is halted.