tiny_tds 1.0.4 → 2.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +20 -0
  3. data/.gitattributes +1 -0
  4. data/.rubocop.yml +31 -0
  5. data/.travis.yml +25 -0
  6. data/{CHANGELOG → CHANGELOG.md} +102 -26
  7. data/Gemfile +4 -1
  8. data/ISSUE_TEMPLATE.md +35 -2
  9. data/README.md +131 -56
  10. data/Rakefile +31 -88
  11. data/VERSION +1 -1
  12. data/appveyor.yml +38 -17
  13. data/docker-compose.yml +22 -0
  14. data/ext/tiny_tds/client.c +147 -60
  15. data/ext/tiny_tds/client.h +11 -5
  16. data/ext/tiny_tds/extconf.rb +41 -297
  17. data/ext/tiny_tds/extconsts.rb +7 -7
  18. data/ext/tiny_tds/result.c +40 -15
  19. data/lib/tiny_tds/bin.rb +45 -27
  20. data/lib/tiny_tds/client.rb +46 -34
  21. data/lib/tiny_tds/error.rb +0 -1
  22. data/lib/tiny_tds/gem.rb +32 -0
  23. data/lib/tiny_tds/result.rb +2 -3
  24. data/lib/tiny_tds/version.rb +1 -1
  25. data/lib/tiny_tds.rb +38 -14
  26. data/{ports/patches/freetds/1.00 → patches/freetds/1.00.27}/0001-mingw_missing_inet_pton.diff +4 -4
  27. data/patches/freetds/1.00.27/0002-Don-t-use-MSYS2-file-libws2_32.diff +28 -0
  28. data/patches/libiconv/1.14/1-avoid-gets-error.patch +17 -0
  29. data/tasks/native_gem.rake +14 -0
  30. data/tasks/package.rake +8 -0
  31. data/tasks/ports/freetds.rb +37 -0
  32. data/tasks/ports/libiconv.rb +43 -0
  33. data/tasks/ports/openssl.rb +62 -0
  34. data/tasks/ports/recipe.rb +52 -0
  35. data/tasks/ports.rake +85 -0
  36. data/tasks/test.rake +9 -0
  37. data/test/appveyor/dbsetup.ps1 +1 -1
  38. data/test/bin/install-freetds.sh +20 -0
  39. data/test/bin/install-openssl.sh +18 -0
  40. data/test/bin/setup.sh +19 -0
  41. data/test/client_test.rb +124 -66
  42. data/test/gem_test.rb +179 -0
  43. data/test/result_test.rb +128 -42
  44. data/test/schema/sqlserver_2016.sql +140 -0
  45. data/test/schema_test.rb +23 -23
  46. data/test/test_helper.rb +65 -7
  47. data/test/thread_test.rb +1 -1
  48. data/tiny_tds.gemspec +9 -7
  49. metadata +60 -20
  50. /data/bin/{defncopy → defncopy-ttds} +0 -0
  51. /data/bin/{tsql → tsql-ttds} +0 -0
@@ -3,9 +3,9 @@
3
3
 
4
4
  VALUE cTinyTdsClient;
5
5
  extern VALUE mTinyTds, cTinyTdsError;
6
- static ID sym_username, sym_password, sym_dataserver, sym_database, sym_appname, sym_tds_version, sym_login_timeout, sym_timeout, sym_encoding, sym_azure;
6
+ static ID sym_username, sym_password, sym_dataserver, sym_database, sym_appname, sym_tds_version, sym_login_timeout, sym_timeout, sym_encoding, sym_azure, sym_contained, sym_use_utf16, sym_message_handler;
7
7
  static ID intern_source_eql, intern_severity_eql, intern_db_error_number_eql, intern_os_error_number_eql;
8
- static ID intern_new, intern_dup, intern_transpose_iconv_encoding, intern_local_offset, intern_gsub;
8
+ static ID intern_new, intern_dup, intern_transpose_iconv_encoding, intern_local_offset, intern_gsub, intern_call;
9
9
  VALUE opt_escape_regex, opt_escape_dblquote;
10
10
 
11
11
 
@@ -24,29 +24,49 @@ VALUE opt_escape_regex, opt_escape_dblquote;
24
24
 
25
25
  // Lib Backend (Helpers)
26
26
 
27
- VALUE rb_tinytds_raise_error(DBPROCESS *dbproc, int cancel, const char *error, const char *source, int severity, int dberr, int oserr) {
27
+ VALUE rb_tinytds_raise_error(DBPROCESS *dbproc, tinytds_errordata error) {
28
28
  VALUE e;
29
29
  GET_CLIENT_USERDATA(dbproc);
30
- if (cancel && !dbdead(dbproc) && userdata && !userdata->closed) {
30
+ if (error.cancel && !dbdead(dbproc) && userdata && !userdata->closed) {
31
31
  userdata->dbsqlok_sent = 1;
32
32
  dbsqlok(dbproc);
33
33
  userdata->dbcancel_sent = 1;
34
34
  dbcancel(dbproc);
35
35
  }
36
- e = rb_exc_new2(cTinyTdsError, error);
37
- rb_funcall(e, intern_source_eql, 1, rb_str_new2(source));
38
- if (severity)
39
- rb_funcall(e, intern_severity_eql, 1, INT2FIX(severity));
40
- if (dberr)
41
- rb_funcall(e, intern_db_error_number_eql, 1, INT2FIX(dberr));
42
- if (oserr)
43
- rb_funcall(e, intern_os_error_number_eql, 1, INT2FIX(oserr));
36
+ e = rb_exc_new2(cTinyTdsError, error.error);
37
+ rb_funcall(e, intern_source_eql, 1, rb_str_new2(error.source));
38
+ if (error.severity)
39
+ rb_funcall(e, intern_severity_eql, 1, INT2FIX(error.severity));
40
+ if (error.dberr)
41
+ rb_funcall(e, intern_db_error_number_eql, 1, INT2FIX(error.dberr));
42
+ if (error.oserr)
43
+ rb_funcall(e, intern_os_error_number_eql, 1, INT2FIX(error.oserr));
44
+
45
+ if (error.severity <= 10 && error.is_message) {
46
+ VALUE message_handler = userdata && userdata->message_handler ? userdata->message_handler : Qnil;
47
+ if (message_handler && message_handler != Qnil && rb_respond_to(message_handler, intern_call) != 0) {
48
+ rb_funcall(message_handler, intern_call, 1, e);
49
+ }
50
+
51
+ return Qnil;
52
+ }
53
+
44
54
  rb_exc_raise(e);
45
55
  return Qnil;
46
56
  }
47
57
 
48
58
 
49
59
  // Lib Backend (Memory Management & Handlers)
60
+ static void push_userdata_error(tinytds_client_userdata *userdata, tinytds_errordata error) {
61
+ // reallocate memory for the array as needed
62
+ if (userdata->nonblocking_errors_size == userdata->nonblocking_errors_length) {
63
+ userdata->nonblocking_errors_size *= 2;
64
+ userdata->nonblocking_errors = realloc(userdata->nonblocking_errors, userdata->nonblocking_errors_size * sizeof(tinytds_errordata));
65
+ }
66
+
67
+ userdata->nonblocking_errors[userdata->nonblocking_errors_length] = error;
68
+ userdata->nonblocking_errors_length++;
69
+ }
50
70
 
51
71
  int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, char *dberrstr, char *oserrstr) {
52
72
  static const char *source = "error";
@@ -76,6 +96,13 @@ int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, c
76
96
  but we don't ever want to automatically retry. Instead have the app
77
97
  decide what to do.
78
98
  */
99
+ if (userdata && userdata->timing_out) {
100
+ return INT_CANCEL;
101
+ }
102
+ // userdata will not be set if hitting timeout during login so check for it first
103
+ if (userdata) {
104
+ userdata->timing_out = 1;
105
+ }
79
106
  return_value = INT_TIMEOUT;
80
107
  cancel = 1;
81
108
  break;
@@ -89,6 +116,16 @@ int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, c
89
116
  break;
90
117
  }
91
118
 
119
+ tinytds_errordata error_data = {
120
+ .is_message = 0,
121
+ .cancel = cancel,
122
+ .severity = severity,
123
+ .dberr = dberr,
124
+ .oserr = oserr
125
+ };
126
+ strncpy(error_data.error, dberrstr, ERROR_MSG_SIZE);
127
+ strncpy(error_data.source, source, ERROR_MSG_SIZE);
128
+
92
129
  /*
93
130
  When in non-blocking mode we need to store the exception data to throw it
94
131
  once the blocking call returns, otherwise we will segfault ruby since part
@@ -100,26 +137,9 @@ int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, c
100
137
  dbcancel(dbproc);
101
138
  userdata->dbcancel_sent = 1;
102
139
  }
103
-
104
- /*
105
- If we've already captured an error message, don't overwrite it. This is
106
- here because FreeTDS sends a generic "General SQL Server error" message
107
- that will overwrite the real message. This is not normally a problem
108
- because a ruby exception is normally thrown and we bail before the
109
- generic message can be sent.
110
- */
111
- if (!userdata->nonblocking_error.is_set) {
112
- userdata->nonblocking_error.cancel = cancel;
113
- strcpy(userdata->nonblocking_error.error, dberrstr);
114
- strcpy(userdata->nonblocking_error.source, source);
115
- userdata->nonblocking_error.severity = severity;
116
- userdata->nonblocking_error.dberr = dberr;
117
- userdata->nonblocking_error.oserr = oserr;
118
- userdata->nonblocking_error.is_set = 1;
119
- }
120
-
140
+ push_userdata_error(userdata, error_data);
121
141
  } else {
122
- rb_tinytds_raise_error(dbproc, cancel, dberrstr, source, severity, dberr, oserr);
142
+ rb_tinytds_raise_error(dbproc, error_data);
123
143
  }
124
144
 
125
145
  return return_value;
@@ -128,36 +148,75 @@ int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, c
128
148
  int tinytds_msg_handler(DBPROCESS *dbproc, DBINT msgno, int msgstate, int severity, char *msgtext, char *srvname, char *procname, int line) {
129
149
  static const char *source = "message";
130
150
  GET_CLIENT_USERDATA(dbproc);
131
- if (severity > 10) {
132
- // See tinytds_err_handler() for info about why we do this
133
- if (userdata && userdata->nonblocking) {
134
- if (!userdata->nonblocking_error.is_set) {
135
- userdata->nonblocking_error.cancel = 1;
136
- strcpy(userdata->nonblocking_error.error, msgtext);
137
- strcpy(userdata->nonblocking_error.source, source);
138
- userdata->nonblocking_error.severity = severity;
139
- userdata->nonblocking_error.dberr = msgno;
140
- userdata->nonblocking_error.oserr = msgstate;
141
- userdata->nonblocking_error.is_set = 1;
142
- }
143
- if (!dbdead(dbproc) && !userdata->closed) {
144
- dbcancel(dbproc);
145
- userdata->dbcancel_sent = 1;
146
- }
147
- } else {
148
- rb_tinytds_raise_error(dbproc, 1, msgtext, source, severity, msgno, msgstate);
151
+
152
+ int is_message_an_error = severity > 10 ? 1 : 0;
153
+
154
+ tinytds_errordata error_data = {
155
+ .is_message = !is_message_an_error,
156
+ .cancel = is_message_an_error,
157
+ .severity = severity,
158
+ .dberr = msgno,
159
+ .oserr = msgstate
160
+ };
161
+ strncpy(error_data.error, msgtext, ERROR_MSG_SIZE);
162
+ strncpy(error_data.source, source, ERROR_MSG_SIZE);
163
+
164
+ // See tinytds_err_handler() for info about why we do this
165
+ if (userdata && userdata->nonblocking) {
166
+ /*
167
+ In the case of non-blocking command batch execution we can receive multiple messages
168
+ (including errors). We keep track of those here so they can be processed once the
169
+ non-blocking call returns.
170
+ */
171
+ push_userdata_error(userdata, error_data);
172
+
173
+ if (is_message_an_error && !dbdead(dbproc) && !userdata->closed) {
174
+ dbcancel(dbproc);
175
+ userdata->dbcancel_sent = 1;
149
176
  }
177
+ } else {
178
+ rb_tinytds_raise_error(dbproc, error_data);
150
179
  }
151
180
  return 0;
152
181
  }
153
182
 
183
+ /*
184
+ Used by dbsetinterrupt -
185
+ This gets called periodically while waiting on a read from the server
186
+ Right now, we only care about cases where a read from the server is
187
+ taking longer than the specified timeout and dbcancel is not working.
188
+ In these cases we decide that we actually want to handle the interrupt
189
+ */
190
+ static int check_interrupt(void *ptr) {
191
+ GET_CLIENT_USERDATA((DBPROCESS *)ptr);
192
+ return userdata->timing_out;
193
+ }
194
+
195
+ /*
196
+ Used by dbsetinterrupt -
197
+ This gets called if check_interrupt returns TRUE.
198
+ Right now, this is only used in cases where a read from the server is
199
+ taking longer than the specified timeout and dbcancel is not working.
200
+ Return INT_CANCEL to abort the current command batch.
201
+ */
202
+ static int handle_interrupt(void *ptr) {
203
+ GET_CLIENT_USERDATA((DBPROCESS *)ptr);
204
+ if (userdata->timing_out) {
205
+ return INT_CANCEL;
206
+ }
207
+ return INT_CONTINUE;
208
+ }
209
+
154
210
  static void rb_tinytds_client_reset_userdata(tinytds_client_userdata *userdata) {
155
211
  userdata->timing_out = 0;
156
212
  userdata->dbsql_sent = 0;
157
213
  userdata->dbsqlok_sent = 0;
158
214
  userdata->dbcancel_sent = 0;
159
215
  userdata->nonblocking = 0;
160
- userdata->nonblocking_error.is_set = 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;
161
220
  }
162
221
 
163
222
  static void rb_tinytds_client_mark(void *ptr) {
@@ -293,7 +352,7 @@ static VALUE rb_tinytds_identity_sql(VALUE self) {
293
352
 
294
353
  static VALUE rb_tinytds_connect(VALUE self, VALUE opts) {
295
354
  /* Parsing options hash to local vars. */
296
- VALUE user, pass, dataserver, database, app, version, ltimeout, timeout, charset, azure;
355
+ VALUE user, pass, dataserver, database, app, version, ltimeout, timeout, charset, azure, contained, use_utf16;
297
356
  GET_CLIENT_WRAPPER(self);
298
357
 
299
358
  user = rb_hash_aref(opts, sym_username);
@@ -306,6 +365,9 @@ static VALUE rb_tinytds_connect(VALUE self, VALUE opts) {
306
365
  timeout = rb_hash_aref(opts, sym_timeout);
307
366
  charset = rb_hash_aref(opts, sym_encoding);
308
367
  azure = rb_hash_aref(opts, sym_azure);
368
+ contained = rb_hash_aref(opts, sym_contained);
369
+ use_utf16 = rb_hash_aref(opts, sym_use_utf16);
370
+ cwrap->userdata->message_handler = rb_hash_aref(opts, sym_message_handler);
309
371
  /* Dealing with options. */
310
372
  if (dbinit() == FAIL) {
311
373
  rb_raise(cTinyTdsError, "failed dbinit() function");
@@ -324,26 +386,47 @@ static VALUE rb_tinytds_connect(VALUE self, VALUE opts) {
324
386
  dbsetlapp(cwrap->login, StringValueCStr(app));
325
387
  if (!NIL_P(ltimeout))
326
388
  dbsetlogintime(NUM2INT(ltimeout));
327
- if (!NIL_P(timeout))
328
- dbsettime(NUM2INT(timeout));
329
389
  if (!NIL_P(charset))
330
390
  DBSETLCHARSET(cwrap->login, StringValueCStr(charset));
331
- if (!NIL_P(database) && (azure == Qtrue)) {
332
- #ifdef DBSETLDBNAME
333
- DBSETLDBNAME(cwrap->login, StringValueCStr(database));
334
- #else
335
- rb_warn("TinyTds: Azure connections not supported in this version of FreeTDS.\n");
336
- #endif
391
+ if (!NIL_P(database)) {
392
+ if (azure == Qtrue || contained == Qtrue) {
393
+ #ifdef DBSETLDBNAME
394
+ DBSETLDBNAME(cwrap->login, StringValueCStr(database));
395
+ #else
396
+ if (azure == Qtrue) {
397
+ rb_warn("TinyTds: :azure option is not supported in this version of FreeTDS.\n");
398
+ }
399
+ if (contained == Qtrue) {
400
+ rb_warn("TinyTds: :contained option is not supported in this version of FreeTDS.\n");
401
+ }
402
+ #endif
403
+ }
337
404
  }
405
+ #ifdef DBSETUTF16
406
+ if (use_utf16 == Qtrue) { DBSETLUTF16(cwrap->login, 1); }
407
+ if (use_utf16 == Qfalse) { DBSETLUTF16(cwrap->login, 0); }
408
+ #else
409
+ if (use_utf16 == Qtrue || use_utf16 == Qfalse) {
410
+ rb_warning("TinyTds: Please consider upgrading to FreeTDS 0.99 or higher for better unicode support.\n");
411
+ }
412
+ #endif
413
+
338
414
  cwrap->client = dbopen(cwrap->login, StringValueCStr(dataserver));
339
415
  if (cwrap->client) {
340
- VALUE transposed_encoding;
416
+ VALUE transposed_encoding, timeout_string;
341
417
 
342
418
  cwrap->closed = 0;
343
419
  cwrap->charset = charset;
344
420
  if (!NIL_P(version))
345
421
  dbsetversion(NUM2INT(version));
422
+ if (!NIL_P(timeout)) {
423
+ timeout_string = rb_sprintf("%"PRIsVALUE"", timeout);
424
+ if (dbsetopt(cwrap->client, DBSETTIME, StringValueCStr(timeout_string), 0) == FAIL) {
425
+ dbsettime(NUM2INT(timeout));
426
+ }
427
+ }
346
428
  dbsetuserdata(cwrap->client, (BYTE*)cwrap->userdata);
429
+ dbsetinterrupt(cwrap->client, check_interrupt, handle_interrupt);
347
430
  cwrap->userdata->closed = 0;
348
431
  if (!NIL_P(database) && (azure != Qtrue)) {
349
432
  dbuse(cwrap->client, StringValueCStr(database));
@@ -391,6 +474,9 @@ void init_tinytds_client() {
391
474
  sym_timeout = ID2SYM(rb_intern("timeout"));
392
475
  sym_encoding = ID2SYM(rb_intern("encoding"));
393
476
  sym_azure = ID2SYM(rb_intern("azure"));
477
+ sym_contained = ID2SYM(rb_intern("contained"));
478
+ sym_use_utf16 = ID2SYM(rb_intern("use_utf16"));
479
+ sym_message_handler = ID2SYM(rb_intern("message_handler"));
394
480
  /* Intern TinyTds::Error Accessors */
395
481
  intern_source_eql = rb_intern("source=");
396
482
  intern_severity_eql = rb_intern("severity=");
@@ -402,6 +488,7 @@ void init_tinytds_client() {
402
488
  intern_transpose_iconv_encoding = rb_intern("transpose_iconv_encoding");
403
489
  intern_local_offset = rb_intern("local_offset");
404
490
  intern_gsub = rb_intern("gsub");
491
+ intern_call = rb_intern("call");
405
492
  /* Escape Regexp Global */
406
493
  opt_escape_regex = rb_funcall(rb_cRegexp, intern_new, 1, rb_str_new2("\\\'"));
407
494
  opt_escape_dblquote = rb_str_new2("''");
@@ -4,11 +4,14 @@
4
4
 
5
5
  void init_tinytds_client();
6
6
 
7
+ #define ERROR_MSG_SIZE 1024
8
+ #define ERRORS_STACK_INIT_SIZE 2
9
+
7
10
  typedef struct {
8
- short int is_set;
11
+ int is_message;
9
12
  int cancel;
10
- char error[1024];
11
- char source[1024];
13
+ char error[ERROR_MSG_SIZE];
14
+ char source[ERROR_MSG_SIZE];
12
15
  int severity;
13
16
  int dberr;
14
17
  int oserr;
@@ -22,7 +25,10 @@ typedef struct {
22
25
  RETCODE dbsqlok_retcode;
23
26
  short int dbcancel_sent;
24
27
  short int nonblocking;
25
- tinytds_errordata nonblocking_error;
28
+ short int nonblocking_errors_length;
29
+ short int nonblocking_errors_size;
30
+ tinytds_errordata *nonblocking_errors;
31
+ VALUE message_handler;
26
32
  } tinytds_client_userdata;
27
33
 
28
34
  typedef struct {
@@ -36,7 +42,7 @@ typedef struct {
36
42
  rb_encoding *encoding;
37
43
  } tinytds_client_wrapper;
38
44
 
39
- VALUE rb_tinytds_raise_error(DBPROCESS *dbproc, int cancel, const char *error, const char *source, int severity, int dberr, int oserr);
45
+ VALUE rb_tinytds_raise_error(DBPROCESS *dbproc, tinytds_errordata error);
40
46
 
41
47
  // Lib Macros
42
48