tiny_tds 1.0.4 → 3.2.0

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 (64) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +20 -0
  3. data/.gitattributes +1 -0
  4. data/.github/workflows/ci.yml +590 -0
  5. data/.gitignore +2 -0
  6. data/.rubocop.yml +31 -0
  7. data/{CHANGELOG → CHANGELOG.md} +133 -26
  8. data/Gemfile +1 -5
  9. data/ISSUE_TEMPLATE.md +36 -3
  10. data/README.md +147 -85
  11. data/Rakefile +51 -94
  12. data/VERSION +1 -1
  13. data/docker-compose.yml +34 -0
  14. data/ext/tiny_tds/client.c +149 -67
  15. data/ext/tiny_tds/client.h +11 -5
  16. data/ext/tiny_tds/extconf.rb +144 -283
  17. data/ext/tiny_tds/extconsts.rb +4 -11
  18. data/ext/tiny_tds/result.c +68 -50
  19. data/ext/tiny_tds/tiny_tds_ext.c +4 -1
  20. data/lib/tiny_tds/bin.rb +44 -40
  21. data/lib/tiny_tds/client.rb +63 -55
  22. data/lib/tiny_tds/error.rb +0 -3
  23. data/lib/tiny_tds/gem.rb +23 -0
  24. data/lib/tiny_tds/result.rb +0 -3
  25. data/lib/tiny_tds.rb +37 -32
  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/setup_cimgruby_dev.sh +25 -0
  30. data/start_dev.sh +21 -0
  31. data/tasks/native_gem.rake +16 -0
  32. data/tasks/package.rake +6 -0
  33. data/tasks/ports.rake +24 -0
  34. data/tasks/test.rake +7 -0
  35. data/test/bin/install-freetds.sh +18 -0
  36. data/test/bin/install-mssql.ps1 +42 -0
  37. data/test/bin/install-mssqltools.sh +9 -0
  38. data/test/bin/install-openssl.sh +18 -0
  39. data/test/bin/restore-from-native-gem.ps1 +10 -0
  40. data/test/bin/setup_tinytds_db.sh +7 -0
  41. data/test/bin/setup_volume_permissions.sh +10 -0
  42. data/test/client_test.rb +161 -112
  43. data/test/gem_test.rb +100 -0
  44. data/test/result_test.rb +293 -313
  45. data/test/schema_test.rb +369 -395
  46. data/test/sql/db-create.sql +18 -0
  47. data/test/sql/db-login.sql +38 -0
  48. data/test/test_helper.rb +116 -85
  49. data/test/thread_test.rb +22 -31
  50. data/tiny_tds.gemspec +27 -24
  51. metadata +109 -56
  52. data/appveyor.yml +0 -51
  53. data/test/appveyor/dbsetup.ps1 +0 -27
  54. data/test/appveyor/dbsetup.sql +0 -9
  55. data/test/benchmark/query.rb +0 -77
  56. data/test/benchmark/query_odbc.rb +0 -106
  57. data/test/benchmark/query_tinytds.rb +0 -126
  58. data/test/schema/sqlserver_2000.sql +0 -140
  59. data/test/schema/sqlserver_2005.sql +0 -140
  60. data/test/schema/sqlserver_2014.sql +0 -140
  61. data/test/schema/sybase_ase.sql +0 -138
  62. /data/bin/{defncopy → defncopy-ttds} +0 -0
  63. /data/bin/{tsql → tsql-ttds} +0 -0
  64. /data/test/schema/{sqlserver_2008.sql → sqlserver_2017.sql} +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) {
@@ -173,6 +232,7 @@ static void rb_tinytds_client_free(void *ptr) {
173
232
  dbloginfree(cwrap->login);
174
233
  if (cwrap->client && !cwrap->closed) {
175
234
  dbclose(cwrap->client);
235
+ cwrap->client = NULL;
176
236
  cwrap->closed = 1;
177
237
  cwrap->userdata->closed = 1;
178
238
  }
@@ -204,6 +264,7 @@ static VALUE rb_tinytds_close(VALUE self) {
204
264
  GET_CLIENT_WRAPPER(self);
205
265
  if (cwrap->client && !cwrap->closed) {
206
266
  dbclose(cwrap->client);
267
+ cwrap->client = NULL;
207
268
  cwrap->closed = 1;
208
269
  cwrap->userdata->closed = 1;
209
270
  }
@@ -238,8 +299,7 @@ static VALUE rb_tinytds_execute(VALUE self, VALUE sql) {
238
299
  REQUIRE_OPEN_CLIENT(cwrap);
239
300
  dbcmd(cwrap->client, StringValueCStr(sql));
240
301
  if (dbsqlsend(cwrap->client) == FAIL) {
241
- rb_warn("TinyTds: dbsqlsend() returned FAIL.\n");
242
- return Qfalse;
302
+ rb_raise(cTinyTdsError, "failed dbsqlsend() function");
243
303
  }
244
304
  cwrap->userdata->dbsql_sent = 1;
245
305
  result = rb_tinytds_new_result_obj(cwrap);
@@ -293,7 +353,7 @@ static VALUE rb_tinytds_identity_sql(VALUE self) {
293
353
 
294
354
  static VALUE rb_tinytds_connect(VALUE self, VALUE opts) {
295
355
  /* Parsing options hash to local vars. */
296
- VALUE user, pass, dataserver, database, app, version, ltimeout, timeout, charset, azure;
356
+ VALUE user, pass, dataserver, database, app, version, ltimeout, timeout, charset, azure, contained, use_utf16;
297
357
  GET_CLIENT_WRAPPER(self);
298
358
 
299
359
  user = rb_hash_aref(opts, sym_username);
@@ -306,6 +366,9 @@ static VALUE rb_tinytds_connect(VALUE self, VALUE opts) {
306
366
  timeout = rb_hash_aref(opts, sym_timeout);
307
367
  charset = rb_hash_aref(opts, sym_encoding);
308
368
  azure = rb_hash_aref(opts, sym_azure);
369
+ contained = rb_hash_aref(opts, sym_contained);
370
+ use_utf16 = rb_hash_aref(opts, sym_use_utf16);
371
+ cwrap->userdata->message_handler = rb_hash_aref(opts, sym_message_handler);
309
372
  /* Dealing with options. */
310
373
  if (dbinit() == FAIL) {
311
374
  rb_raise(cTinyTdsError, "failed dbinit() function");
@@ -324,37 +387,52 @@ static VALUE rb_tinytds_connect(VALUE self, VALUE opts) {
324
387
  dbsetlapp(cwrap->login, StringValueCStr(app));
325
388
  if (!NIL_P(ltimeout))
326
389
  dbsetlogintime(NUM2INT(ltimeout));
327
- if (!NIL_P(timeout))
328
- dbsettime(NUM2INT(timeout));
329
390
  if (!NIL_P(charset))
330
391
  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
392
+ if (!NIL_P(database)) {
393
+ if (azure == Qtrue || contained == Qtrue) {
394
+ #ifdef DBSETLDBNAME
395
+ DBSETLDBNAME(cwrap->login, StringValueCStr(database));
396
+ #else
397
+ if (azure == Qtrue) {
398
+ rb_warn("TinyTds: :azure option is not supported in this version of FreeTDS.\n");
399
+ }
400
+ if (contained == Qtrue) {
401
+ rb_warn("TinyTds: :contained option is not supported in this version of FreeTDS.\n");
402
+ }
403
+ #endif
404
+ }
337
405
  }
406
+ if (use_utf16 == Qtrue) { DBSETLUTF16(cwrap->login, 1); }
407
+ if (use_utf16 == Qfalse) { DBSETLUTF16(cwrap->login, 0); }
408
+
338
409
  cwrap->client = dbopen(cwrap->login, StringValueCStr(dataserver));
339
410
  if (cwrap->client) {
340
- VALUE transposed_encoding;
411
+ if (dbtds(cwrap->client) < 11) {
412
+ rb_raise(cTinyTdsError, "connecting with a TDS version older than 7.3!");
413
+ }
414
+
415
+ VALUE transposed_encoding, timeout_string;
341
416
 
342
417
  cwrap->closed = 0;
343
418
  cwrap->charset = charset;
344
419
  if (!NIL_P(version))
345
420
  dbsetversion(NUM2INT(version));
421
+ if (!NIL_P(timeout)) {
422
+ timeout_string = rb_sprintf("%"PRIsVALUE"", timeout);
423
+ if (dbsetopt(cwrap->client, DBSETTIME, StringValueCStr(timeout_string), 0) == FAIL) {
424
+ dbsettime(NUM2INT(timeout));
425
+ }
426
+ }
346
427
  dbsetuserdata(cwrap->client, (BYTE*)cwrap->userdata);
428
+ dbsetinterrupt(cwrap->client, check_interrupt, handle_interrupt);
347
429
  cwrap->userdata->closed = 0;
348
430
  if (!NIL_P(database) && (azure != Qtrue)) {
349
431
  dbuse(cwrap->client, StringValueCStr(database));
350
432
  }
351
433
  transposed_encoding = rb_funcall(cTinyTdsClient, intern_transpose_iconv_encoding, 1, charset);
352
434
  cwrap->encoding = rb_enc_find(StringValueCStr(transposed_encoding));
353
- if (dbtds(cwrap->client) <= 7) {
354
- cwrap->identity_insert_sql = "SELECT CAST(@@IDENTITY AS bigint) AS Ident";
355
- } else {
356
- cwrap->identity_insert_sql = "SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident";
357
- }
435
+ cwrap->identity_insert_sql = "SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident";
358
436
  }
359
437
  return self;
360
438
  }
@@ -391,6 +469,9 @@ void init_tinytds_client() {
391
469
  sym_timeout = ID2SYM(rb_intern("timeout"));
392
470
  sym_encoding = ID2SYM(rb_intern("encoding"));
393
471
  sym_azure = ID2SYM(rb_intern("azure"));
472
+ sym_contained = ID2SYM(rb_intern("contained"));
473
+ sym_use_utf16 = ID2SYM(rb_intern("use_utf16"));
474
+ sym_message_handler = ID2SYM(rb_intern("message_handler"));
394
475
  /* Intern TinyTds::Error Accessors */
395
476
  intern_source_eql = rb_intern("source=");
396
477
  intern_severity_eql = rb_intern("severity=");
@@ -402,6 +483,7 @@ void init_tinytds_client() {
402
483
  intern_transpose_iconv_encoding = rb_intern("transpose_iconv_encoding");
403
484
  intern_local_offset = rb_intern("local_offset");
404
485
  intern_gsub = rb_intern("gsub");
486
+ intern_call = rb_intern("call");
405
487
  /* Escape Regexp Global */
406
488
  opt_escape_regex = rb_funcall(rb_cRegexp, intern_new, 1, rb_str_new2("\\\'"));
407
489
  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