tiny_tds 2.1.3-x64-mingw32 → 2.1.4.pre2-x64-mingw32
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/CHANGELOG.md +6 -1
- data/README.md +13 -6
- data/VERSION +1 -1
- data/appveyor.yml +13 -1
- data/docker-compose.yml +22 -0
- data/ext/tiny_tds/client.c +92 -47
- data/ext/tiny_tds/client.h +5 -3
- data/ext/tiny_tds/result.c +22 -10
- data/test/client_test.rb +38 -19
- data/test/result_test.rb +42 -1
- data/test/test_helper.rb +57 -3
- data/tiny_tds.gemspec +1 -0
- metadata +19 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a33a28cbb91241932396a3f039b62ad3d2934853ab55b58925cff0462f051777
|
4
|
+
data.tar.gz: 512f9e8ea1728b77606a8ea40e0d494af9d5ced2793c34d9d898932caa23b54c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e9947a953f6a20512edc3a9045c70a87827da0143fecd73b2acf0320ae42bf043a11e6ccaa581ff78dde1980abfe8b8da9d6b8f72b76b6eac1813dbfa9eea857
|
7
|
+
data.tar.gz: 727ffa945c0ad7874016cfc481743015f7360126cabb87720ddf4eab1f6bb66894edb38746d22dcf2e35e729ed26a2d26707194d91d1bd3f4da1a96756e5a446
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
## (unreleased)
|
2
2
|
|
3
|
+
## 2.1.4
|
4
|
+
|
5
|
+
* Raise error if client is unable to execute statement
|
6
|
+
* Improve handling of network related timeouts
|
7
|
+
* Fix error reporting when preceeded by info message
|
8
|
+
|
3
9
|
## 2.1.3
|
4
10
|
|
5
11
|
* Removed old/unused appveyor config
|
@@ -262,4 +268,3 @@ Use both dbsetversion() vs. dbsetlversion. Partially reverts #62.
|
|
262
268
|
|
263
269
|
|
264
270
|
## 0.1.0 Initial release!
|
265
|
-
|
data/README.md
CHANGED
@@ -110,7 +110,7 @@ Creating a new client takes a hash of options. For valid iconv encoding options,
|
|
110
110
|
* :appname - Short string seen in SQL Servers process/activity window.
|
111
111
|
* :tds_version - TDS version. Defaults to "7.3".
|
112
112
|
* :login_timeout - Seconds to wait for login. Default to 60 seconds.
|
113
|
-
* :timeout - Seconds to wait for a response to a SQL command. Default 5 seconds. Prior to 1.0rc5, FreeTDS was unable to set the timeout on a per-client basis, permitting only a global timeout value. This means that if you're using an older version, the timeout values for all clients will be overwritten each time you instantiate a new `TinyTds::Client` object. If you are using 1.0rc5 or later, all clients will have an independent timeout setting as you'd expect.
|
113
|
+
* :timeout - Seconds to wait for a response to a SQL command. Default 5 seconds. Prior to 1.0rc5, FreeTDS was unable to set the timeout on a per-client basis, permitting only a global timeout value. This means that if you're using an older version, the timeout values for all clients will be overwritten each time you instantiate a new `TinyTds::Client` object. If you are using 1.0rc5 or later, all clients will have an independent timeout setting as you'd expect. Timeouts caused by network failure will raise a timeout error 1 second after the configured timeout limit is hit (see [#481](https://github.com/rails-sqlserver/tiny_tds/pull/481) for details).
|
114
114
|
* :encoding - Any valid iconv value like CP1251 or ISO-8859-1. Default UTF-8.
|
115
115
|
* :azure - Pass true to signal that you are connecting to azure.
|
116
116
|
* :contained - Pass true to signal that you are connecting with a contained database user.
|
@@ -322,9 +322,13 @@ By default row caching is turned on because the SQL Server adapter for ActiveRec
|
|
322
322
|
TinyTDS takes an opinionated stance on how we handle encoding errors. First, we treat errors differently on reads vs. writes. Our opinion is that if you are reading bad data due to your client's encoding option, you would rather just find `?` marks in your strings vs being blocked with exceptions. This is how things wold work via ODBC or SMS. On the other hand, writes will raise an exception. In this case we raise the SYBEICONVO/2402 error message which has a description of `Error converting characters into server's character set. Some character(s) could not be converted.`. Even though the severity of this message is only a `4` and TinyTDS will automatically strip/ignore unknown characters, we feel you should know that you are inserting bad encodings. In this way, a transaction can be rolled back, etc. Remember, any database write that has bad characters due to the client encoding will still be written to the database, but it is up to you rollback said write if needed. Most ORMs like ActiveRecord handle this scenario just fine.
|
323
323
|
|
324
324
|
|
325
|
+
## Timeout Error Handling
|
326
|
+
|
327
|
+
TinyTDS will raise a `TinyTDS::Error` when a timeout is reached based on the options supplied to the client. Depending on the reason for the timeout, the connection could be dead or alive. When db processing is the cause for the timeout, the connection should still be usable after the error is raised. When network failure is the cause of the timeout, the connection will be dead. If you attempt to execute another command batch on a dead connection you will see a `DBPROCESS is dead or not enabled` error. Therefore, it is recommended to check for a `dead?` connection before trying to execute another command batch.
|
328
|
+
|
325
329
|
## Binstubs
|
326
330
|
|
327
|
-
The TinyTDS gem uses binstub wrappers which mirror compiled [FreeTDS Utilities](
|
331
|
+
The TinyTDS gem uses binstub wrappers which mirror compiled [FreeTDS Utilities](https://www.freetds.org/userguide/usefreetds.html) binaries. These native executables are usually installed at the system level when installing FreeTDS. However, when using MiniPortile to install TinyTDS as we do with Windows binaries, these binstubs will find and prefer local gem `exe` directory executables. These are the following binstubs we wrap.
|
328
332
|
|
329
333
|
* tsql - Used to test connections and debug compile time settings.
|
330
334
|
* defncopy - Used to dump schema structures.
|
@@ -419,17 +423,20 @@ First, clone the repo using the command line or your Git GUI of choice.
|
|
419
423
|
$ git clone git@github.com:rails-sqlserver/tiny_tds.git
|
420
424
|
```
|
421
425
|
|
422
|
-
After that, the quickest way to get setup for development is to use [Docker](https://www.docker.com/). Assuming you have [downloaded docker](https://www.docker.com/products/docker) for your platform
|
426
|
+
After that, the quickest way to get setup for development is to use [Docker](https://www.docker.com/). Assuming you have [downloaded docker](https://www.docker.com/products/docker) for your platform, you can use [docker-compose](https://docs.docker.com/compose/install/) to run the necessary containers for testing.
|
423
427
|
|
424
428
|
```shell
|
425
|
-
$
|
429
|
+
$ docker-compose up -d
|
426
430
|
```
|
427
431
|
|
428
|
-
This will download our SQL Server for Linux Docker image based from [microsoft/mssql-server-linux/](https://hub.docker.com/r/microsoft/mssql-server-linux/). Our image already has the `[tinytdstest]` DB and `tinytds` users created. Basically, it does the following.
|
432
|
+
This will download our SQL Server for Linux Docker image based from [microsoft/mssql-server-linux/](https://hub.docker.com/r/microsoft/mssql-server-linux/). Our image already has the `[tinytdstest]` DB and `tinytds` users created. This will also download a [toxiproxy](https://github.com/shopify/toxiproxy) Docker image which we can use to simulate network failures for tests. Basically, it does the following.
|
429
433
|
|
430
434
|
```shell
|
435
|
+
$ docker network create main-network
|
431
436
|
$ docker pull metaskills/mssql-server-linux-tinytds
|
432
|
-
$ docker run -p 1433:1433 -d metaskills/mssql-server-linux-tinytds
|
437
|
+
$ docker run -p 1433:1433 -d --name sqlserver --network main-network metaskills/mssql-server-linux-tinytds
|
438
|
+
$ docker pull shopify/toxiproxy
|
439
|
+
$ docker run -p 8474:8474 -p 1234:1234 -d --name toxiproxy --network main-network shopify/toxiproxy
|
433
440
|
```
|
434
441
|
|
435
442
|
If you are using your own database. Make sure to run these SQL commands as SA to get the test database and user installed.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.1.
|
1
|
+
2.1.4.pre2
|
data/appveyor.yml
CHANGED
@@ -19,7 +19,19 @@ install:
|
|
19
19
|
- perl --version
|
20
20
|
- ruby --version
|
21
21
|
- gem --version
|
22
|
-
|
22
|
+
# Update keyring according to https://www.msys2.org/news/#2020-06-29-new-packagers
|
23
|
+
- C:\msys64\usr\bin\curl -O http://repo.msys2.org/msys/x86_64/msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz
|
24
|
+
- C:\msys64\usr\bin\curl -O http://repo.msys2.org/msys/x86_64/msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz.sig
|
25
|
+
- ridk exec bash -c "pacman-key --verify msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz.sig"
|
26
|
+
- ridk exec bash -c "pacman -U --noconfirm --config <(echo) msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz"
|
27
|
+
# Update zstd and pacman first https://github.com/msys2/MSYS2-packages/issues/2300
|
28
|
+
- C:\msys64\usr\bin\pacman --noconfirm --upgrade https://repo.msys2.org/msys/x86_64/zstd-1.4.7-1-x86_64.pkg.tar.xz # Must come First, or else pacman will install 1.4.8
|
29
|
+
- C:\msys64\usr\bin\pacman --noconfirm --upgrade https://repo.msys2.org/msys/x86_64/pacman-5.2.2-5-x86_64.pkg.tar.xz
|
30
|
+
# update packages
|
31
|
+
- C:\msys64\usr\bin\pacman --noconfirm --ask 20 --sync --refresh --refresh --sysupgrade --sysupgrade
|
32
|
+
# Kill all running msys2 binaries to avoid error "size of shared memory region changed".
|
33
|
+
# See https://github.com/msys2/MSYS2-packages/issues/258
|
34
|
+
- powershell -Command "Get-Process | Where-Object {$_.path -like 'C:\msys64*'} | Stop-Process"
|
23
35
|
# prevent freetds to link to wrong ws2_32 lib on i686-w64-mingw32
|
24
36
|
- c:/msys64/usr/bin/rm /usr/lib/w32api/libws2_32.a
|
25
37
|
# Set up project prerequisits
|
data/docker-compose.yml
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
version: '3'
|
2
|
+
|
3
|
+
networks:
|
4
|
+
main-network:
|
5
|
+
|
6
|
+
services:
|
7
|
+
mssql:
|
8
|
+
image: metaskills/mssql-server-linux-tinytds:2017-GA
|
9
|
+
container_name: sqlserver
|
10
|
+
ports:
|
11
|
+
- "1433:1433"
|
12
|
+
networks:
|
13
|
+
- main-network
|
14
|
+
|
15
|
+
toxiproxy:
|
16
|
+
image: shopify/toxiproxy
|
17
|
+
container_name: toxiproxy
|
18
|
+
ports:
|
19
|
+
- "8474:8474"
|
20
|
+
- "1234:1234"
|
21
|
+
networks:
|
22
|
+
- main-network
|
data/ext/tiny_tds/client.c
CHANGED
@@ -24,25 +24,25 @@ VALUE opt_escape_regex, opt_escape_dblquote;
|
|
24
24
|
|
25
25
|
// Lib Backend (Helpers)
|
26
26
|
|
27
|
-
VALUE rb_tinytds_raise_error(DBPROCESS *dbproc,
|
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));
|
44
|
-
|
45
|
-
if (severity <= 10 && is_message) {
|
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
46
|
VALUE message_handler = userdata && userdata->message_handler ? userdata->message_handler : Qnil;
|
47
47
|
if (message_handler && message_handler != Qnil && rb_respond_to(message_handler, intern_call) != 0) {
|
48
48
|
rb_funcall(message_handler, intern_call, 1, e);
|
@@ -57,6 +57,16 @@ VALUE rb_tinytds_raise_error(DBPROCESS *dbproc, int is_message, int cancel, cons
|
|
57
57
|
|
58
58
|
|
59
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
|
+
}
|
60
70
|
|
61
71
|
int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, char *dberrstr, char *oserrstr) {
|
62
72
|
static const char *source = "error";
|
@@ -86,7 +96,13 @@ int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, c
|
|
86
96
|
but we don't ever want to automatically retry. Instead have the app
|
87
97
|
decide what to do.
|
88
98
|
*/
|
89
|
-
|
99
|
+
if (userdata->timing_out) {
|
100
|
+
return INT_CANCEL;
|
101
|
+
}
|
102
|
+
else {
|
103
|
+
userdata->timing_out = 1;
|
104
|
+
return_value = INT_TIMEOUT;
|
105
|
+
}
|
90
106
|
cancel = 1;
|
91
107
|
break;
|
92
108
|
|
@@ -99,6 +115,16 @@ int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, c
|
|
99
115
|
break;
|
100
116
|
}
|
101
117
|
|
118
|
+
tinytds_errordata error_data = {
|
119
|
+
.is_message = 0,
|
120
|
+
.cancel = cancel,
|
121
|
+
.severity = severity,
|
122
|
+
.dberr = dberr,
|
123
|
+
.oserr = oserr
|
124
|
+
};
|
125
|
+
strncpy(error_data.error, dberrstr, ERROR_MSG_SIZE);
|
126
|
+
strncpy(error_data.source, source, ERROR_MSG_SIZE);
|
127
|
+
|
102
128
|
/*
|
103
129
|
When in non-blocking mode we need to store the exception data to throw it
|
104
130
|
once the blocking call returns, otherwise we will segfault ruby since part
|
@@ -110,27 +136,9 @@ int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, c
|
|
110
136
|
dbcancel(dbproc);
|
111
137
|
userdata->dbcancel_sent = 1;
|
112
138
|
}
|
113
|
-
|
114
|
-
/*
|
115
|
-
If we've already captured an error message, don't overwrite it. This is
|
116
|
-
here because FreeTDS sends a generic "General SQL Server error" message
|
117
|
-
that will overwrite the real message. This is not normally a problem
|
118
|
-
because a ruby exception is normally thrown and we bail before the
|
119
|
-
generic message can be sent.
|
120
|
-
*/
|
121
|
-
if (!userdata->nonblocking_error.is_set) {
|
122
|
-
userdata->nonblocking_error.is_message = 0;
|
123
|
-
userdata->nonblocking_error.cancel = cancel;
|
124
|
-
strncpy(userdata->nonblocking_error.error, dberrstr, ERROR_MSG_SIZE);
|
125
|
-
strncpy(userdata->nonblocking_error.source, source, ERROR_MSG_SIZE);
|
126
|
-
userdata->nonblocking_error.severity = severity;
|
127
|
-
userdata->nonblocking_error.dberr = dberr;
|
128
|
-
userdata->nonblocking_error.oserr = oserr;
|
129
|
-
userdata->nonblocking_error.is_set = 1;
|
130
|
-
}
|
131
|
-
|
139
|
+
push_userdata_error(userdata, error_data);
|
132
140
|
} else {
|
133
|
-
rb_tinytds_raise_error(dbproc,
|
141
|
+
rb_tinytds_raise_error(dbproc, error_data);
|
134
142
|
}
|
135
143
|
|
136
144
|
return return_value;
|
@@ -142,36 +150,72 @@ int tinytds_msg_handler(DBPROCESS *dbproc, DBINT msgno, int msgstate, int severi
|
|
142
150
|
|
143
151
|
int is_message_an_error = severity > 10 ? 1 : 0;
|
144
152
|
|
153
|
+
tinytds_errordata error_data = {
|
154
|
+
.is_message = !is_message_an_error,
|
155
|
+
.cancel = is_message_an_error,
|
156
|
+
.severity = severity,
|
157
|
+
.dberr = msgno,
|
158
|
+
.oserr = msgstate
|
159
|
+
};
|
160
|
+
strncpy(error_data.error, msgtext, ERROR_MSG_SIZE);
|
161
|
+
strncpy(error_data.source, source, ERROR_MSG_SIZE);
|
162
|
+
|
145
163
|
// See tinytds_err_handler() for info about why we do this
|
146
164
|
if (userdata && userdata->nonblocking) {
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
userdata->nonblocking_error.dberr = msgno;
|
154
|
-
userdata->nonblocking_error.oserr = msgstate;
|
155
|
-
userdata->nonblocking_error.is_set = 1;
|
156
|
-
}
|
165
|
+
/*
|
166
|
+
In the case of non-blocking command batch execution we can receive multiple messages
|
167
|
+
(including errors). We keep track of those here so they can be processed once the
|
168
|
+
non-blocking call returns.
|
169
|
+
*/
|
170
|
+
push_userdata_error(userdata, error_data);
|
157
171
|
|
158
172
|
if (is_message_an_error && !dbdead(dbproc) && !userdata->closed) {
|
159
173
|
dbcancel(dbproc);
|
160
174
|
userdata->dbcancel_sent = 1;
|
161
175
|
}
|
162
176
|
} else {
|
163
|
-
rb_tinytds_raise_error(dbproc,
|
177
|
+
rb_tinytds_raise_error(dbproc, error_data);
|
164
178
|
}
|
165
179
|
return 0;
|
166
180
|
}
|
167
181
|
|
182
|
+
/*
|
183
|
+
Used by dbsetinterrupt -
|
184
|
+
This gets called periodically while waiting on a read from the server
|
185
|
+
Right now, we only care about cases where a read from the server is
|
186
|
+
taking longer than the specified timeout and dbcancel is not working.
|
187
|
+
In these cases we decide that we actually want to handle the interrupt
|
188
|
+
*/
|
189
|
+
static int check_interrupt(void *ptr) {
|
190
|
+
GET_CLIENT_USERDATA((DBPROCESS *)ptr);
|
191
|
+
return userdata->timing_out;
|
192
|
+
}
|
193
|
+
|
194
|
+
/*
|
195
|
+
Used by dbsetinterrupt -
|
196
|
+
This gets called if check_interrupt returns TRUE.
|
197
|
+
Right now, this is only used in cases where a read from the server is
|
198
|
+
taking longer than the specified timeout and dbcancel is not working.
|
199
|
+
Return INT_CANCEL to abort the current command batch.
|
200
|
+
*/
|
201
|
+
static int handle_interrupt(void *ptr) {
|
202
|
+
GET_CLIENT_USERDATA((DBPROCESS *)ptr);
|
203
|
+
if (userdata->timing_out) {
|
204
|
+
return INT_CANCEL;
|
205
|
+
}
|
206
|
+
return INT_CONTINUE;
|
207
|
+
}
|
208
|
+
|
168
209
|
static void rb_tinytds_client_reset_userdata(tinytds_client_userdata *userdata) {
|
169
210
|
userdata->timing_out = 0;
|
170
211
|
userdata->dbsql_sent = 0;
|
171
212
|
userdata->dbsqlok_sent = 0;
|
172
213
|
userdata->dbcancel_sent = 0;
|
173
214
|
userdata->nonblocking = 0;
|
174
|
-
|
215
|
+
// the following is mainly done for consistency since the values are reset accordingly in nogvl_setup/cleanup.
|
216
|
+
// the nonblocking_errors array does not need to be freed here. That is done as part of nogvl_cleanup.
|
217
|
+
userdata->nonblocking_errors_length = 0;
|
218
|
+
userdata->nonblocking_errors_size = 0;
|
175
219
|
}
|
176
220
|
|
177
221
|
static void rb_tinytds_client_mark(void *ptr) {
|
@@ -252,8 +296,8 @@ static VALUE rb_tinytds_execute(VALUE self, VALUE sql) {
|
|
252
296
|
REQUIRE_OPEN_CLIENT(cwrap);
|
253
297
|
dbcmd(cwrap->client, StringValueCStr(sql));
|
254
298
|
if (dbsqlsend(cwrap->client) == FAIL) {
|
255
|
-
|
256
|
-
return
|
299
|
+
rb_raise(cTinyTdsError, "failed to execute statement");
|
300
|
+
return self;
|
257
301
|
}
|
258
302
|
cwrap->userdata->dbsql_sent = 1;
|
259
303
|
result = rb_tinytds_new_result_obj(cwrap);
|
@@ -381,6 +425,7 @@ static VALUE rb_tinytds_connect(VALUE self, VALUE opts) {
|
|
381
425
|
}
|
382
426
|
}
|
383
427
|
dbsetuserdata(cwrap->client, (BYTE*)cwrap->userdata);
|
428
|
+
dbsetinterrupt(cwrap->client, check_interrupt, handle_interrupt);
|
384
429
|
cwrap->userdata->closed = 0;
|
385
430
|
if (!NIL_P(database) && (azure != Qtrue)) {
|
386
431
|
dbuse(cwrap->client, StringValueCStr(database));
|
data/ext/tiny_tds/client.h
CHANGED
@@ -5,9 +5,9 @@
|
|
5
5
|
void init_tinytds_client();
|
6
6
|
|
7
7
|
#define ERROR_MSG_SIZE 1024
|
8
|
+
#define ERRORS_STACK_INIT_SIZE 2
|
8
9
|
|
9
10
|
typedef struct {
|
10
|
-
short int is_set;
|
11
11
|
int is_message;
|
12
12
|
int cancel;
|
13
13
|
char error[ERROR_MSG_SIZE];
|
@@ -25,7 +25,9 @@ typedef struct {
|
|
25
25
|
RETCODE dbsqlok_retcode;
|
26
26
|
short int dbcancel_sent;
|
27
27
|
short int nonblocking;
|
28
|
-
|
28
|
+
short int nonblocking_errors_length;
|
29
|
+
short int nonblocking_errors_size;
|
30
|
+
tinytds_errordata *nonblocking_errors;
|
29
31
|
VALUE message_handler;
|
30
32
|
} tinytds_client_userdata;
|
31
33
|
|
@@ -40,7 +42,7 @@ typedef struct {
|
|
40
42
|
rb_encoding *encoding;
|
41
43
|
} tinytds_client_wrapper;
|
42
44
|
|
43
|
-
VALUE rb_tinytds_raise_error(DBPROCESS *dbproc,
|
45
|
+
VALUE rb_tinytds_raise_error(DBPROCESS *dbproc, tinytds_errordata error);
|
44
46
|
|
45
47
|
// Lib Macros
|
46
48
|
|
data/ext/tiny_tds/result.c
CHANGED
@@ -86,26 +86,38 @@ static void dbcancel_ubf(DBPROCESS *client) {
|
|
86
86
|
static void nogvl_setup(DBPROCESS *client) {
|
87
87
|
GET_CLIENT_USERDATA(client);
|
88
88
|
userdata->nonblocking = 1;
|
89
|
+
userdata->nonblocking_errors_length = 0;
|
90
|
+
userdata->nonblocking_errors = malloc(ERRORS_STACK_INIT_SIZE * sizeof(tinytds_errordata));
|
91
|
+
userdata->nonblocking_errors_size = ERRORS_STACK_INIT_SIZE;
|
89
92
|
}
|
90
93
|
|
91
94
|
static void nogvl_cleanup(DBPROCESS *client) {
|
92
95
|
GET_CLIENT_USERDATA(client);
|
93
96
|
userdata->nonblocking = 0;
|
97
|
+
userdata->timing_out = 0;
|
94
98
|
/*
|
95
99
|
Now that the blocking operation is done, we can finally throw any
|
96
100
|
exceptions based on errors from SQL Server.
|
97
101
|
*/
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
userdata->
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
102
|
+
for (short int i = 0; i < userdata->nonblocking_errors_length; i++) {
|
103
|
+
tinytds_errordata error = userdata->nonblocking_errors[i];
|
104
|
+
|
105
|
+
// lookahead to drain any info messages ahead of raising error
|
106
|
+
if (!error.is_message) {
|
107
|
+
for (short int j = i; j < userdata->nonblocking_errors_length; j++) {
|
108
|
+
tinytds_errordata msg_error = userdata->nonblocking_errors[j];
|
109
|
+
if (msg_error.is_message) {
|
110
|
+
rb_tinytds_raise_error(client, msg_error);
|
111
|
+
}
|
112
|
+
}
|
113
|
+
}
|
114
|
+
|
115
|
+
rb_tinytds_raise_error(client, error);
|
108
116
|
}
|
117
|
+
|
118
|
+
free(userdata->nonblocking_errors);
|
119
|
+
userdata->nonblocking_errors_length = 0;
|
120
|
+
userdata->nonblocking_errors_size = 0;
|
109
121
|
}
|
110
122
|
|
111
123
|
static RETCODE nogvl_dbsqlok(DBPROCESS *client) {
|
data/test/client_test.rb
CHANGED
@@ -68,6 +68,9 @@ class ClientTest < TinyTds::TestCase
|
|
68
68
|
end
|
69
69
|
|
70
70
|
describe 'With in-valid options' do
|
71
|
+
before(:all) do
|
72
|
+
init_toxiproxy
|
73
|
+
end
|
71
74
|
|
72
75
|
it 'raises an argument error when no :host given and :dataserver is blank' do
|
73
76
|
assert_raises(ArgumentError) { new_connection :dataserver => nil, :host => nil }
|
@@ -129,30 +132,46 @@ class ClientTest < TinyTds::TestCase
|
|
129
132
|
end
|
130
133
|
end
|
131
134
|
|
132
|
-
it '
|
133
|
-
skip
|
135
|
+
it 'raises TinyTds exception with tcp socket network failure' do
|
136
|
+
skip if ENV['CI'] && ENV['APPVEYOR_BUILD_FOLDER'] # only CI using docker
|
134
137
|
begin
|
135
|
-
client = new_connection :
|
138
|
+
client = new_connection timeout: 2, port: 1234
|
136
139
|
assert_client_works(client)
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
140
|
+
action = lambda { client.execute("waitfor delay '00:00:05'").do }
|
141
|
+
|
142
|
+
# Use toxiproxy to close the TCP socket after 1 second.
|
143
|
+
# We want TinyTds to execute the statement, hit the timeout configured above, and then not be able to use the network to cancel
|
144
|
+
# the network connection needs to close after the sql batch is sent and before the timeout above is hit
|
145
|
+
Toxiproxy[:sqlserver_test].toxic(:slow_close, delay: 1000).apply do
|
146
|
+
assert_raise_tinytds_error(action) do |e|
|
147
|
+
assert_equal 20003, e.db_error_number
|
148
|
+
assert_equal 6, e.severity
|
149
|
+
assert_match %r{timed out}i, e.message, 'ignore if non-english test run'
|
150
|
+
end
|
145
151
|
end
|
146
152
|
ensure
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
153
|
+
assert_new_connections_work
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'raises TinyTds exception with dead connection network failure' do
|
158
|
+
skip if ENV['CI'] && ENV['APPVEYOR_BUILD_FOLDER'] # only CI using docker
|
159
|
+
begin
|
160
|
+
client = new_connection timeout: 2, port: 1234
|
161
|
+
assert_client_works(client)
|
162
|
+
action = lambda { client.execute("waitfor delay '00:00:05'").do }
|
163
|
+
|
164
|
+
# Use toxiproxy to close the network connection after 1 second.
|
165
|
+
# We want TinyTds to execute the statement, hit the timeout configured above, and then not be able to use the network to cancel
|
166
|
+
# the network connection needs to close after the sql batch is sent and before the timeout above is hit
|
167
|
+
Toxiproxy[:sqlserver_test].toxic(:timeout, timeout: 1000).apply do
|
168
|
+
assert_raise_tinytds_error(action) do |e|
|
169
|
+
assert_equal 20047, e.db_error_number
|
170
|
+
assert_includes [1,9], e.severity
|
171
|
+
assert_match %r{dead or not enabled}i, e.message, 'ignore if non-english test run'
|
172
|
+
end
|
154
173
|
end
|
155
|
-
|
174
|
+
ensure
|
156
175
|
assert_new_connections_work
|
157
176
|
end
|
158
177
|
end
|
data/test/result_test.rb
CHANGED
@@ -652,6 +652,48 @@ class ResultTest < TinyTds::TestCase
|
|
652
652
|
assert_equal 1, messages.length, 'there should be one message after one print statement'
|
653
653
|
assert_equal msg, m.message, 'message text'
|
654
654
|
end
|
655
|
+
|
656
|
+
it 'must raise an error preceded by a `print` message' do
|
657
|
+
messages.clear
|
658
|
+
action = lambda { @client.execute("EXEC tinytds_TestPrintWithError").do }
|
659
|
+
assert_raise_tinytds_error(action) do |e|
|
660
|
+
assert_equal 'hello', messages.first.message, 'message text'
|
661
|
+
|
662
|
+
assert_equal "Error following print", e.message
|
663
|
+
assert_equal 16, e.severity
|
664
|
+
assert_equal 50000, e.db_error_number
|
665
|
+
end
|
666
|
+
end
|
667
|
+
|
668
|
+
it 'calls the provided message handler for each of a series of `print` messages' do
|
669
|
+
messages.clear
|
670
|
+
@client.execute("EXEC tinytds_TestSeveralPrints").do
|
671
|
+
assert_equal ['hello 1', 'hello 2', 'hello 3'], messages.map { |e| e.message }, 'message list'
|
672
|
+
end
|
673
|
+
|
674
|
+
it 'should flush info messages before raising error in cases of timeout' do
|
675
|
+
@client = new_connection timeout: 1, message_handler: Proc.new { |m| messages << m }
|
676
|
+
action = lambda { @client.execute("print 'hello'; waitfor delay '00:00:02'").do }
|
677
|
+
messages.clear
|
678
|
+
assert_raise_tinytds_error(action) do |e|
|
679
|
+
assert_match %r{timed out}i, e.message, 'ignore if non-english test run'
|
680
|
+
assert_equal 6, e.severity
|
681
|
+
assert_equal 20003, e.db_error_number
|
682
|
+
assert_equal 'hello', messages.first&.message, 'message text'
|
683
|
+
end
|
684
|
+
end
|
685
|
+
|
686
|
+
it 'should print info messages before raising error in cases of timeout' do
|
687
|
+
@client = new_connection timeout: 1, message_handler: Proc.new { |m| messages << m }
|
688
|
+
action = lambda { @client.execute("raiserror('hello', 1, 1) with nowait; waitfor delay '00:00:02'").do }
|
689
|
+
messages.clear
|
690
|
+
assert_raise_tinytds_error(action) do |e|
|
691
|
+
assert_match %r{timed out}i, e.message, 'ignore if non-english test run'
|
692
|
+
assert_equal 6, e.severity
|
693
|
+
assert_equal 20003, e.db_error_number
|
694
|
+
assert_equal 'hello', messages.first&.message, 'message text'
|
695
|
+
end
|
696
|
+
end
|
655
697
|
end
|
656
698
|
|
657
699
|
it 'must not raise an error when severity is 10 or less' do
|
@@ -770,4 +812,3 @@ class ResultTest < TinyTds::TestCase
|
|
770
812
|
end
|
771
813
|
|
772
814
|
end
|
773
|
-
|
data/test/test_helper.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
require 'bundler' ; Bundler.require :development, :test
|
3
3
|
require 'tiny_tds'
|
4
4
|
require 'minitest/autorun'
|
5
|
+
require 'toxiproxy'
|
5
6
|
|
6
7
|
TINYTDS_SCHEMAS = ['sqlserver_2000', 'sqlserver_2005', 'sqlserver_2008', 'sqlserver_2014', 'sqlserver_azure', 'sybase_ase'].freeze
|
7
8
|
|
@@ -153,6 +154,8 @@ module TinyTds
|
|
153
154
|
loader.execute(drop_sql).do
|
154
155
|
loader.execute(schema_sql).do
|
155
156
|
loader.execute(sp_sql).do
|
157
|
+
loader.execute(sp_error_sql).do
|
158
|
+
loader.execute(sp_several_prints_sql).do
|
156
159
|
loader.close
|
157
160
|
true
|
158
161
|
end
|
@@ -167,7 +170,16 @@ module TinyTds
|
|
167
170
|
) DROP TABLE datatypes
|
168
171
|
IF EXISTS(
|
169
172
|
SELECT 1 FROM sysobjects WHERE type = 'P' AND name = 'tinytds_TestReturnCodes'
|
170
|
-
) DROP PROCEDURE tinytds_TestReturnCodes
|
173
|
+
) DROP PROCEDURE tinytds_TestReturnCodes
|
174
|
+
IF EXISTS(
|
175
|
+
SELECT 1 FROM sysobjects WHERE type = 'P' AND name = 'tinytds_TestPrintWithError'
|
176
|
+
) DROP PROCEDURE tinytds_TestPrintWithError
|
177
|
+
IF EXISTS(
|
178
|
+
SELECT 1 FROM sysobjects WHERE type = 'P' AND name = 'tinytds_TestPrintWithError'
|
179
|
+
) DROP PROCEDURE tinytds_TestPrintWithError
|
180
|
+
IF EXISTS(
|
181
|
+
SELECT 1 FROM sysobjects WHERE type = 'P' AND name = 'tinytds_TestSeveralPrints'
|
182
|
+
) DROP PROCEDURE tinytds_TestSeveralPrints|
|
171
183
|
end
|
172
184
|
|
173
185
|
def drop_sql_microsoft
|
@@ -181,7 +193,15 @@ module TinyTds
|
|
181
193
|
IF EXISTS (
|
182
194
|
SELECT name FROM sysobjects
|
183
195
|
WHERE name = 'tinytds_TestReturnCodes' AND type = 'P'
|
184
|
-
) DROP PROCEDURE tinytds_TestReturnCodes
|
196
|
+
) DROP PROCEDURE tinytds_TestReturnCodes
|
197
|
+
IF EXISTS (
|
198
|
+
SELECT name FROM sysobjects
|
199
|
+
WHERE name = 'tinytds_TestPrintWithError' AND type = 'P'
|
200
|
+
) DROP PROCEDURE tinytds_TestPrintWithError
|
201
|
+
IF EXISTS (
|
202
|
+
SELECT name FROM sysobjects
|
203
|
+
WHERE name = 'tinytds_TestSeveralPrints' AND type = 'P'
|
204
|
+
) DROP PROCEDURE tinytds_TestSeveralPrints|
|
185
205
|
end
|
186
206
|
|
187
207
|
def sp_sql
|
@@ -191,6 +211,21 @@ module TinyTds
|
|
191
211
|
RETURN(420) |
|
192
212
|
end
|
193
213
|
|
214
|
+
def sp_error_sql
|
215
|
+
%|CREATE PROCEDURE tinytds_TestPrintWithError
|
216
|
+
AS
|
217
|
+
PRINT 'hello'
|
218
|
+
RAISERROR('Error following print', 16, 1)|
|
219
|
+
end
|
220
|
+
|
221
|
+
def sp_several_prints_sql
|
222
|
+
%|CREATE PROCEDURE tinytds_TestSeveralPrints
|
223
|
+
AS
|
224
|
+
PRINT 'hello 1'
|
225
|
+
PRINT 'hello 2'
|
226
|
+
PRINT 'hello 3'|
|
227
|
+
end
|
228
|
+
|
194
229
|
def find_value(id, column, query_options={})
|
195
230
|
query_options[:timezone] ||= :utc
|
196
231
|
sql = "SELECT [#{column}] FROM [datatypes] WHERE [id] = #{id}"
|
@@ -212,6 +247,25 @@ module TinyTds
|
|
212
247
|
client.execute("ROLLBACK TRANSACTION").do
|
213
248
|
end
|
214
249
|
|
250
|
+
def init_toxiproxy
|
251
|
+
return if ENV['APPVEYOR_BUILD_FOLDER'] # only for CI using docker
|
252
|
+
|
253
|
+
# In order for toxiproxy to work for local docker instances of mssql, the containers must be on the same network
|
254
|
+
# and the host used below must match the mssql container name so toxiproxy knows where to proxy to.
|
255
|
+
# localhost from the perspective of toxiproxy's container is its own container an *not* the mssql container it needs to proxy to.
|
256
|
+
# docker-compose.yml handles this automatically for us. In instances where someone is using their own local mssql container they'll
|
257
|
+
# need to set up the networks manually and set TINYTDS_UNIT_HOST to their mssql container name
|
258
|
+
# For anything other than localhost just use the environment config
|
259
|
+
env_host = ENV['TINYTDS_UNIT_HOST_TEST'] || ENV['TINYTDS_UNIT_HOST'] || 'localhost'
|
260
|
+
host = ['localhost', '127.0.0.1', '0.0.0.0'].include?(env_host) ? 'sqlserver' : env_host
|
261
|
+
port = ENV['TINYTDS_UNIT_PORT'] || 1433
|
262
|
+
Toxiproxy.populate([
|
263
|
+
{
|
264
|
+
name: "sqlserver_test",
|
265
|
+
listen: "0.0.0.0:1234",
|
266
|
+
upstream: "#{host}:#{port}"
|
267
|
+
}
|
268
|
+
])
|
269
|
+
end
|
215
270
|
end
|
216
271
|
end
|
217
|
-
|
data/tiny_tds.gemspec
CHANGED
@@ -26,4 +26,5 @@ Gem::Specification.new do |s|
|
|
26
26
|
s.add_development_dependency 'rake-compiler-dock', '~> 1.0'
|
27
27
|
s.add_development_dependency 'minitest', '~> 5.6'
|
28
28
|
s.add_development_dependency 'connection_pool', '~> 2.2'
|
29
|
+
s.add_development_dependency 'toxiproxy', '~> 2.0.0'
|
29
30
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tiny_tds
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.1.
|
4
|
+
version: 2.1.4.pre2
|
5
5
|
platform: x64-mingw32
|
6
6
|
authors:
|
7
7
|
- Ken Collins
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
13
|
+
date: 2021-03-22 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: mini_portile2
|
@@ -96,6 +96,20 @@ dependencies:
|
|
96
96
|
- - "~>"
|
97
97
|
- !ruby/object:Gem::Version
|
98
98
|
version: '2.2'
|
99
|
+
- !ruby/object:Gem::Dependency
|
100
|
+
name: toxiproxy
|
101
|
+
requirement: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - "~>"
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: 2.0.0
|
106
|
+
type: :development
|
107
|
+
prerelease: false
|
108
|
+
version_requirements: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - "~>"
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: 2.0.0
|
99
113
|
description: TinyTDS - A modern, simple and fast FreeTDS library for Ruby using DB-Library.
|
100
114
|
Developed for the ActiveRecord SQL Server adapter.
|
101
115
|
email:
|
@@ -123,6 +137,7 @@ files:
|
|
123
137
|
- appveyor.yml
|
124
138
|
- bin/defncopy-ttds
|
125
139
|
- bin/tsql-ttds
|
140
|
+
- docker-compose.yml
|
126
141
|
- exe/.keep
|
127
142
|
- ext/tiny_tds/client.c
|
128
143
|
- ext/tiny_tds/client.h
|
@@ -225,9 +240,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
225
240
|
version: 2.8.dev
|
226
241
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
227
242
|
requirements:
|
228
|
-
- - "
|
243
|
+
- - ">"
|
229
244
|
- !ruby/object:Gem::Version
|
230
|
-
version:
|
245
|
+
version: 1.3.1
|
231
246
|
requirements: []
|
232
247
|
rubygems_version: 3.1.2
|
233
248
|
signing_key:
|