trino-client 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.github/CODEOWNERS +1 -0
  3. data/.github/PULL_REQUEST_TEMPLATE.md +18 -0
  4. data/.github/workflows/ruby.yml +30 -0
  5. data/.gitignore +4 -0
  6. data/ChangeLog.md +168 -0
  7. data/Gemfile +7 -0
  8. data/LICENSE +202 -0
  9. data/README.md +131 -0
  10. data/Rakefile +45 -0
  11. data/lib/trino-client.rb +1 -0
  12. data/lib/trino/client.rb +23 -0
  13. data/lib/trino/client/client.rb +78 -0
  14. data/lib/trino/client/errors.rb +46 -0
  15. data/lib/trino/client/faraday_client.rb +242 -0
  16. data/lib/trino/client/model_versions/0.149.rb +1683 -0
  17. data/lib/trino/client/model_versions/0.153.rb +1719 -0
  18. data/lib/trino/client/model_versions/0.173.rb +1685 -0
  19. data/lib/trino/client/model_versions/0.178.rb +1964 -0
  20. data/lib/trino/client/model_versions/0.205.rb +2169 -0
  21. data/lib/trino/client/model_versions/303.rb +2574 -0
  22. data/lib/trino/client/model_versions/316.rb +2595 -0
  23. data/lib/trino/client/model_versions/351.rb +2726 -0
  24. data/lib/trino/client/models.rb +38 -0
  25. data/lib/trino/client/query.rb +144 -0
  26. data/lib/trino/client/statement_client.rb +279 -0
  27. data/lib/trino/client/version.rb +20 -0
  28. data/modelgen/model_versions.rb +280 -0
  29. data/modelgen/modelgen.rb +119 -0
  30. data/modelgen/models.rb +31 -0
  31. data/modelgen/trino_models.rb +270 -0
  32. data/release.rb +56 -0
  33. data/spec/basic_query_spec.rb +82 -0
  34. data/spec/client_spec.rb +75 -0
  35. data/spec/gzip_spec.rb +40 -0
  36. data/spec/model_spec.rb +35 -0
  37. data/spec/spec_helper.rb +42 -0
  38. data/spec/statement_client_spec.rb +637 -0
  39. data/spec/tpch/q01.sql +21 -0
  40. data/spec/tpch/q02.sql +43 -0
  41. data/spec/tpch_query_spec.rb +41 -0
  42. data/trino-client.gemspec +31 -0
  43. metadata +211 -0
data/spec/gzip_spec.rb ADDED
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+
3
+ describe Trino::Client::Client do
4
+ before(:all) do
5
+ @spec_path = File.dirname(__FILE__)
6
+ WebMock.disable!
7
+ @cluster = TinyPresto::Cluster.new()
8
+ @container = @cluster.run
9
+ @client = Trino::Client.new(server: 'localhost:8080', catalog: 'tpch', user: 'test-user', schema: 'tiny', gzip: true, http_debug: true)
10
+ loop do
11
+ begin
12
+ # Make sure to all workers are available.
13
+ @client.run('show schemas')
14
+ break
15
+ rescue StandardError => exception
16
+ puts "Waiting for cluster ready... #{exception}"
17
+ sleep(3)
18
+ end
19
+ end
20
+ puts 'Cluster is ready'
21
+ end
22
+
23
+ after(:all) do
24
+ @cluster.stop
25
+ WebMock.enable!
26
+ end
27
+
28
+ it 'tpch q01 with gzip option' do
29
+ $stdout = StringIO.new
30
+ begin
31
+ q = File.read("#{@spec_path}/tpch/q01.sql")
32
+ columns, rows = run_with_retry(@client, q)
33
+ expect(columns.length).to be(10)
34
+ expect(rows.length).to be(4)
35
+ expect($stdout.string).to include ('content-encoding: "gzip"')
36
+ ensure
37
+ $stdout = STDOUT
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe Trino::Client::Models do
4
+ describe 'rehash of BlockedReason' do
5
+ h = {
6
+ "operatorId" => 0,
7
+ "planNodeId" => "47",
8
+ "operatorType" => "ScanFilterAndProjectOperator",
9
+ "addInputCalls" => 0,
10
+ "addInputWall" => "0.00ns",
11
+ "addInputCpu" => "0.00ns",
12
+ "addInputUser" => "0.00ns",
13
+ "inputDataSize" => "9.46MB",
14
+ "inputPositions" => 440674,
15
+ "getOutputCalls" => 734,
16
+ "getOutputWall" => "7.29s",
17
+ "getOutputCpu" => "0.00ns",
18
+ "getOutputUser" => "0.00ns",
19
+ "outputDataSize" => "36.99MB",
20
+ "outputPositions" => 440674,
21
+ "blockedWall" => "0.00ns",
22
+ "finishCalls" => 0,
23
+ "finishWall" => "0.00ns",
24
+ "finishCpu" => "0.00ns",
25
+ "finishUser" => "0.00ns",
26
+ "memoryReservation" => "0B",
27
+ "systemMemoryReservation" => "0b",
28
+ "blockedReason" => "WAITING_FOR_MEMORY",
29
+ "info" => {"k" => "v"}
30
+ }
31
+
32
+ stats = Models::OperatorStats.decode(h)
33
+ stats.blocked_reason.should == :waiting_for_memory
34
+ end
35
+ end
@@ -0,0 +1,42 @@
1
+ require 'bundler'
2
+
3
+ begin
4
+ Bundler.setup(:default, :test)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+
11
+ require 'simplecov'
12
+ SimpleCov.start
13
+
14
+ require 'json'
15
+ require 'webmock/rspec'
16
+
17
+ require 'trino-client'
18
+ include Trino::Client
19
+
20
+ require 'tiny-presto'
21
+
22
+ MAX_RETRY_COUNT = 5
23
+ RETRYABLE_ERRORS = [
24
+ /No nodes available to run query/
25
+ ]
26
+
27
+ def run_with_retry(client, sql)
28
+ i = 0
29
+ while i < MAX_RETRY_COUNT
30
+ begin
31
+ columns, rows = @client.run(sql)
32
+ return columns, rows
33
+ rescue Trino::Client::TrinoQueryError => e
34
+ if RETRYABLE_ERRORS.any? { |error| e.message =~ error }
35
+ sleep(i)
36
+ i += 1
37
+ next
38
+ end
39
+ raise "Fail to run query: #{e}"
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,637 @@
1
+ require 'spec_helper'
2
+
3
+ describe Trino::Client::StatementClient do
4
+ let :options do
5
+ {
6
+ server: "localhost",
7
+ user: "frsyuki",
8
+ catalog: "native",
9
+ schema: "default",
10
+ time_zone: "US/Pacific",
11
+ language: "ja_JP",
12
+ debug: true,
13
+ follow_redirect: true
14
+ }
15
+ end
16
+
17
+ let :query do
18
+ "select * from sys.node"
19
+ end
20
+
21
+ let :response_json do
22
+ {
23
+ id: "queryid",
24
+ stats: {}
25
+ }
26
+ end
27
+
28
+ let :faraday do
29
+ Trino::Client.faraday_client(options)
30
+ end
31
+
32
+ it "sets headers" do
33
+ stub_request(:post, "localhost/v1/statement").
34
+ with(body: query,
35
+ headers: {
36
+ "User-Agent" => "trino-ruby/#{VERSION}",
37
+ "X-Trino-Catalog" => options[:catalog],
38
+ "X-Trino-Schema" => options[:schema],
39
+ "X-Trino-User" => options[:user],
40
+ "X-Trino-Language" => options[:language],
41
+ "X-Trino-Time-Zone" => options[:time_zone],
42
+ }).to_return(body: response_json.to_json)
43
+
44
+ StatementClient.new(faraday, query, options)
45
+ end
46
+
47
+ let :response_json2 do
48
+ {
49
+ id: "queryid",
50
+ nextUri: 'http://localhost/v1/next_uri',
51
+ stats: {}
52
+ }
53
+ end
54
+
55
+ it "sets headers" do
56
+ retry_p = false
57
+ stub_request(:post, "localhost/v1/statement").
58
+ with(body: query,
59
+ headers: {
60
+ "User-Agent" => "trino-ruby/#{VERSION}",
61
+ "X-Trino-Catalog" => options[:catalog],
62
+ "X-Trino-Schema" => options[:schema],
63
+ "X-Trino-User" => options[:user],
64
+ "X-Trino-Language" => options[:language],
65
+ "X-Trino-Time-Zone" => options[:time_zone],
66
+ }).to_return(body: response_json2.to_json)
67
+
68
+ stub_request(:get, "localhost/v1/next_uri").
69
+ with(headers: {
70
+ "User-Agent" => "trino-ruby/#{VERSION}",
71
+ "X-Trino-Catalog" => options[:catalog],
72
+ "X-Trino-Schema" => options[:schema],
73
+ "X-Trino-User" => options[:user],
74
+ "X-Trino-Language" => options[:language],
75
+ "X-Trino-Time-Zone" => options[:time_zone],
76
+ }).to_return(body: lambda{|req|if retry_p; response_json.to_json; else; retry_p=true; raise Timeout::Error.new("execution expired"); end })
77
+
78
+ sc = StatementClient.new(faraday, query, options.merge(http_open_timeout: 1))
79
+ sc.has_next?.should be_true
80
+ sc.advance.should be_true
81
+ retry_p.should be_true
82
+ end
83
+
84
+ it "uses 'Accept: application/x-msgpack' if option is set" do
85
+ retry_p = false
86
+ stub_request(:post, "localhost/v1/statement").
87
+ with(body: query,
88
+ headers: {
89
+ "User-Agent" => "trino-ruby/#{VERSION}",
90
+ "X-Trino-Catalog" => options[:catalog],
91
+ "X-Trino-Schema" => options[:schema],
92
+ "X-Trino-User" => options[:user],
93
+ "X-Trino-Language" => options[:language],
94
+ "X-Trino-Time-Zone" => options[:time_zone],
95
+ "Accept" => "application/x-msgpack,application/json"
96
+ }).to_return(body: MessagePack.dump(response_json2), headers: {"Content-Type" => "application/x-msgpack"})
97
+
98
+ stub_request(:get, "localhost/v1/next_uri").
99
+ with(headers: {
100
+ "User-Agent" => "trino-ruby/#{VERSION}",
101
+ "X-Trino-Catalog" => options[:catalog],
102
+ "X-Trino-Schema" => options[:schema],
103
+ "X-Trino-User" => options[:user],
104
+ "X-Trino-Language" => options[:language],
105
+ "X-Trino-Time-Zone" => options[:time_zone],
106
+ "Accept" => "application/x-msgpack,application/json"
107
+ }).to_return(body: lambda{|req|if retry_p; MessagePack.dump(response_json); else; retry_p=true; raise Timeout::Error.new("execution expired"); end }, headers: {"Content-Type" => "application/x-msgpack"})
108
+
109
+ options.merge!(http_open_timeout: 1, enable_x_msgpack: "application/x-msgpack")
110
+ sc = StatementClient.new(faraday, query, options)
111
+ sc.has_next?.should be_true
112
+ sc.advance.should be_true
113
+ retry_p.should be_true
114
+ end
115
+
116
+ # trino version could be "V0_ddd" or "Vddd"
117
+ /\Trino::Client::ModelVersions::V(\w+)/ =~ Trino::Client::Models.to_s
118
+
119
+ # https://github.com/prestosql/presto/commit/80a2c5113d47e3390bf6dc041486a1c9dfc04592
120
+ # renamed DeleteHandle to DeleteTarget, then DeleteHandle exists when trino version
121
+ # is less than 313.
122
+ if $1[0, 2] == "0_" || $1.to_i < 314
123
+ it "decodes DeleteHandle" do
124
+ dh = Models::DeleteHandle.decode({
125
+ "handle" => {
126
+ "connectorId" => "c1",
127
+ "connectorHandle" => {}
128
+ }
129
+ })
130
+ dh.handle.should be_a_kind_of Models::TableHandle
131
+ dh.handle.connector_id.should == "c1"
132
+ dh.handle.connector_handle.should == {}
133
+ end
134
+
135
+ it "validates models" do
136
+ lambda do
137
+ Models::DeleteHandle.decode({
138
+ "handle" => "invalid"
139
+ })
140
+ end.should raise_error(TypeError, /String to Hash/)
141
+ end
142
+ else
143
+ it "decodes DeleteTarget" do
144
+ dh = Models::DeleteTarget.decode({
145
+ "handle" => {
146
+ "catalogName" => "c1",
147
+ "connectorHandle" => {}
148
+ }
149
+ })
150
+ dh.handle.should be_a_kind_of Models::TableHandle
151
+ dh.handle.catalog_name.should == "c1"
152
+ dh.handle.connector_handle.should == {}
153
+ end
154
+
155
+ it "validates models" do
156
+ lambda do
157
+ Models::DeleteTarget.decode({
158
+ "catalogName" => "c1",
159
+ "handle" => "invalid"
160
+ })
161
+ end.should raise_error(TypeError, /String to Hash/)
162
+ end
163
+ end
164
+
165
+ it "receives headers of POST" do
166
+ stub_request(:post, "localhost/v1/statement").
167
+ with(body: query).to_return(body: response_json2.to_json, headers: {"X-Test-Header" => "123"})
168
+
169
+ sc = StatementClient.new(faraday, query, options.merge(http_open_timeout: 1))
170
+ sc.current_results_headers["X-Test-Header"].should == "123"
171
+ end
172
+
173
+ it "receives headers of POST through Query" do
174
+ stub_request(:post, "localhost/v1/statement").
175
+ with(body: query).to_return(body: response_json2.to_json, headers: {"X-Test-Header" => "123"})
176
+
177
+ q = Trino::Client.new(options).query(query)
178
+ q.current_results_headers["X-Test-Header"].should == "123"
179
+ end
180
+
181
+ describe "#query_id" do
182
+ it "returns query_id" do
183
+ stub_request(:post, "localhost/v1/statement").
184
+ with(body: query).to_return(body: response_json2.to_json, headers: {"X-Test-Header" => "123"})
185
+
186
+ stub_request(:get, "localhost/v1/next_uri").
187
+ to_return(body: response_json.to_json, headers: {"X-Test-Header" => "123"})
188
+
189
+ sc = StatementClient.new(faraday, query, options.merge(http_open_timeout: 1))
190
+ sc.query_id.should == "queryid"
191
+ sc.has_next?.should be_true
192
+ sc.advance.should be_true
193
+ sc.query_id.should == "queryid"
194
+ end
195
+ end
196
+
197
+ describe '#query_info' do
198
+ let :headers do
199
+ {
200
+ "User-Agent" => "trino-ruby/#{VERSION}",
201
+ "X-Trino-Catalog" => options[:catalog],
202
+ "X-Trino-Schema" => options[:schema],
203
+ "X-Trino-User" => options[:user],
204
+ "X-Trino-Language" => options[:language],
205
+ "X-Trino-Time-Zone" => options[:time_zone],
206
+ }
207
+ end
208
+
209
+ let :statement_client do
210
+ stub_request(:post, "http://localhost/v1/statement").
211
+ with(body: query, headers: headers).
212
+ to_return(body: response_json2.to_json)
213
+ StatementClient.new(faraday, query, options)
214
+ end
215
+
216
+ it "raises an exception with sample JSON if response is unexpected" do
217
+ lambda do
218
+ stub_request(:get, "http://localhost/v1/query/#{response_json2[:id]}").
219
+ with(headers: headers).
220
+ to_return(body: {"session" => "invalid session structure"}.to_json)
221
+ statement_client.query_info
222
+ end.should raise_error(TrinoHttpError, /Trino API returned unexpected structure at \/v1\/query\/queryid\. Expected Trino::Client::ModelVersions::.*::QueryInfo but got {"session":"invalid session structure"}/)
223
+ end
224
+
225
+ it "raises an exception if response format is unexpected" do
226
+ lambda do
227
+ stub_request(:get, "http://localhost/v1/query/#{response_json2[:id]}").
228
+ with(headers: headers).
229
+ to_return(body: "unexpected data structure (not JSON)")
230
+ statement_client.query_info
231
+ end.should raise_error(TrinoHttpError, /Trino API returned unexpected data format./)
232
+ end
233
+
234
+ it "is redirected if server returned 301" do
235
+ stub_request(:get, "http://localhost/v1/query/#{response_json2[:id]}").
236
+ with(headers: headers).
237
+ to_return(status: 301, headers: {"Location" => "http://localhost/v1/query/redirected"})
238
+
239
+ stub_request(:get, "http://localhost/v1/query/redirected").
240
+ with(headers: headers).
241
+ to_return(body: {"queryId" => "queryid"}.to_json)
242
+
243
+ query_info = statement_client.query_info
244
+ query_info.query_id.should == "queryid"
245
+ end
246
+ end
247
+
248
+ describe "Killing a query" do
249
+ let(:query_id) { 'A_QUERY_ID' }
250
+
251
+ it "sends DELETE request with empty body to /v1/query/{queryId}" do
252
+ stub_request(:delete, "http://localhost/v1/query/#{query_id}").
253
+ with(body: "",
254
+ headers: {
255
+ "User-Agent" => "trino-ruby/#{VERSION}",
256
+ "X-Trino-Catalog" => options[:catalog],
257
+ "X-Trino-Schema" => options[:schema],
258
+ "X-Trino-User" => options[:user],
259
+ "X-Trino-Language" => options[:language],
260
+ "X-Trino-Time-Zone" => options[:time_zone],
261
+ }).to_return(body: {}.to_json)
262
+
263
+ Trino::Client.new(options).kill(query_id)
264
+ end
265
+ end
266
+
267
+ describe 'advanced HTTP headers' do
268
+ let(:headers) do
269
+ {
270
+ "User-Agent" => "trino-ruby/#{VERSION}",
271
+ "X-Trino-Catalog" => options[:catalog],
272
+ "X-Trino-Schema" => options[:schema],
273
+ "X-Trino-User" => options[:user],
274
+ "X-Trino-Language" => options[:language],
275
+ "X-Trino-Time-Zone" => options[:time_zone],
276
+ }
277
+ end
278
+
279
+ it "sets X-Trino-Session from properties" do
280
+ options[:properties] = {"hello" => "world", "name"=>"value"}
281
+
282
+ stub_request(:post, "localhost/v1/statement").
283
+ with(body: query,
284
+ headers: headers.merge({
285
+ "X-Trino-Session" => options[:properties].map {|k,v| "#{k}=#{v}"}.join(", ")
286
+ })).
287
+ to_return(body: response_json.to_json)
288
+
289
+ StatementClient.new(faraday, query, options)
290
+ end
291
+
292
+ it "sets X-Trino-Client-Info from client_info" do
293
+ options[:client_info] = "raw"
294
+
295
+ stub_request(:post, "localhost/v1/statement").
296
+ with(body: query,
297
+ headers: headers.merge("X-Trino-Client-Info" => "raw")).
298
+ to_return(body: response_json.to_json)
299
+
300
+ StatementClient.new(faraday, query, options)
301
+ end
302
+
303
+ it "sets X-Trino-Client-Info in JSON from client_info" do
304
+ options[:client_info] = {"k1" => "v1", "k2" => "v2"}
305
+
306
+ stub_request(:post, "localhost/v1/statement").
307
+ with(body: query,
308
+ headers: headers.merge("X-Trino-Client-Info" => '{"k1":"v1","k2":"v2"}')).
309
+ to_return(body: response_json.to_json)
310
+
311
+ StatementClient.new(faraday, query, options)
312
+ end
313
+
314
+ it "sets X-Trino-Client-Tags" do
315
+ options[:client_tags] = ["k1:v1", "k2:v2"]
316
+
317
+ stub_request(:post, "localhost/v1/statement").
318
+ with(body: query,
319
+ headers: headers.merge("X-Trino-Client-Tags" => "k1:v1,k2:v2")).
320
+ to_return(body: response_json.to_json)
321
+
322
+ StatementClient.new(faraday, query, options)
323
+ end
324
+ end
325
+
326
+ describe 'HTTP basic auth' do
327
+ let(:password) { 'abcd' }
328
+
329
+ it "adds basic auth headers when ssl is enabled and a password is given" do
330
+ stub_request(:post, "https://localhost/v1/statement").
331
+ with(body: query,
332
+ headers: {
333
+ "User-Agent" => "trino-ruby/#{VERSION}",
334
+ "X-Trino-Catalog" => options[:catalog],
335
+ "X-Trino-Schema" => options[:schema],
336
+ "X-Trino-User" => options[:user],
337
+ "X-Trino-Language" => options[:language],
338
+ "X-Trino-Time-Zone" => options[:time_zone],
339
+ },
340
+ basic_auth: [options[:user], password]
341
+ ).to_return(body: response_json.to_json)
342
+
343
+ options.merge!(ssl: { verify: true }, password: password)
344
+ StatementClient.new(faraday, query, options)
345
+ end
346
+
347
+ it "forbids using basic auth when ssl is disabled" do
348
+ lambda do
349
+ Query.__send__(:faraday_client, {
350
+ server: 'localhost',
351
+ password: 'abcd'
352
+ })
353
+ end.should raise_error(ArgumentError)
354
+ end
355
+ end
356
+
357
+ describe "ssl" do
358
+ it "is disabled by default" do
359
+ f = Query.__send__(:faraday_client, {
360
+ server: "localhost",
361
+ })
362
+ f.url_prefix.to_s.should == "http://localhost/"
363
+ end
364
+
365
+ it "is enabled with ssl: true" do
366
+ f = Query.__send__(:faraday_client, {
367
+ server: "localhost",
368
+ ssl: true,
369
+ })
370
+ f.url_prefix.to_s.should == "https://localhost/"
371
+ f.ssl.verify?.should == true
372
+ end
373
+
374
+ it "is enabled with ssl: {verify: false}" do
375
+ f = Query.__send__(:faraday_client, {
376
+ server: "localhost",
377
+ ssl: {verify: false}
378
+ })
379
+ f.url_prefix.to_s.should == "https://localhost/"
380
+ f.ssl.verify?.should == false
381
+ end
382
+
383
+ it "rejects invalid ssl: verify: object" do
384
+ lambda do
385
+ f = Query.__send__(:faraday_client, {
386
+ server: "localhost",
387
+ ssl: {verify: "??"}
388
+ })
389
+ end.should raise_error(ArgumentError, /String/)
390
+ end
391
+
392
+ it "is enabled with ssl: Hash" do
393
+ require 'openssl'
394
+
395
+ ssl = {
396
+ ca_file: "/path/to/dummy.pem",
397
+ ca_path: "/path/to/pemdir",
398
+ cert_store: OpenSSL::X509::Store.new,
399
+ client_cert: OpenSSL::X509::Certificate.new,
400
+ client_key: OpenSSL::PKey::DSA.new,
401
+ }
402
+
403
+ f = Query.__send__(:faraday_client, {
404
+ server: "localhost",
405
+ ssl: ssl,
406
+ })
407
+
408
+ f.url_prefix.to_s.should == "https://localhost/"
409
+ f.ssl.verify?.should == true
410
+ f.ssl.ca_file.should == ssl[:ca_file]
411
+ f.ssl.ca_path.should == ssl[:ca_path]
412
+ f.ssl.cert_store.should == ssl[:cert_store]
413
+ f.ssl.client_cert.should == ssl[:client_cert]
414
+ f.ssl.client_key.should == ssl[:client_key]
415
+ end
416
+
417
+ it "rejects an invalid string" do
418
+ lambda do
419
+ Query.__send__(:faraday_client, {
420
+ server: "localhost",
421
+ ssl: '??',
422
+ })
423
+ end.should raise_error(ArgumentError, /String/)
424
+ end
425
+
426
+ it "rejects an integer" do
427
+ lambda do
428
+ Query.__send__(:faraday_client, {
429
+ server: "localhost",
430
+ ssl: 3,
431
+ })
432
+ end.should raise_error(ArgumentError, /:ssl/)
433
+ end
434
+ end
435
+
436
+ it "supports Presto" do
437
+ stub_request(:post, "localhost/v1/statement").
438
+ with({body: query}).
439
+ to_return(body: response_json.to_json)
440
+
441
+ faraday = Faraday.new(url: "http://localhost")
442
+ client = StatementClient.new(faraday, query, options.merge(model_version: "316"))
443
+ client.current_results.should be_a_kind_of(ModelVersions::V316::QueryResults)
444
+ end
445
+
446
+ it "rejects unsupported model version" do
447
+ lambda do
448
+ StatementClient.new(faraday, query, options.merge(model_version: "0.111"))
449
+ end.should raise_error(NameError)
450
+ end
451
+
452
+
453
+ let :nested_json do
454
+ nested_stats = {createTime: Time.now}
455
+ # JSON max nesting default value is 100
456
+ for i in 0..100 do
457
+ nested_stats = {stats: nested_stats}
458
+ end
459
+ {
460
+ id: "queryid",
461
+ stats: nested_stats
462
+ }
463
+ end
464
+
465
+ it "parse nested json properly" do
466
+ stub_request(:post, "localhost/v1/statement").
467
+ with(body: query,
468
+ headers: {
469
+ "User-Agent" => "trino-ruby/#{VERSION}",
470
+ "X-Trino-Catalog" => options[:catalog],
471
+ "X-Trino-Schema" => options[:schema],
472
+ "X-Trino-User" => options[:user],
473
+ "X-Trino-Language" => options[:language],
474
+ "X-Trino-Time-Zone" => options[:time_zone],
475
+ }).to_return(body: nested_json.to_json(:max_nesting => false))
476
+
477
+ StatementClient.new(faraday, query, options)
478
+ end
479
+
480
+ describe "query timeout" do
481
+ let :headers do
482
+ {
483
+ "User-Agent" => "trino-ruby/#{VERSION}",
484
+ "X-Trino-Catalog" => options[:catalog],
485
+ "X-Trino-Schema" => options[:schema],
486
+ "X-Trino-User" => options[:user],
487
+ "X-Trino-Language" => options[:language],
488
+ "X-Trino-Time-Zone" => options[:time_zone],
489
+ }
490
+ end
491
+
492
+ let :planning_response do
493
+ {
494
+ id: "queryid",
495
+ nextUri: 'http://localhost/v1/next_uri',
496
+ stats: {},
497
+ }
498
+ end
499
+
500
+ let :early_running_response do
501
+ {
502
+ id: "queryid",
503
+ nextUri: 'http://localhost/v1/next_uri',
504
+ stats: {},
505
+ columns: [{name: "_col0", type: "bigint"}],
506
+ }
507
+ end
508
+
509
+ let :late_running_response do
510
+ {
511
+ id: "queryid",
512
+ nextUri: 'http://localhost/v1/next_uri',
513
+ stats: {},
514
+ columns: [{name: "_col0", type: "bigint"}],
515
+ data: "",
516
+ }
517
+ end
518
+
519
+ let :done_response do
520
+ {
521
+ id: "queryid",
522
+ stats: {},
523
+ columns: [{name: "_col0", type: "bigint"}],
524
+ }
525
+ end
526
+
527
+ before(:each) do
528
+ end
529
+
530
+ [:plan_timeout, :query_timeout].each do |timeout_type|
531
+ it "raises TrinoQueryTimeoutError if timeout during planning" do
532
+ stub_request(:post, "localhost/v1/statement").
533
+ with(body: query, headers: headers).
534
+ to_return(body: planning_response.to_json)
535
+
536
+ client = StatementClient.new(faraday, query, options.merge(timeout_type => 1))
537
+
538
+ stub_request(:get, "localhost/v1/next_uri").
539
+ with(headers: headers).
540
+ to_return(body: planning_response.to_json)
541
+ client.advance
542
+
543
+ sleep 1
544
+ stub_request(:get, "localhost/v1/next_uri").
545
+ with(headers: headers).
546
+ to_return(body: planning_response.to_json)
547
+ lambda do
548
+ client.advance
549
+ end.should raise_error(Trino::Client::TrinoQueryTimeoutError, "Query queryid timed out")
550
+ end
551
+
552
+ it "raises TrinoQueryTimeoutError if timeout during initial resuming" do
553
+ stub_request(:get, "localhost/v1/next_uri").
554
+ with(headers: headers).
555
+ to_return(body: lambda{|req| raise Timeout::Error.new("execution expired")})
556
+
557
+ lambda do
558
+ StatementClient.new(faraday, query, options.merge(timeout_type => 1), "/v1/next_uri")
559
+ end.should raise_error(Trino::Client::TrinoQueryTimeoutError, "Query timed out")
560
+ end
561
+
562
+ it "raises TrinoHttpError if timeout during initial resuming and #{timeout_type} < retry_timeout" do
563
+ stub_request(:get, "localhost/v1/next_uri").
564
+ with(headers: headers).
565
+ to_return(body: lambda{|req| raise Timeout::Error.new("execution expired")})
566
+
567
+ lambda do
568
+ StatementClient.new(faraday, query, options.merge(timeout_type => 2, retry_timeout: 1), "/v1/next_uri")
569
+ end.should raise_error(Trino::Client::TrinoHttpError, "Trino API error due to timeout")
570
+ end
571
+ end
572
+
573
+ it "doesn't raise errors with plan_timeout if query planning is done" do
574
+ stub_request(:post, "localhost/v1/statement").
575
+ with(body: query, headers: headers).
576
+ to_return(body: planning_response.to_json)
577
+
578
+ client = StatementClient.new(faraday, query, options.merge(plan_timeout: 1))
579
+
580
+ sleep 1
581
+
582
+ stub_request(:get, "localhost/v1/next_uri").
583
+ with(headers: headers).
584
+ to_return(body: early_running_response.to_json)
585
+ client.advance
586
+
587
+ stub_request(:get, "localhost/v1/next_uri").
588
+ with(headers: headers).
589
+ to_return(body: late_running_response.to_json)
590
+ client.advance
591
+ end
592
+
593
+ it "raises TrinoQueryTimeoutError if timeout during execution" do
594
+ stub_request(:post, "localhost/v1/statement").
595
+ with(body: query, headers: headers).
596
+ to_return(body: planning_response.to_json)
597
+
598
+ client = StatementClient.new(faraday, query, options.merge(query_timeout: 1))
599
+
600
+ stub_request(:get, "localhost/v1/next_uri").
601
+ with(headers: headers).
602
+ to_return(body: early_running_response.to_json)
603
+ client.advance
604
+
605
+ sleep 1
606
+ stub_request(:get, "localhost/v1/next_uri").
607
+ with(headers: headers).
608
+ to_return(body: late_running_response.to_json)
609
+ lambda do
610
+ client.advance
611
+ end.should raise_error(Trino::Client::TrinoQueryTimeoutError, "Query queryid timed out")
612
+ end
613
+
614
+ it "doesn't raise errors if query is done" do
615
+ stub_request(:post, "localhost/v1/statement").
616
+ with(body: query, headers: headers).
617
+ to_return(body: planning_response.to_json)
618
+
619
+ client = StatementClient.new(faraday, query, options.merge(query_timeout: 1))
620
+
621
+ stub_request(:get, "localhost/v1/next_uri").
622
+ with(headers: headers).
623
+ to_return(body: early_running_response.to_json)
624
+ client.advance
625
+
626
+ stub_request(:get, "localhost/v1/next_uri").
627
+ with(headers: headers).
628
+ to_return(body: done_response.to_json)
629
+ client.advance # set finished
630
+
631
+ sleep 1
632
+ client.advance # set finished
633
+ end
634
+
635
+ end
636
+ end
637
+