diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c7e7ab5..46ef46fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Drop support for SQL Server < 2017 * Drop support for FreeTDS < 1.0 * Raise error if FreeTDS is unable to sent command buffer to the server +* Cancel previous query results automatically when invoking a new query ## 2.1.7 * Add Ruby 3.3 to the cross compile list diff --git a/README.md b/README.md index 4aa68e69..082afefe 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ result = client.execute("SELECT * FROM [datatypes]") ## TinyTds::Result Usage -A result object is returned by the client's execute command. It is important that you either return the data from the query, most likely with the #each method, or that you cancel the results before asking the client to execute another SQL batch. Failing to do so will yield an error. +A result object is returned by the client's execute command. It is important that you either return the data from the query, most likely with the #each method. You can manually cancel the results early using `#cancel`, otherwise tiny_tds will also automatically do so when running a new query with `#execute`. Calling #each on the result will lazily load each row from the database. @@ -171,7 +171,6 @@ result.do result = client.execute("SELECT [id] FROM [datatypes]") result.fields # => ["id"] -result.cancel result = client.execute("SELECT [id] FROM [datatypes]") result.each(:symbolize_keys => true) result.fields # => [:id] diff --git a/ext/tiny_tds/client.c b/ext/tiny_tds/client.c index 03a1b4b5..8b5505c4 100644 --- a/ext/tiny_tds/client.c +++ b/ext/tiny_tds/client.c @@ -1,5 +1,6 @@ #include #include +#include VALUE cTinyTdsClient; extern VALUE mTinyTds, cTinyTdsError; @@ -15,12 +16,6 @@ VALUE opt_escape_regex, opt_escape_dblquote; tinytds_client_wrapper *cwrap; \ Data_Get_Struct(self, tinytds_client_wrapper, cwrap) -#define REQUIRE_OPEN_CLIENT(cwrap) \ - if (cwrap->closed || cwrap->userdata->closed) { \ - rb_raise(cTinyTdsError, "closed connection"); \ - return Qnil; \ - } - // Lib Backend (Helpers) @@ -295,8 +290,32 @@ static VALUE rb_tinytds_execute(VALUE self, VALUE sql) { VALUE result; GET_CLIENT_WRAPPER(self); + + if (cwrap->closed || cwrap->userdata->closed) { + rb_raise(cTinyTdsError, "closed connection"); + return Qnil; + } + + if (rb_tinytds_dead(self) == Qtrue) { + rb_raise(cTinyTdsError, "client is dead, please create a new instance"); + return Qnil; + } + + // user is coming back from an each loop, make sure we cancel the pending results + if (cwrap->userdata->dbsql_sent) { + // if we do not run dbsqlok, FreeTDS will throw an error + // Attempt to initiate a new Adaptive Server operation with results pending + if (cwrap->userdata->dbsqlok_sent == 0) { + if(nogvl_dbsqlok(cwrap->client) != SUCCEED) { + rb_raise(cTinyTdsError, "unable to acknowledge previous results with server"); + } + } + + dbcancel(cwrap->client); + } + rb_tinytds_client_reset_userdata(cwrap->userdata); - REQUIRE_OPEN_CLIENT(cwrap); + dbcmd(cwrap->client, StringValueCStr(sql)); if (dbsqlsend(cwrap->client) == FAIL) { rb_raise(cTinyTdsError, "failed dbsqlsend() function"); diff --git a/ext/tiny_tds/nogvl.c b/ext/tiny_tds/nogvl.c new file mode 100644 index 00000000..8a418276 --- /dev/null +++ b/ext/tiny_tds/nogvl.c @@ -0,0 +1,81 @@ +#include + +void nogvl_setup(DBPROCESS *client) { + GET_CLIENT_USERDATA(client); + userdata->nonblocking = 1; + userdata->nonblocking_errors_length = 0; + userdata->nonblocking_errors = malloc(ERRORS_STACK_INIT_SIZE * sizeof(tinytds_errordata)); + userdata->nonblocking_errors_size = ERRORS_STACK_INIT_SIZE; +} + +void nogvl_cleanup(DBPROCESS *client) { + GET_CLIENT_USERDATA(client); + userdata->nonblocking = 0; + userdata->timing_out = 0; + /* + Now that the blocking operation is done, we can finally throw any + exceptions based on errors from SQL Server. + */ + short int i; + for (i = 0; i < userdata->nonblocking_errors_length; i++) { + tinytds_errordata error = userdata->nonblocking_errors[i]; + + // lookahead to drain any info messages ahead of raising error + if (!error.is_message) { + short int j; + for (j = i; j < userdata->nonblocking_errors_length; j++) { + tinytds_errordata msg_error = userdata->nonblocking_errors[j]; + if (msg_error.is_message) { + rb_tinytds_raise_error(client, msg_error); + } + } + } + + rb_tinytds_raise_error(client, error); + } + + free(userdata->nonblocking_errors); + userdata->nonblocking_errors_length = 0; + userdata->nonblocking_errors_size = 0; +} + +void dbcancel_ubf(DBPROCESS *client) { + GET_CLIENT_USERDATA(client); + dbcancel(client); + userdata->dbcancel_sent = 1; +} + +// No GVL Helpers +RETCODE nogvl_dbsqlexec(DBPROCESS *client) { + int retcode = FAIL; + nogvl_setup(client); + retcode = NOGVL_DBCALL(dbsqlexec, client); + nogvl_cleanup(client); + return retcode; +} + +RETCODE nogvl_dbsqlok(DBPROCESS *client) { + int retcode = FAIL; + GET_CLIENT_USERDATA(client); + nogvl_setup(client); + retcode = NOGVL_DBCALL(dbsqlok, client); + nogvl_cleanup(client); + userdata->dbsqlok_sent = 1; + return retcode; +} + +RETCODE nogvl_dbresults(DBPROCESS *client) { + int retcode = FAIL; + nogvl_setup(client); + retcode = NOGVL_DBCALL(dbresults, client); + nogvl_cleanup(client); + return retcode; +} + +RETCODE nogvl_dbnextrow(DBPROCESS * client) { + int retcode = FAIL; + nogvl_setup(client); + retcode = NOGVL_DBCALL(dbnextrow, client); + nogvl_cleanup(client); + return retcode; +} diff --git a/ext/tiny_tds/nogvl.h b/ext/tiny_tds/nogvl.h new file mode 100644 index 00000000..d6fabce3 --- /dev/null +++ b/ext/tiny_tds/nogvl.h @@ -0,0 +1,16 @@ +#ifndef TINYTDS_NOGVL_H +#define TINYTDS_NOGVL_H + +#define NOGVL_DBCALL(_dbfunction, _client) ( \ + (RETCODE)(intptr_t)rb_thread_call_without_gvl( \ + (void *(*)(void *))_dbfunction, _client, \ + (rb_unblock_function_t*)dbcancel_ubf, _client ) \ +) + +void dbcancel_ubf(DBPROCESS *client); +RETCODE nogvl_dbnextrow(DBPROCESS * client); +RETCODE nogvl_dbresults(DBPROCESS *client); +RETCODE nogvl_dbsqlexec(DBPROCESS *client); +RETCODE nogvl_dbsqlok(DBPROCESS *client); + +#endif \ No newline at end of file diff --git a/ext/tiny_tds/result.c b/ext/tiny_tds/result.c index fc17ce07..610dc62e 100644 --- a/ext/tiny_tds/result.c +++ b/ext/tiny_tds/result.c @@ -69,93 +69,6 @@ VALUE rb_tinytds_new_result_obj(tinytds_client_wrapper *cwrap) { return obj; } -// No GVL Helpers - -#define NOGVL_DBCALL(_dbfunction, _client) ( \ - (RETCODE)(intptr_t)rb_thread_call_without_gvl( \ - (void *(*)(void *))_dbfunction, _client, \ - (rb_unblock_function_t*)dbcancel_ubf, _client ) \ -) - -static void dbcancel_ubf(DBPROCESS *client) { - GET_CLIENT_USERDATA(client); - dbcancel(client); - userdata->dbcancel_sent = 1; -} - -static void nogvl_setup(DBPROCESS *client) { - GET_CLIENT_USERDATA(client); - userdata->nonblocking = 1; - userdata->nonblocking_errors_length = 0; - userdata->nonblocking_errors = malloc(ERRORS_STACK_INIT_SIZE * sizeof(tinytds_errordata)); - userdata->nonblocking_errors_size = ERRORS_STACK_INIT_SIZE; -} - -static void nogvl_cleanup(DBPROCESS *client) { - GET_CLIENT_USERDATA(client); - userdata->nonblocking = 0; - userdata->timing_out = 0; - /* - Now that the blocking operation is done, we can finally throw any - exceptions based on errors from SQL Server. - */ - short int i; - for (i = 0; i < userdata->nonblocking_errors_length; i++) { - tinytds_errordata error = userdata->nonblocking_errors[i]; - - // lookahead to drain any info messages ahead of raising error - if (!error.is_message) { - short int j; - for (j = i; j < userdata->nonblocking_errors_length; j++) { - tinytds_errordata msg_error = userdata->nonblocking_errors[j]; - if (msg_error.is_message) { - rb_tinytds_raise_error(client, msg_error); - } - } - } - - rb_tinytds_raise_error(client, error); - } - - free(userdata->nonblocking_errors); - userdata->nonblocking_errors_length = 0; - userdata->nonblocking_errors_size = 0; -} - -static RETCODE nogvl_dbsqlok(DBPROCESS *client) { - int retcode = FAIL; - GET_CLIENT_USERDATA(client); - nogvl_setup(client); - retcode = NOGVL_DBCALL(dbsqlok, client); - nogvl_cleanup(client); - userdata->dbsqlok_sent = 1; - return retcode; -} - -static RETCODE nogvl_dbsqlexec(DBPROCESS *client) { - int retcode = FAIL; - nogvl_setup(client); - retcode = NOGVL_DBCALL(dbsqlexec, client); - nogvl_cleanup(client); - return retcode; -} - -static RETCODE nogvl_dbresults(DBPROCESS *client) { - int retcode = FAIL; - nogvl_setup(client); - retcode = NOGVL_DBCALL(dbresults, client); - nogvl_cleanup(client); - return retcode; -} - -static RETCODE nogvl_dbnextrow(DBPROCESS * client) { - int retcode = FAIL; - nogvl_setup(client); - retcode = NOGVL_DBCALL(dbnextrow, client); - nogvl_cleanup(client); - return retcode; -} - // Lib Backend (Helpers) static RETCODE rb_tinytds_result_dbresults_retcode(VALUE self) { diff --git a/ext/tiny_tds/tiny_tds_ext.h b/ext/tiny_tds/tiny_tds_ext.h index 55494981..ffb020bf 100644 --- a/ext/tiny_tds/tiny_tds_ext.h +++ b/ext/tiny_tds/tiny_tds_ext.h @@ -11,6 +11,7 @@ #include #include +#include #include #include diff --git a/test/client_test.rb b/test/client_test.rb index fff406f6..cb254be6 100644 --- a/test/client_test.rb +++ b/test/client_test.rb @@ -269,4 +269,45 @@ class ClientTest < TinyTds::TestCase ).must_equal 'user' end end + + describe "#execute" do + it "cancels pending select query" do + client = new_connection + client.execute("SELECT 1 as [one]") + + assert client.sqlsent? + assert !client.canceled? + + result = client.execute("SELECT 1 as [one]") + assert_equal [{"one"=>1}], result.to_a + assert_client_works(client) + end + + it "cancels pending wait query" do + client = new_connection + client.execute("WaitFor Delay '00:00:05'") + + assert client.sqlsent? + assert !client.canceled? + + result = client.execute("SELECT 1 as [one]") + assert_equal [{"one"=>1}], result.to_a + assert_client_works(client) + end + + # this requires to not send another `dbsqlok` compared to the previous to test cases + it "cancel partially retrieved results" do + client = new_connection + result = client.execute("SELECT 1 as [one]; SELECT 2 as [two]; SELECT 3 as [three]") + result.each { |r| break if r.key?("two") } + + assert_equal 1, result.count + assert client.sqlsent? + assert !client.canceled? + + result = client.execute("SELECT 1 as [one]") + assert_equal [{"one"=>1}], result.to_a + assert_client_works(client) + end + end end