presto-client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org/'
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,38 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ presto-client (0.1.0)
5
+ faraday (~> 0.8.8)
6
+ multi_json (~> 1.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ addressable (2.3.5)
12
+ crack (0.3.2)
13
+ diff-lcs (1.2.4)
14
+ faraday (0.8.8)
15
+ multipart-post (~> 1.2.0)
16
+ multi_json (1.8.2)
17
+ multipart-post (1.2.0)
18
+ rake (10.1.1)
19
+ rspec (2.13.0)
20
+ rspec-core (~> 2.13.0)
21
+ rspec-expectations (~> 2.13.0)
22
+ rspec-mocks (~> 2.13.0)
23
+ rspec-core (2.13.1)
24
+ rspec-expectations (2.13.0)
25
+ diff-lcs (>= 1.1.3, < 2.0)
26
+ rspec-mocks (2.13.1)
27
+ webmock (1.16.1)
28
+ addressable (>= 2.2.7)
29
+ crack (>= 0.3.2)
30
+
31
+ PLATFORMS
32
+ ruby
33
+
34
+ DEPENDENCIES
35
+ presto-client!
36
+ rake (>= 0.9.2)
37
+ rspec (~> 2.13.0)
38
+ webmock (~> 1.16.1)
data/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # Presto client library for Ruby
2
+
3
+ Presto is a distributed SQL query engine for big data:
4
+ https://github.com/facebook/presto
5
+
6
+ This is a client library for Ruby to run queries on Presto.
7
+
8
+ ## Example
9
+
10
+ ```ruby
11
+ require 'presto-client'
12
+
13
+ # create a client object
14
+ client = PrestoClient::Client.new(
15
+ server: "localhost:8880",
16
+ user: "frsyuki",
17
+ catalog: "native",
18
+ schema: "default",
19
+ )
20
+
21
+ # start running a query on presto
22
+ q = client.query("select * from sys.query")
23
+
24
+ # wait for completion and get columns
25
+ q.columns.each {|column|
26
+ puts "column: #{column.name}.#{column.type}"
27
+ }
28
+
29
+ # get query results
30
+ q.each_row {|row|
31
+ p row
32
+ }
33
+ ```
34
+
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/gem_tasks'
3
+
4
+ require 'rake/testtask'
5
+ require 'rake/clean'
6
+
7
+ require 'rspec/core/rake_task'
8
+
9
+ RSpec::Core::RakeTask.new(:spec) do |t|
10
+ t.fail_on_error = false
11
+ end
12
+
13
+ task :default => [:spec, :build]
@@ -0,0 +1 @@
1
+ require 'presto/client'
@@ -0,0 +1,23 @@
1
+ #
2
+ # Presto client for Ruby
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ module Presto
17
+ module Client
18
+
19
+ require 'presto/client/version'
20
+ require 'presto/client/client'
21
+
22
+ end
23
+ end
@@ -0,0 +1,35 @@
1
+ #
2
+ # Presto client for Ruby
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ module Presto::Client
17
+
18
+ require 'presto/client/models'
19
+ require 'presto/client/query'
20
+
21
+ class Client
22
+ def initialize(options)
23
+ @session = ClientSession.new(options)
24
+ end
25
+
26
+ def query(query)
27
+ Query.start(@session, query)
28
+ end
29
+ end
30
+
31
+ def self.new(*args)
32
+ Client.new(*args)
33
+ end
34
+
35
+ end
@@ -0,0 +1,194 @@
1
+ #
2
+ # Presto client for Ruby
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ module Presto::Client
17
+
18
+ class Column
19
+ attr_reader :name
20
+ attr_reader :type
21
+
22
+ def initialize(options={})
23
+ @name = options[:name]
24
+ @type = options[:type]
25
+ end
26
+
27
+ def self.decode_hash(hash)
28
+ new(
29
+ name: hash["name"],
30
+ type: hash["type"],
31
+ )
32
+ end
33
+ end
34
+
35
+ class ClientSession
36
+ def initialize(options)
37
+ @server = options[:server]
38
+ @user = options[:user]
39
+ @source = options[:source]
40
+ @catalog = options[:catalog]
41
+ @schema = options[:schema]
42
+ @debug = !!options[:debug]
43
+ end
44
+
45
+ attr_reader :server
46
+ attr_reader :user
47
+ attr_reader :source
48
+ attr_reader :catalog
49
+ attr_reader :schema
50
+
51
+ def debug?
52
+ @debug
53
+ end
54
+ end
55
+
56
+ #class StageStats
57
+ # attr_reader :stage_id
58
+ # attr_reader :state
59
+ # attr_reader :done
60
+ # attr_reader :nodes
61
+ # attr_reader :total_splits
62
+ # attr_reader :queued_splits
63
+ # attr_reader :running_splits
64
+ # attr_reader :completed_splits
65
+ # attr_reader :user_time_millis
66
+ # attr_reader :cpu_time_millis
67
+ # attr_reader :wall_time_millis
68
+ # attr_reader :processed_rows
69
+ # attr_reader :processed_bytes
70
+ # attr_reader :sub_stages
71
+ #
72
+ # def initialize(options={})
73
+ # @stage_id = options[:stage_id]
74
+ # @state = options[:state]
75
+ # @done = options[:done]
76
+ # @nodes = options[:nodes]
77
+ # @total_splits = options[:total_splits]
78
+ # @queued_splits = options[:queued_splits]
79
+ # @running_splits = options[:running_splits]
80
+ # @completed_splits = options[:completed_splits]
81
+ # @user_time_millis = options[:user_time_millis]
82
+ # @cpu_time_millis = options[:cpu_time_millis]
83
+ # @wall_time_millis = options[:wall_time_millis]
84
+ # @processed_rows = options[:processed_rows]
85
+ # @processed_bytes = options[:processed_bytes]
86
+ # @sub_stages = options[:sub_stages]
87
+ # end
88
+ #
89
+ # def self.decode_hash(hash)
90
+ # new(
91
+ # stage_id: hash["stageId"],
92
+ # state: hash["state"],
93
+ # done: hash["done"],
94
+ # nodes: hash["nodes"],
95
+ # total_splits: hash["totalSplits"],
96
+ # queued_splits: hash["queuedSplits"],
97
+ # running_splits: hash["runningSplits"],
98
+ # completed_splits: hash["completedSplits"],
99
+ # user_time_millis: hash["userTimeMillis"],
100
+ # cpu_time_millis: hash["cpuTimeMillis"],
101
+ # wall_time_millis: hash["wallTimeMillis"],
102
+ # processed_rows: hash["processedRows"],
103
+ # processed_bytes: hash["processedBytes"],
104
+ # sub_stages: hash["subStages"].map {|h| StageStats.decode_hash(h) },
105
+ # )
106
+ # end
107
+ #end
108
+
109
+ class StatementStats
110
+ attr_reader :state
111
+ attr_reader :scheduled
112
+ attr_reader :nodes
113
+ attr_reader :total_splits
114
+ attr_reader :queued_splits
115
+ attr_reader :running_splits
116
+ attr_reader :completed_splits
117
+ attr_reader :user_time_millis
118
+ attr_reader :cpu_time_millis
119
+ attr_reader :wall_time_millis
120
+ attr_reader :processed_rows
121
+ attr_reader :processed_bytes
122
+ #attr_reader :root_stage
123
+
124
+ def initialize(options={})
125
+ @state = state
126
+ @scheduled = scheduled
127
+ @nodes = nodes
128
+ @total_splits = total_splits
129
+ @queued_splits = queued_splits
130
+ @running_splits = running_splits
131
+ @completed_splits = completed_splits
132
+ @user_time_millis = user_time_millis
133
+ @cpu_time_millis = cpu_time_millis
134
+ @wall_time_millis = wall_time_millis
135
+ @processed_rows = processed_rows
136
+ @processed_bytes = processed_bytes
137
+ #@root_stage = root_stage
138
+ end
139
+
140
+ def self.decode_hash(hash)
141
+ new(
142
+ state: hash["state"],
143
+ scheduled: hash["scheduled"],
144
+ nodes: hash["nodes"],
145
+ total_splits: hash["totalSplits"],
146
+ queued_splits: hash["queuedSplits"],
147
+ running_splits: hash["runningSplits"],
148
+ completed_splits: hash["completedSplits"],
149
+ user_time_millis: hash["userTimeMillis"],
150
+ cpu_time_millis: hash["cpuTimeMillis"],
151
+ wall_time_millis: hash["wallTimeMillis"],
152
+ processed_rows: hash["processedRows"],
153
+ processed_bytes: hash["processedBytes"],
154
+ #root_stage: StageStats.decode_hash(hash["rootStage"]),
155
+ )
156
+ end
157
+ end
158
+
159
+ class QueryResults
160
+ attr_reader :id
161
+ attr_reader :info_uri
162
+ attr_reader :partial_cache_uri
163
+ attr_reader :next_uri
164
+ attr_reader :columns
165
+ attr_reader :data
166
+ attr_reader :stats
167
+ attr_reader :error
168
+
169
+ def initialize(options={})
170
+ @id = options[:id]
171
+ @info_uri = options[:info_uri]
172
+ @partial_cache_uri = options[:partial_cache_uri]
173
+ @next_uri = options[:next_uri]
174
+ @columns = options[:columns]
175
+ @data = options[:data]
176
+ @stats = options[:stats]
177
+ @error = options[:error]
178
+ end
179
+
180
+ def self.decode_hash(hash)
181
+ new(
182
+ id: hash["id"],
183
+ info_uri: hash["infoUri"],
184
+ partial_cache_uri: hash["partialCancelUri"],
185
+ next_uri: hash["nextUri"],
186
+ columns: hash["columns"] ? hash["columns"].map {|h| Column.decode_hash(h) } : nil,
187
+ data: hash["data"],
188
+ stats: StatementStats.decode_hash(hash["stats"]),
189
+ error: hash["error"], # TODO
190
+ )
191
+ end
192
+ end
193
+
194
+ end
@@ -0,0 +1,85 @@
1
+ #
2
+ # Presto client for Ruby
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ module Presto::Client
17
+
18
+ require 'faraday'
19
+ require 'presto/client/models'
20
+ require 'presto/client/statement_client'
21
+
22
+ class Query
23
+ def self.start(session, query)
24
+ faraday = Faraday.new(url: "http://#{session.server}") do |faraday|
25
+ #faraday.request :url_encoded
26
+ faraday.response :logger
27
+ faraday.adapter Faraday.default_adapter
28
+ end
29
+
30
+ new StatementClient.new(faraday, session, query)
31
+ end
32
+
33
+ def initialize(client)
34
+ @client = client
35
+ end
36
+
37
+ def wait_for_data
38
+ while @client.has_next? && @client.current_results.data == nil
39
+ @client.advance
40
+ end
41
+ end
42
+
43
+ private :wait_for_data
44
+
45
+ def columns
46
+ wait_for_data
47
+
48
+ raise_error unless @client.query_succeeded?
49
+
50
+ return @client.current_results.columns
51
+ end
52
+
53
+ def each_row(&block)
54
+ wait_for_data
55
+
56
+ raise_error unless @client.query_succeeded?
57
+
58
+ if self.columns == nil
59
+ raise "Query #{@client.current_results.id} has no columns"
60
+ end
61
+
62
+ begin
63
+ if data = @client.current_results.data
64
+ data.each(&block)
65
+ end
66
+ @client.advance
67
+ end while @client.has_next?
68
+ end
69
+
70
+ def raise_error
71
+ if @client.closed?
72
+ raise "Query aborted by user"
73
+ elsif @client.exception?
74
+ raise "Query is gone: #{@client.exception}"
75
+ elsif @client.query_failed?
76
+ results = @client.current_results
77
+ # TODO error location
78
+ raise "Query #{results.id} failed: #{results.error}"
79
+ end
80
+ end
81
+
82
+ private :raise_error
83
+ end
84
+
85
+ end
@@ -0,0 +1,168 @@
1
+ #
2
+ # Presto client for Ruby
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ module Presto::Client
17
+
18
+ require 'multi_json'
19
+ require 'presto/client/models'
20
+
21
+ module PrestoHeaders
22
+ PRESTO_USER = "X-Presto-User"
23
+ PRESTO_SOURCE = "X-Presto-Source"
24
+ PRESTO_CATALOG = "X-Presto-Catalog"
25
+ PRESTO_SCHEMA = "X-Presto-Schema"
26
+
27
+ PRESTO_CURRENT_STATE = "X-Presto-Current-State"
28
+ PRESTO_MAX_WAIT = "X-Presto-Max-Wait"
29
+ PRESTO_MAX_SIZE = "X-Presto-Max-Size"
30
+ PRESTO_PAGE_SEQUENCE_ID = "X-Presto-Page-Sequence-Id"
31
+ end
32
+
33
+ class StatementClient
34
+ HEADERS = {
35
+ "User-Agent" => "presto-ruby/#{VERSION}"
36
+ }
37
+
38
+ def initialize(faraday, session, query)
39
+ @faraday = faraday
40
+ @faraday.headers.merge!(HEADERS)
41
+
42
+ @session = session
43
+ @query = query
44
+ @closed = false
45
+ @exception = nil
46
+ post_query_request!
47
+ end
48
+
49
+ def post_query_request!
50
+ response = @faraday.post do |req|
51
+ req.url "/v1/statement"
52
+
53
+ if v = @session.user
54
+ req.headers[PrestoHeaders::PRESTO_USER] = v
55
+ end
56
+ if v = @session.source
57
+ req.headers[PrestoHeaders::PRESTO_SOURCE] = v
58
+ end
59
+ if v = @session.catalog
60
+ req.headers[PrestoHeaders::PRESTO_CATALOG] = v
61
+ end
62
+ if v = @session.schema
63
+ req.headers[PrestoHeaders::PRESTO_SCHEMA] = v
64
+ end
65
+
66
+ req.body = @query
67
+ end
68
+
69
+ # TODO error handling
70
+ if response.status != 200
71
+ raise "Failed to start query: #{response.body}" # TODO error class
72
+ end
73
+
74
+ body = response.body
75
+ hash = MultiJson.load(body)
76
+ @results = QueryResults.decode_hash(hash)
77
+ end
78
+
79
+ private :post_query_request!
80
+
81
+ attr_reader :query
82
+
83
+ def debug?
84
+ @session.debug?
85
+ end
86
+
87
+ def closed?
88
+ @closed
89
+ end
90
+
91
+ attr_reader :exception
92
+
93
+ def exception?
94
+ @exception
95
+ end
96
+
97
+ def query_failed?
98
+ @results.error != nil
99
+ end
100
+
101
+ def query_succeeded?
102
+ @results.error == nil && !@exception && !@closed
103
+ end
104
+
105
+ def current_results
106
+ @results
107
+ end
108
+
109
+ def has_next?
110
+ !!@results.next_uri
111
+ end
112
+
113
+ def advance
114
+ if closed? || !has_next?
115
+ return false
116
+ end
117
+ uri = @results.next_uri
118
+
119
+ start = Time.now
120
+ attempts = 0
121
+
122
+ begin
123
+ begin
124
+ response = @faraday.get do |req|
125
+ req.url uri
126
+ end
127
+ rescue => e
128
+ @exception = e
129
+ raise @exception
130
+ end
131
+
132
+ if response.status == 200 && !response.body.to_s.empty?
133
+ @results = QueryResults.decode_hash(MultiJson.load(response.body))
134
+ return true
135
+ end
136
+
137
+ if response.status != 503 # retry on 503 Service Unavailable
138
+ # deterministic error
139
+ @exception = StandardError.new("Error fetching next at #{uri} returned #{response.status}: #{response.body}") # TODO error class
140
+ raise @exception
141
+ end
142
+
143
+ attempts += 1
144
+ sleep attempts * 0.1
145
+ end while (Time.now - start) < 2*60*60 && !@closed
146
+
147
+ @exception = StandardError.new("Error fetching next") # TODO error class
148
+ raise @exception
149
+ end
150
+
151
+ def close
152
+ return if @closed
153
+
154
+ # cancel running statement
155
+ if uri = @results.next_uri
156
+ # TODO error handling
157
+ # TODO make async reqeust and ignore response
158
+ @faraday.delete do |req|
159
+ req.url uri
160
+ end
161
+ end
162
+
163
+ @closed = true
164
+ nil
165
+ end
166
+ end
167
+
168
+ end
@@ -0,0 +1,20 @@
1
+ #
2
+ # Presto client for Ruby
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ module Presto
17
+ module Client
18
+ VERSION = "0.1.0"
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ require File.expand_path 'lib/presto/client/version', File.dirname(__FILE__)
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.name = "presto-client"
5
+ gem.version = Presto::Client::VERSION
6
+
7
+ gem.authors = ["Sadayuki Furuhashi"]
8
+ gem.email = ["sf@treasure-data.com"]
9
+ gem.description = %q{Presto client library}
10
+ gem.summary = %q{Presto client library}
11
+ gem.homepage = "https://github.com/treasure-data/presto-client-ruby"
12
+ gem.license = "Apache 2.0"
13
+
14
+ gem.files = `git ls-files`.split($\)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.require_paths = ["lib"]
18
+ gem.has_rdoc = false
19
+
20
+ gem.required_ruby_version = ">= 1.9.3"
21
+
22
+ gem.add_dependency "faraday", ["~> 0.8.8"]
23
+ gem.add_dependency "multi_json", ["~> 1.0"]
24
+
25
+ gem.add_development_dependency "rake", [">= 0.9.2"]
26
+ gem.add_development_dependency "rspec", ["~> 2.13.0"]
27
+ gem.add_development_dependency "webmock", ["~> 1.16.1"]
28
+ end
data/presto-client.rb ADDED
@@ -0,0 +1,420 @@
1
+
2
+ module PrestoClient
3
+ VERSION = "0.1.0"
4
+
5
+ require 'faraday'
6
+ require 'json'
7
+
8
+ class ClientSession
9
+ def initialize(options)
10
+ @server = options[:server]
11
+ @user = options[:user]
12
+ @source = options[:source]
13
+ @catalog = options[:catalog]
14
+ @schema = options[:schema]
15
+ @debug = !!options[:debug]
16
+ end
17
+
18
+ attr_reader :server
19
+ attr_reader :user
20
+ attr_reader :source
21
+ attr_reader :catalog
22
+ attr_reader :schema
23
+
24
+ def debug?
25
+ @debug
26
+ end
27
+ end
28
+
29
+ #class StageStats
30
+ # attr_reader :stage_id
31
+ # attr_reader :state
32
+ # attr_reader :done
33
+ # attr_reader :nodes
34
+ # attr_reader :total_splits
35
+ # attr_reader :queued_splits
36
+ # attr_reader :running_splits
37
+ # attr_reader :completed_splits
38
+ # attr_reader :user_time_millis
39
+ # attr_reader :cpu_time_millis
40
+ # attr_reader :wall_time_millis
41
+ # attr_reader :processed_rows
42
+ # attr_reader :processed_bytes
43
+ # attr_reader :sub_stages
44
+ #
45
+ # def initialize(options={})
46
+ # @stage_id = options[:stage_id]
47
+ # @state = options[:state]
48
+ # @done = options[:done]
49
+ # @nodes = options[:nodes]
50
+ # @total_splits = options[:total_splits]
51
+ # @queued_splits = options[:queued_splits]
52
+ # @running_splits = options[:running_splits]
53
+ # @completed_splits = options[:completed_splits]
54
+ # @user_time_millis = options[:user_time_millis]
55
+ # @cpu_time_millis = options[:cpu_time_millis]
56
+ # @wall_time_millis = options[:wall_time_millis]
57
+ # @processed_rows = options[:processed_rows]
58
+ # @processed_bytes = options[:processed_bytes]
59
+ # @sub_stages = options[:sub_stages]
60
+ # end
61
+ #
62
+ # def self.decode_hash(hash)
63
+ # new(
64
+ # stage_id: hash["stageId"],
65
+ # state: hash["state"],
66
+ # done: hash["done"],
67
+ # nodes: hash["nodes"],
68
+ # total_splits: hash["totalSplits"],
69
+ # queued_splits: hash["queuedSplits"],
70
+ # running_splits: hash["runningSplits"],
71
+ # completed_splits: hash["completedSplits"],
72
+ # user_time_millis: hash["userTimeMillis"],
73
+ # cpu_time_millis: hash["cpuTimeMillis"],
74
+ # wall_time_millis: hash["wallTimeMillis"],
75
+ # processed_rows: hash["processedRows"],
76
+ # processed_bytes: hash["processedBytes"],
77
+ # sub_stages: hash["subStages"].map {|h| StageStats.decode_hash(h) },
78
+ # )
79
+ # end
80
+ #end
81
+
82
+ class StatementStats
83
+ attr_reader :state
84
+ attr_reader :scheduled
85
+ attr_reader :nodes
86
+ attr_reader :total_splits
87
+ attr_reader :queued_splits
88
+ attr_reader :running_splits
89
+ attr_reader :completed_splits
90
+ attr_reader :user_time_millis
91
+ attr_reader :cpu_time_millis
92
+ attr_reader :wall_time_millis
93
+ attr_reader :processed_rows
94
+ attr_reader :processed_bytes
95
+ #attr_reader :root_stage
96
+
97
+ def initialize(options={})
98
+ @state = state
99
+ @scheduled = scheduled
100
+ @nodes = nodes
101
+ @total_splits = total_splits
102
+ @queued_splits = queued_splits
103
+ @running_splits = running_splits
104
+ @completed_splits = completed_splits
105
+ @user_time_millis = user_time_millis
106
+ @cpu_time_millis = cpu_time_millis
107
+ @wall_time_millis = wall_time_millis
108
+ @processed_rows = processed_rows
109
+ @processed_bytes = processed_bytes
110
+ #@root_stage = root_stage
111
+ end
112
+
113
+ def self.decode_hash(hash)
114
+ new(
115
+ state: hash["state"],
116
+ scheduled: hash["scheduled"],
117
+ nodes: hash["nodes"],
118
+ total_splits: hash["totalSplits"],
119
+ queued_splits: hash["queuedSplits"],
120
+ running_splits: hash["runningSplits"],
121
+ completed_splits: hash["completedSplits"],
122
+ user_time_millis: hash["userTimeMillis"],
123
+ cpu_time_millis: hash["cpuTimeMillis"],
124
+ wall_time_millis: hash["wallTimeMillis"],
125
+ processed_rows: hash["processedRows"],
126
+ processed_bytes: hash["processedBytes"],
127
+ #root_stage: StageStats.decode_hash(hash["rootStage"]),
128
+ )
129
+ end
130
+ end
131
+
132
+ class Column
133
+ attr_reader :name
134
+ attr_reader :type
135
+
136
+ def initialize(options={})
137
+ @name = options[:name]
138
+ @type = options[:type]
139
+ end
140
+
141
+ def self.decode_hash(hash)
142
+ new(
143
+ name: hash["name"],
144
+ type: hash["type"],
145
+ )
146
+ end
147
+ end
148
+
149
+ class QueryResults
150
+ attr_reader :id
151
+ attr_reader :info_uri
152
+ attr_reader :partial_cache_uri
153
+ attr_reader :next_uri
154
+ attr_reader :columns
155
+ attr_reader :data
156
+ attr_reader :stats
157
+ attr_reader :error
158
+
159
+ def initialize(options={})
160
+ @id = options[:id]
161
+ @info_uri = options[:info_uri]
162
+ @partial_cache_uri = options[:partial_cache_uri]
163
+ @next_uri = options[:next_uri]
164
+ @columns = options[:columns]
165
+ @data = options[:data]
166
+ @stats = options[:stats]
167
+ @error = options[:error]
168
+ end
169
+
170
+ def self.decode_hash(hash)
171
+ new(
172
+ id: hash["id"],
173
+ info_uri: hash["infoUri"],
174
+ partial_cache_uri: hash["partialCancelUri"],
175
+ next_uri: hash["nextUri"],
176
+ columns: hash["columns"] ? hash["columns"].map {|h| Column.decode_hash(h) } : nil,
177
+ data: hash["data"]
178
+ stats: StatementStats.decode_hash(hash["stats"]),
179
+ error: hash["error"], # TODO
180
+ )
181
+ end
182
+ end
183
+
184
+ module PrestoHeaders
185
+ PRESTO_USER = "X-Presto-User"
186
+ PRESTO_SOURCE = "X-Presto-Source"
187
+ PRESTO_CATALOG = "X-Presto-Catalog"
188
+ PRESTO_SCHEMA = "X-Presto-Schema"
189
+
190
+ PRESTO_CURRENT_STATE = "X-Presto-Current-State"
191
+ PRESTO_MAX_WAIT = "X-Presto-Max-Wait"
192
+ PRESTO_MAX_SIZE = "X-Presto-Max-Size"
193
+ PRESTO_PAGE_SEQUENCE_ID = "X-Presto-Page-Sequence-Id"
194
+ end
195
+
196
+ class StatementClient
197
+ HEADERS = {
198
+ "User-Agent" => "presto-ruby/#{VERSION}"
199
+ }
200
+
201
+ def initialize(faraday, session, query)
202
+ @faraday = faraday
203
+ @faraday.headers.merge!(HEADERS)
204
+
205
+ @session = session
206
+ @query = query
207
+ @closed = false
208
+ @exception = nil
209
+ post_query_request!
210
+ end
211
+
212
+ def post_query_request!
213
+ response = @faraday.post do |req|
214
+ req.url "/v1/statement"
215
+
216
+ if v = @session.user
217
+ req.headers[PrestoHeaders::PRESTO_USER] = v
218
+ end
219
+ if v = @session.source
220
+ req.headers[PrestoHeaders::PRESTO_SOURCE] = v
221
+ end
222
+ if catalog = @session.catalog
223
+ req.headers[PrestoHeaders::PRESTO_CATALOG] = catalog
224
+ end
225
+ if v = @session.schema
226
+ req.headers[PrestoHeaders::PRESTO_SCHEMA] = v
227
+ end
228
+
229
+ req.body = @query
230
+ end
231
+
232
+ # TODO error handling
233
+ if response.status != 200
234
+ raise "Failed to start query: #{response.body}" # TODO error class
235
+ end
236
+
237
+ body = response.body
238
+ hash = JSON.parse(body)
239
+ @results = QueryResults.decode_hash(hash)
240
+ end
241
+
242
+ private :post_query_request!
243
+
244
+ attr_reader :query
245
+
246
+ def debug?
247
+ @session.debug?
248
+ end
249
+
250
+ def closed?
251
+ @closed
252
+ end
253
+
254
+ attr_reader :exception
255
+
256
+ def exception?
257
+ @exception
258
+ end
259
+
260
+ def query_failed?
261
+ @results.error != nil
262
+ end
263
+
264
+ def query_succeeded?
265
+ @results.error == nil && !@exception && !@closed
266
+ end
267
+
268
+ def current_results
269
+ @results
270
+ end
271
+
272
+ def has_next?
273
+ !!@results.next_uri
274
+ end
275
+
276
+ def advance
277
+ if closed? || !has_next?
278
+ return false
279
+ end
280
+ uri = @results.next_uri
281
+
282
+ start = Time.now
283
+ attempts = 0
284
+
285
+ begin
286
+ begin
287
+ response = @faraday.get do |req|
288
+ req.url uri
289
+ end
290
+ rescue => e
291
+ @exception = e
292
+ raise @exception
293
+ end
294
+
295
+ if response.status == 200 && !response.body.to_s.empty?
296
+ @results = QueryResults.decode_hash(JSON.parse(response.body))
297
+ return true
298
+ end
299
+
300
+ if response.status != 503 # retry on 503 Service Unavailable
301
+ # deterministic error
302
+ @exception = StandardError.new("Error fetching next at #{uri} returned #{response.status}: #{response.body}") # TODO error class
303
+ raise @exception
304
+ end
305
+
306
+ attempts += 1
307
+ sleep attempts * 0.1
308
+ end while (Time.now - start) < 2*60*60 && !@closed
309
+
310
+ @exception = StandardError.new("Error fetching next") # TODO error class
311
+ raise @exception
312
+ end
313
+
314
+ def close
315
+ return if @closed
316
+
317
+ # cancel running statement
318
+ if uri = @results.next_uri
319
+ # TODO error handling
320
+ # TODO make async reqeust and ignore response
321
+ @faraday.delete do |req|
322
+ req.url uri
323
+ end
324
+ end
325
+
326
+ @closed = true
327
+ nil
328
+ end
329
+ end
330
+
331
+ class Query
332
+ def self.start(session, query)
333
+ faraday = Faraday.new(url: "http://#{session.server}") do |faraday|
334
+ faraday.request :url_encoded
335
+ faraday.response :logger
336
+ faraday.adapter Faraday.default_adapter
337
+ end
338
+
339
+ new StatementClient.new(faraday, session, query)
340
+ end
341
+
342
+ def initialize(client)
343
+ @client = client
344
+ end
345
+
346
+ def wait_for_data
347
+ while @client.has_next? && @client.current_results.data == nil
348
+ @client.advance
349
+ end
350
+ end
351
+
352
+ private :wait_for_data
353
+
354
+ def columns
355
+ wait_for_data
356
+
357
+ raise_error unless @client.query_succeeded?
358
+
359
+ return @client.current_results.columns
360
+ end
361
+
362
+ def each_row(&block)
363
+ wait_for_data
364
+
365
+ raise_error unless @client.query_succeeded?
366
+
367
+ if self.columns == nil
368
+ raise "Query #{@client.current_results.id} has no columns"
369
+ end
370
+
371
+ begin
372
+ if data = @client.current_results.data
373
+ data.each(&block)
374
+ end
375
+ @client.advance
376
+ end while @client.has_next?
377
+ end
378
+
379
+ def raise_error
380
+ if @client.closed?
381
+ raise "Query aborted by user"
382
+ elsif @client.exception?
383
+ raise "Query is gone: #{@client.exception}"
384
+ elsif @client.query_failed?
385
+ results = @client.current_results
386
+ # TODO error location
387
+ raise "Query #{results.id} failed: #{results.error}"
388
+ end
389
+ end
390
+
391
+ private :raise_error
392
+ end
393
+
394
+ class Client
395
+ def initialize(options)
396
+ @session = ClientSession.new(options)
397
+ end
398
+
399
+ def query(query)
400
+ Query.start(@session, query)
401
+ end
402
+ end
403
+ end
404
+
405
+ require 'pp'
406
+
407
+ client = PrestoClient::Client.new(
408
+ server: "localhost:8880",
409
+ user: "frsyuki",
410
+ catalog: "native",
411
+ schema: "default",
412
+ debug: true
413
+ )
414
+
415
+ q = client.query("select * from sys.query")
416
+ p q.columns
417
+ q.each_row {|row|
418
+ p row
419
+ }
420
+
@@ -0,0 +1,15 @@
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 'json'
12
+ require 'webmock/rspec'
13
+
14
+ require 'presto-client'
15
+ include Presto::Client
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ describe Presto::Client::StatementClient do
4
+ let :session do
5
+ session = ClientSession.new(
6
+ server: "localhost",
7
+ user: "frsyuki",
8
+ catalog: "native",
9
+ schema: "default",
10
+ debug: true,
11
+ )
12
+ end
13
+
14
+ let :query do
15
+ "select * from sys.node"
16
+ end
17
+
18
+ let :response_json do
19
+ {
20
+ id: "queryid",
21
+ stats: {}
22
+ }
23
+ end
24
+
25
+ it do
26
+ stub_request(:post, "localhost/v1/statement").
27
+ with(body: query,
28
+ headers: {
29
+ "User-Agent" => "presto-ruby/#{VERSION}",
30
+ "X-Presto-Catalog" => session.catalog,
31
+ "X-Presto-Schema" => session.schema,
32
+ "X-Presto-User" => session.user,
33
+ }).to_return(body: response_json.to_json)
34
+
35
+ faraday = Faraday.new(url: "http://localhost")
36
+ StatementClient.new(faraday, session, query)
37
+ end
38
+ end
39
+
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: presto-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Sadayuki Furuhashi
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-01-07 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: faraday
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.8.8
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 0.8.8
30
+ - !ruby/object:Gem::Dependency
31
+ name: multi_json
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '1.0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '1.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rake
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: 0.9.2
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: 0.9.2
62
+ - !ruby/object:Gem::Dependency
63
+ name: rspec
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 2.13.0
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 2.13.0
78
+ - !ruby/object:Gem::Dependency
79
+ name: webmock
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: 1.16.1
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: 1.16.1
94
+ description: Presto client library
95
+ email:
96
+ - sf@treasure-data.com
97
+ executables: []
98
+ extensions: []
99
+ extra_rdoc_files: []
100
+ files:
101
+ - Gemfile
102
+ - Gemfile.lock
103
+ - README.md
104
+ - Rakefile
105
+ - lib/presto-client.rb
106
+ - lib/presto/client.rb
107
+ - lib/presto/client/client.rb
108
+ - lib/presto/client/models.rb
109
+ - lib/presto/client/query.rb
110
+ - lib/presto/client/statement_client.rb
111
+ - lib/presto/client/version.rb
112
+ - presto-client.gemspec
113
+ - presto-client.rb
114
+ - spec/spec_helper.rb
115
+ - spec/statement_client_spec.rb
116
+ homepage: https://github.com/treasure-data/presto-client-ruby
117
+ licenses:
118
+ - Apache 2.0
119
+ post_install_message:
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ none: false
125
+ requirements:
126
+ - - ! '>='
127
+ - !ruby/object:Gem::Version
128
+ version: 1.9.3
129
+ required_rubygems_version: !ruby/object:Gem::Requirement
130
+ none: false
131
+ requirements:
132
+ - - ! '>='
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ segments:
136
+ - 0
137
+ hash: 1002266789771022757
138
+ requirements: []
139
+ rubyforge_project:
140
+ rubygems_version: 1.8.23
141
+ signing_key:
142
+ specification_version: 3
143
+ summary: Presto client library
144
+ test_files:
145
+ - spec/spec_helper.rb
146
+ - spec/statement_client_spec.rb