presto-client 0.1.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.
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