tiny_tds 2.1.4.pre → 2.1.4.pre2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef24128290a7b007a49222ec2d4307a834930f2aa6606fc9bc2b464b2d7400bb
4
- data.tar.gz: e960731768896511f5592e4abd9768df0cf4a63ba089dd7c391bf845c0924788
3
+ metadata.gz: 87a9fdb063e15f668ba85a3efd1c40f8b05cce8e38a4969a5c21175ad1392761
4
+ data.tar.gz: ad2c0b72448fd1ddab27dfbfee62329be786d826188de35c86bc3d8c542a0a13
5
5
  SHA512:
6
- metadata.gz: 44a3b20f0bb91e24d325305fff06046cecc57d16fc6d8f8f7cc530e12689444b1f6b2c6346857cd11d779e0c30c64ebd856a0c9a9b473fdc1ffcc29edf356f30
7
- data.tar.gz: 55c8753d62b46cb1f745f2d6b0a9a3f5e270e83f7a956dc03be540e4ed08aa1a7754c4c6462ba9ed58ad15c37c9f8a5be8f1b8236996999dec13d425cf9aadc5
6
+ metadata.gz: dd0f15ad1042902fe858e662375256389d0b8738c4815280f30b13b80147391ebc3583e840b5eb3709f158c3907742c90f303e6a0dc4b48b2b48f21bba9c349a
7
+ data.tar.gz: d127aeb91246d9024c38d0617c3d634b8f5848deb5e63cb8a70d38b63f03009e463a13175d71be6d9a4e10ad2e409ff5542c306870076e31ce1f2b7859dfc474
data/.travis.yml CHANGED
@@ -14,9 +14,9 @@ rvm:
14
14
  - 2.7.0
15
15
  before_install:
16
16
  - docker info
17
+ - docker-compose up -d
17
18
  - sudo ./test/bin/install-openssl.sh
18
19
  - sudo ./test/bin/install-freetds.sh
19
- - sudo ./test/bin/setup.sh
20
20
  install:
21
21
  - gem install bundler
22
22
  - bundle --version
data/CHANGELOG.md CHANGED
@@ -2,8 +2,9 @@
2
2
 
3
3
  ## 2.1.4
4
4
 
5
- * todo: changelog for info messages
6
- * todo: changelog for network timeouts
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
7
8
 
8
9
  ## 2.1.3
9
10
 
@@ -267,4 +268,3 @@ Use both dbsetversion() vs. dbsetlversion. Partially reverts #62.
267
268
 
268
269
 
269
270
  ## 0.1.0 Initial release!
270
-
data/README.md CHANGED
@@ -328,7 +328,7 @@ TinyTDS will raise a `TinyTDS::Error` when a timeout is reached based on the opt
328
328
 
329
329
  ## Binstubs
330
330
 
331
- The TinyTDS gem uses binstub wrappers which mirror compiled [FreeTDS Utilities](http://www.freetds.org/userguide/usefreetds.htm) 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.
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.
332
332
 
333
333
  * tsql - Used to test connections and debug compile time settings.
334
334
  * defncopy - Used to dump schema structures.
@@ -423,17 +423,20 @@ First, clone the repo using the command line or your Git GUI of choice.
423
423
  $ git clone git@github.com:rails-sqlserver/tiny_tds.git
424
424
  ```
425
425
 
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 and you have , you can run our test setup script.
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.
427
427
 
428
428
  ```shell
429
- $ ./test/bin/setup.sh
429
+ $ docker-compose up -d
430
430
  ```
431
431
 
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. 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.
433
433
 
434
434
  ```shell
435
+ $ docker network create main-network
435
436
  $ docker pull metaskills/mssql-server-linux-tinytds
436
- $ 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
437
440
  ```
438
441
 
439
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.4.pre
1
+ 2.1.4.pre2
@@ -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
@@ -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, int is_message, 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));
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);
@@ -115,6 +115,16 @@ int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, c
115
115
  break;
116
116
  }
117
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
+
118
128
  /*
119
129
  When in non-blocking mode we need to store the exception data to throw it
120
130
  once the blocking call returns, otherwise we will segfault ruby since part
@@ -126,19 +136,9 @@ int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, c
126
136
  dbcancel(dbproc);
127
137
  userdata->dbcancel_sent = 1;
128
138
  }
129
-
130
- tinytds_errordata error = {
131
- .is_message = 0,
132
- .cancel = cancel,
133
- .severity = severity,
134
- .dberr = dberr,
135
- .oserr = oserr
136
- };
137
- strncpy(error.error, dberrstr, ERROR_MSG_SIZE);
138
- strncpy(error.source, source, ERROR_MSG_SIZE);
139
- push_userdata_error(userdata, error);
139
+ push_userdata_error(userdata, error_data);
140
140
  } else {
141
- rb_tinytds_raise_error(dbproc, 0, cancel, dberrstr, source, severity, dberr, oserr);
141
+ rb_tinytds_raise_error(dbproc, error_data);
142
142
  }
143
143
 
144
144
  return return_value;
@@ -150,6 +150,16 @@ int tinytds_msg_handler(DBPROCESS *dbproc, DBINT msgno, int msgstate, int severi
150
150
 
151
151
  int is_message_an_error = severity > 10 ? 1 : 0;
152
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
+
153
163
  // See tinytds_err_handler() for info about why we do this
154
164
  if (userdata && userdata->nonblocking) {
155
165
  /*
@@ -157,23 +167,14 @@ int tinytds_msg_handler(DBPROCESS *dbproc, DBINT msgno, int msgstate, int severi
157
167
  (including errors). We keep track of those here so they can be processed once the
158
168
  non-blocking call returns.
159
169
  */
160
- tinytds_errordata error = {
161
- .is_message = !is_message_an_error,
162
- .cancel = is_message_an_error,
163
- .severity = severity,
164
- .dberr = msgno,
165
- .oserr = msgstate
166
- };
167
- strncpy(error.error, msgtext, ERROR_MSG_SIZE);
168
- strncpy(error.source, source, ERROR_MSG_SIZE);
169
- push_userdata_error(userdata, error);
170
+ push_userdata_error(userdata, error_data);
170
171
 
171
172
  if (is_message_an_error && !dbdead(dbproc) && !userdata->closed) {
172
173
  dbcancel(dbproc);
173
174
  userdata->dbcancel_sent = 1;
174
175
  }
175
176
  } else {
176
- rb_tinytds_raise_error(dbproc, !is_message_an_error, is_message_an_error, msgtext, source, severity, msgno, msgstate);
177
+ rb_tinytds_raise_error(dbproc, error_data);
177
178
  }
178
179
  return 0;
179
180
  }
@@ -295,8 +296,8 @@ static VALUE rb_tinytds_execute(VALUE self, VALUE sql) {
295
296
  REQUIRE_OPEN_CLIENT(cwrap);
296
297
  dbcmd(cwrap->client, StringValueCStr(sql));
297
298
  if (dbsqlsend(cwrap->client) == FAIL) {
298
- rb_warn("TinyTds: dbsqlsend() returned FAIL.\n");
299
- return Qfalse;
299
+ rb_raise(cTinyTdsError, "failed to execute statement");
300
+ return self;
300
301
  }
301
302
  cwrap->userdata->dbsql_sent = 1;
302
303
  result = rb_tinytds_new_result_obj(cwrap);
@@ -42,7 +42,7 @@ typedef struct {
42
42
  rb_encoding *encoding;
43
43
  } tinytds_client_wrapper;
44
44
 
45
- VALUE rb_tinytds_raise_error(DBPROCESS *dbproc, int is_message, 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);
46
46
 
47
47
  // Lib Macros
48
48
 
@@ -101,15 +101,18 @@ static void nogvl_cleanup(DBPROCESS *client) {
101
101
  */
102
102
  for (short int i = 0; i < userdata->nonblocking_errors_length; i++) {
103
103
  tinytds_errordata error = userdata->nonblocking_errors[i];
104
- rb_tinytds_raise_error(client,
105
- error.is_message,
106
- error.cancel,
107
- error.error,
108
- error.source,
109
- error.severity,
110
- error.dberr,
111
- error.oserr
112
- );
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);
113
116
  }
114
117
 
115
118
  free(userdata->nonblocking_errors);
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,27 +132,46 @@ class ClientTest < TinyTds::TestCase
129
132
  end
130
133
  end
131
134
 
132
- it 'raises TinyTds exception with sql batch timeout due to network failure' do
135
+ it 'raises TinyTds exception with tcp socket network failure' do
133
136
  skip if ENV['CI'] && ENV['APPVEYOR_BUILD_FOLDER'] # only CI using docker
134
137
  begin
135
- client = new_connection timeout: 2
138
+ client = new_connection timeout: 2, port: 1234
136
139
  assert_client_works(client)
137
- docker_container('pause', wait_for: 1)
138
- action = lambda { client.execute('SELECT 1 as [one]').each }
139
- assert_raise_tinytds_error(action) do |e|
140
- assert_equal 20003, e.db_error_number
141
- assert_equal 6, e.severity
142
- assert_match %r{timed out}i, e.message, 'ignore if non-english test run'
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
143
151
  end
144
152
  ensure
145
- docker_container('unpause', wait_for: 1)
146
- action = lambda { client.execute('SELECT 1 as [one]').each }
147
- assert_raise_tinytds_error(action) do |e|
148
- assert_equal 20047, e.db_error_number
149
- assert_includes [1,9], e.severity
150
- assert_match %r{dead or not enabled}i, e.message, 'ignore if non-english test run'
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
151
173
  end
152
- close_client(client)
174
+ ensure
153
175
  assert_new_connections_work
154
176
  end
155
177
  end
data/test/result_test.rb CHANGED
@@ -670,6 +670,30 @@ class ResultTest < TinyTds::TestCase
670
670
  @client.execute("EXEC tinytds_TestSeveralPrints").do
671
671
  assert_equal ['hello 1', 'hello 2', 'hello 3'], messages.map { |e| e.message }, 'message list'
672
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
673
697
  end
674
698
 
675
699
  it 'must not raise an error when severity is 10 or less' do
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
 
@@ -246,9 +247,25 @@ module TinyTds
246
247
  client.execute("ROLLBACK TRANSACTION").do
247
248
  end
248
249
 
249
- def docker_container(cmd, wait_for: 0)
250
- system("docker #{cmd} $(docker ps --format '{{.Names}}' --filter 'ancestor=metaskills/mssql-server-linux-tinytds:2017-GA') > /dev/null")
251
- sleep(wait_for) if wait_for > 0
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
+ ])
252
269
  end
253
270
  end
254
271
  end
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.pre
4
+ version: 2.1.4.pre2
5
5
  platform: ruby
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: 2021-01-19 00:00:00.000000000 Z
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:
@@ -124,6 +138,7 @@ files:
124
138
  - appveyor.yml
125
139
  - bin/defncopy-ttds
126
140
  - bin/tsql-ttds
141
+ - docker-compose.yml
127
142
  - exe/.keep
128
143
  - ext/tiny_tds/client.c
129
144
  - ext/tiny_tds/client.h