trino-client 1.0.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 (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
+