Skip to content

Commit d05d70d

Browse files
committed
Move insert to client class
1 parent 25ab0bd commit d05d70d

File tree

4 files changed

+223
-94
lines changed

4 files changed

+223
-94
lines changed

ext/tiny_tds/client.c

Lines changed: 173 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,6 @@ VALUE opt_escape_regex, opt_escape_dblquote;
1515
tinytds_client_wrapper *cwrap; \
1616
Data_Get_Struct(self, tinytds_client_wrapper, cwrap)
1717

18-
#define REQUIRE_OPEN_CLIENT(cwrap) \
19-
if (cwrap->closed || cwrap->userdata->closed) { \
20-
rb_raise(cTinyTdsError, "closed connection"); \
21-
return Qnil; \
22-
}
23-
2418

2519
// Lib Backend (Helpers)
2620

@@ -55,6 +49,128 @@ VALUE rb_tinytds_raise_error(DBPROCESS *dbproc, tinytds_errordata error) {
5549
return Qnil;
5650
}
5751

52+
static void rb_tinytds_client_reset_userdata(tinytds_client_userdata *userdata) {
53+
userdata->timing_out = 0;
54+
userdata->dbsql_sent = 0;
55+
userdata->dbsqlok_sent = 0;
56+
userdata->dbcancel_sent = 0;
57+
userdata->nonblocking = 0;
58+
// the following is mainly done for consistency since the values are reset accordingly in nogvl_setup/cleanup.
59+
// the nonblocking_errors array does not need to be freed here. That is done as part of nogvl_cleanup.
60+
userdata->nonblocking_errors_length = 0;
61+
userdata->nonblocking_errors_size = 0;
62+
}
63+
64+
static VALUE rb_tinytds_send_sql_to_server(tinytds_client_wrapper *cwrap, VALUE sql) {
65+
rb_tinytds_client_reset_userdata(cwrap->userdata);
66+
67+
if (cwrap->closed || cwrap->userdata->closed) { \
68+
rb_raise(cTinyTdsError, "closed connection"); \
69+
return Qnil; \
70+
}
71+
72+
dbcmd(cwrap->client, StringValueCStr(sql));
73+
if (dbsqlsend(cwrap->client) == FAIL) {
74+
rb_raise(cTinyTdsError, "failed dbsqlsend() function");
75+
}
76+
77+
cwrap->userdata->dbsql_sent = 1;
78+
}
79+
80+
// code part used to invoke FreeTDS functions with releasing the Ruby GVL
81+
// basically, while FreeTDS is interacting with the SQL server, other Ruby code can be executed
82+
#define NOGVL_DBCALL(_dbfunction, _client) ( \
83+
(RETCODE)(intptr_t)rb_thread_call_without_gvl( \
84+
(void *(*)(void *))_dbfunction, _client, \
85+
(rb_unblock_function_t*)dbcancel_ubf, _client ) \
86+
)
87+
88+
static void dbcancel_ubf(DBPROCESS *client) {
89+
GET_CLIENT_USERDATA(client);
90+
dbcancel(client);
91+
userdata->dbcancel_sent = 1;
92+
}
93+
94+
static void nogvl_setup(DBPROCESS *client) {
95+
GET_CLIENT_USERDATA(client);
96+
userdata->nonblocking = 1;
97+
userdata->nonblocking_errors_length = 0;
98+
userdata->nonblocking_errors = malloc(ERRORS_STACK_INIT_SIZE * sizeof(tinytds_errordata));
99+
userdata->nonblocking_errors_size = ERRORS_STACK_INIT_SIZE;
100+
}
101+
102+
static void nogvl_cleanup(DBPROCESS *client) {
103+
GET_CLIENT_USERDATA(client);
104+
userdata->nonblocking = 0;
105+
userdata->timing_out = 0;
106+
/*
107+
Now that the blocking operation is done, we can finally throw any
108+
exceptions based on errors from SQL Server.
109+
*/
110+
short int i;
111+
for (i = 0; i < userdata->nonblocking_errors_length; i++) {
112+
tinytds_errordata error = userdata->nonblocking_errors[i];
113+
114+
// lookahead to drain any info messages ahead of raising error
115+
if (!error.is_message) {
116+
short int j;
117+
for (j = i; j < userdata->nonblocking_errors_length; j++) {
118+
tinytds_errordata msg_error = userdata->nonblocking_errors[j];
119+
if (msg_error.is_message) {
120+
rb_tinytds_raise_error(client, msg_error);
121+
}
122+
}
123+
}
124+
125+
rb_tinytds_raise_error(client, error);
126+
}
127+
128+
free(userdata->nonblocking_errors);
129+
userdata->nonblocking_errors_length = 0;
130+
userdata->nonblocking_errors_size = 0;
131+
}
132+
133+
static RETCODE nogvl_dbnextrow(DBPROCESS * client) {
134+
int retcode = FAIL;
135+
nogvl_setup(client);
136+
retcode = NOGVL_DBCALL(dbnextrow, client);
137+
nogvl_cleanup(client);
138+
return retcode;
139+
}
140+
141+
static RETCODE nogvl_dbresults(DBPROCESS *client) {
142+
int retcode = FAIL;
143+
nogvl_setup(client);
144+
retcode = NOGVL_DBCALL(dbresults, client);
145+
nogvl_cleanup(client);
146+
return retcode;
147+
}
148+
149+
static RETCODE nogvl_dbsqlexec(DBPROCESS *client) {
150+
int retcode = FAIL;
151+
nogvl_setup(client);
152+
retcode = NOGVL_DBCALL(dbsqlexec, client);
153+
nogvl_cleanup(client);
154+
return retcode;
155+
}
156+
157+
static RETCODE nogvl_dbsqlok(DBPROCESS *client) {
158+
int retcode = FAIL;
159+
GET_CLIENT_USERDATA(client);
160+
nogvl_setup(client);
161+
retcode = NOGVL_DBCALL(dbsqlok, client);
162+
nogvl_cleanup(client);
163+
userdata->dbsqlok_sent = 1;
164+
return retcode;
165+
}
166+
167+
static RETCODE rb_tinytds_result_ok_helper(DBPROCESS *client) {
168+
GET_CLIENT_USERDATA(client);
169+
if (userdata->dbsqlok_sent == 0) {
170+
userdata->dbsqlok_retcode = nogvl_dbsqlok(client);
171+
}
172+
return userdata->dbsqlok_retcode;
173+
}
58174

59175
// Lib Backend (Memory Management & Handlers)
60176
static void push_userdata_error(tinytds_client_userdata *userdata, tinytds_errordata error) {
@@ -207,18 +323,6 @@ static int handle_interrupt(void *ptr) {
207323
return INT_CONTINUE;
208324
}
209325

210-
static void rb_tinytds_client_reset_userdata(tinytds_client_userdata *userdata) {
211-
userdata->timing_out = 0;
212-
userdata->dbsql_sent = 0;
213-
userdata->dbsqlok_sent = 0;
214-
userdata->dbcancel_sent = 0;
215-
userdata->nonblocking = 0;
216-
// the following is mainly done for consistency since the values are reset accordingly in nogvl_setup/cleanup.
217-
// the nonblocking_errors array does not need to be freed here. That is done as part of nogvl_cleanup.
218-
userdata->nonblocking_errors_length = 0;
219-
userdata->nonblocking_errors_size = 0;
220-
}
221-
222326
static void rb_tinytds_client_mark(void *ptr) {
223327
tinytds_client_wrapper *cwrap = (tinytds_client_wrapper *)ptr;
224328
if (cwrap) {
@@ -295,13 +399,7 @@ static VALUE rb_tinytds_execute(VALUE self, VALUE sql) {
295399
VALUE result;
296400

297401
GET_CLIENT_WRAPPER(self);
298-
rb_tinytds_client_reset_userdata(cwrap->userdata);
299-
REQUIRE_OPEN_CLIENT(cwrap);
300-
dbcmd(cwrap->client, StringValueCStr(sql));
301-
if (dbsqlsend(cwrap->client) == FAIL) {
302-
rb_raise(cTinyTdsError, "failed dbsqlsend() function");
303-
}
304-
cwrap->userdata->dbsql_sent = 1;
402+
rb_tinytds_send_sql_to_server(cwrap, sql);
305403
result = rb_tinytds_new_result_obj(cwrap);
306404
rb_iv_set(result, "@query_options", rb_funcall(rb_iv_get(self, "@query_options"), intern_dup, 0));
307405
{
@@ -312,6 +410,55 @@ static VALUE rb_tinytds_execute(VALUE self, VALUE sql) {
312410
}
313411
}
314412

413+
static VALUE rb_tiny_tds_insert(VALUE self, VALUE sql) {
414+
VALUE identity = Qnil;
415+
GET_CLIENT_WRAPPER(self);
416+
rb_tinytds_send_sql_to_server(cwrap, sql);
417+
418+
RETCODE dbsqlok_rc = rb_tinytds_result_ok_helper(cwrap->client);
419+
if (dbsqlok_rc == SUCCEED) {
420+
/*
421+
This is to just process each result set. Commands such as backup and
422+
restore are not done when the first result set is returned, so we need to
423+
exhaust the result sets before it is complete.
424+
*/
425+
while (nogvl_dbresults(cwrap->client) == SUCCEED) {
426+
/*
427+
If we don't loop through each row for calls to TinyTds::Result.do that
428+
actually do return result sets, we will trigger error 20019 about trying
429+
to execute a new command with pending results. Oh well.
430+
*/
431+
while (nogvl_dbnextrow(cwrap->client) != NO_MORE_ROWS);
432+
}
433+
}
434+
435+
dbcancel(cwrap->client);
436+
cwrap->userdata->dbcancel_sent = 1;
437+
cwrap->userdata->dbsql_sent = 0;
438+
439+
// prepare second query to fetch last identity
440+
dbcmd(cwrap->client, cwrap->identity_insert_sql);
441+
442+
if (
443+
nogvl_dbsqlexec(cwrap->client) != FAIL
444+
&& nogvl_dbresults(cwrap->client) != FAIL
445+
&& DBROWS(cwrap->client) != FAIL
446+
) {
447+
while (nogvl_dbnextrow(cwrap->client) != NO_MORE_ROWS) {
448+
int col = 1;
449+
BYTE *data = dbdata(cwrap->client, col);
450+
DBINT data_len = dbdatlen(cwrap->client, col);
451+
int null_val = ((data == NULL) && (data_len == 0));
452+
453+
if (!null_val) {
454+
identity = LL2NUM(*(DBBIGINT *)data);
455+
}
456+
}
457+
}
458+
459+
return identity;
460+
}
461+
315462
static VALUE rb_tinytds_charset(VALUE self) {
316463
GET_CLIENT_WRAPPER(self);
317464
return cwrap->charset;
@@ -451,6 +598,7 @@ void init_tinytds_client() {
451598
rb_define_method(cTinyTdsClient, "dead?", rb_tinytds_dead, 0);
452599
rb_define_method(cTinyTdsClient, "sqlsent?", rb_tinytds_sqlsent, 0);
453600
rb_define_method(cTinyTdsClient, "execute", rb_tinytds_execute, 1);
601+
rb_define_method(cTinyTdsClient, "insert", rb_tiny_tds_insert, 1);
454602
rb_define_method(cTinyTdsClient, "charset", rb_tinytds_charset, 0);
455603
rb_define_method(cTinyTdsClient, "encoding", rb_tinytds_encoding, 0);
456604
rb_define_method(cTinyTdsClient, "escape", rb_tinytds_escape, 1);

ext/tiny_tds/result.c

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,6 @@ static RETCODE nogvl_dbsqlok(DBPROCESS *client) {
132132
return retcode;
133133
}
134134

135-
static RETCODE nogvl_dbsqlexec(DBPROCESS *client) {
136-
int retcode = FAIL;
137-
nogvl_setup(client);
138-
retcode = NOGVL_DBCALL(dbsqlexec, client);
139-
nogvl_cleanup(client);
140-
return retcode;
141-
}
142-
143135
static RETCODE nogvl_dbresults(DBPROCESS *client) {
144136
int retcode = FAIL;
145137
nogvl_setup(client);
@@ -543,31 +535,6 @@ static VALUE rb_tinytds_result_return_code(VALUE self) {
543535
}
544536
}
545537

546-
static VALUE rb_tinytds_result_insert(VALUE self) {
547-
GET_RESULT_WRAPPER(self);
548-
if (rwrap->client) {
549-
VALUE identity = Qnil;
550-
rb_tinytds_result_exec_helper(rwrap->client);
551-
dbcmd(rwrap->client, rwrap->cwrap->identity_insert_sql);
552-
if (nogvl_dbsqlexec(rwrap->client) != FAIL
553-
&& nogvl_dbresults(rwrap->client) != FAIL
554-
&& DBROWS(rwrap->client) != FAIL) {
555-
while (nogvl_dbnextrow(rwrap->client) != NO_MORE_ROWS) {
556-
int col = 1;
557-
BYTE *data = dbdata(rwrap->client, col);
558-
DBINT data_len = dbdatlen(rwrap->client, col);
559-
int null_val = ((data == NULL) && (data_len == 0));
560-
if (!null_val)
561-
identity = LL2NUM(*(DBBIGINT *)data);
562-
}
563-
}
564-
return identity;
565-
} else {
566-
return Qnil;
567-
}
568-
}
569-
570-
571538
// Lib Init
572539

573540
void init_tinytds_result() {
@@ -584,7 +551,6 @@ void init_tinytds_result() {
584551
rb_define_method(cTinyTdsResult, "do", rb_tinytds_result_do, 0);
585552
rb_define_method(cTinyTdsResult, "affected_rows", rb_tinytds_result_affected_rows, 0);
586553
rb_define_method(cTinyTdsResult, "return_code", rb_tinytds_result_return_code, 0);
587-
rb_define_method(cTinyTdsResult, "insert", rb_tinytds_result_insert, 0);
588554
/* Intern String Helpers */
589555
intern_new = rb_intern("new");
590556
intern_utc = rb_intern("utc");

test/client_test.rb

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,4 +269,53 @@ class ClientTest < TinyTds::TestCase
269269
).must_equal 'user'
270270
end
271271
end
272+
273+
describe "#insert" do
274+
before do
275+
@client = new_connection
276+
end
277+
278+
it 'has an #insert method that cancels result rows and returns IDENTITY natively' do
279+
rollback_transaction(@client) do
280+
text = 'test scope identity rows native'
281+
@client.execute("DELETE FROM [datatypes] WHERE [varchar_50] = '#{text}'").do
282+
@client.execute("INSERT INTO [datatypes] ([varchar_50]) VALUES ('#{text}')").do
283+
sql_identity = @client.execute(@client.identity_sql).each.first['Ident']
284+
native_identity = @client.insert("INSERT INTO [datatypes] ([varchar_50]) VALUES ('#{text}')")
285+
286+
assert_equal(sql_identity + 1, native_identity)
287+
assert_client_works(@client)
288+
end
289+
end
290+
291+
it 'returns bigint for #insert when needed' do
292+
return if sqlserver_azure? # We can not alter clustered index like this test does.
293+
# 'CREATE TABLE' command is not allowed within a multi-statement transaction
294+
# and and sp_helpindex creates a temporary table #spindtab.
295+
296+
rollback_transaction(@client) do
297+
seed = 9223372036854775805
298+
@client.execute("DELETE FROM [datatypes]").do
299+
id_constraint_name = @client.execute("EXEC sp_helpindex [datatypes]").detect { |row| row['index_keys'] == 'id' }['index_name']
300+
@client.execute("ALTER TABLE [datatypes] DROP CONSTRAINT [#{id_constraint_name}]").do
301+
@client.execute("ALTER TABLE [datatypes] DROP COLUMN [id]").do
302+
@client.execute("ALTER TABLE [datatypes] ADD [id] [bigint] NOT NULL IDENTITY(1,1) PRIMARY KEY").do
303+
@client.execute("DBCC CHECKIDENT ('datatypes', RESEED, #{seed})").do
304+
identity = @client.insert("INSERT INTO [datatypes] ([varchar_50]) VALUES ('something')")
305+
306+
assert_equal(seed, identity)
307+
assert_client_works(@client)
308+
end
309+
end
310+
311+
it "throws an error if client is closed" do
312+
@client.close
313+
assert @client.closed?
314+
315+
action = lambda { @client.insert('SELECT 1 as [one]') }
316+
assert_raise_tinytds_error(action) do |e|
317+
assert_match %r{closed connection}i, e.message
318+
end
319+
end
320+
end
272321
end

0 commit comments

Comments
 (0)