presto-client 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -36,14 +36,20 @@ module Presto::Client
36
36
  new StatementClient.new(faraday, query, options)
37
37
  end
38
38
 
39
- def initialize(client)
40
- @client = client
39
+ def initialize(api)
40
+ @api = api
41
41
  end
42
42
 
43
- attr_reader :client
43
+ def current_results
44
+ @api.current_results
45
+ end
46
+
47
+ def advance
48
+ @api.advance
49
+ end
44
50
 
45
51
  def wait_for_data
46
- while @client.current_results.data == nil && @client.advance
52
+ while @api.current_results.data == nil && @api.advance
47
53
  end
48
54
  end
49
55
 
@@ -52,9 +58,9 @@ module Presto::Client
52
58
  def columns
53
59
  wait_for_data
54
60
 
55
- raise_error unless @client.query_succeeded?
61
+ raise_error unless @api.query_succeeded?
56
62
 
57
- return @client.current_results.columns
63
+ return @api.current_results.columns
58
64
  end
59
65
 
60
66
  def rows
@@ -74,36 +80,40 @@ module Presto::Client
74
80
  def each_row_chunk(&block)
75
81
  wait_for_data
76
82
 
77
- raise_error unless @client.query_succeeded?
83
+ raise_error unless @api.query_succeeded?
78
84
 
79
85
  if self.columns == nil
80
- raise PrestoError, "Query #{@client.current_results.id} has no columns"
86
+ raise PrestoError, "Query #{@api.current_results.id} has no columns"
81
87
  end
82
88
 
83
89
  begin
84
- if data = @client.current_results.data
90
+ if data = @api.current_results.data
85
91
  block.call(data)
86
92
  end
87
- end while @client.advance
93
+ end while @api.advance
94
+ end
95
+
96
+ def query_info
97
+ @api.query_info
88
98
  end
89
99
 
90
100
  def cancel
91
- @client.cancel_leaf_stage
101
+ @api.cancel_leaf_stage
92
102
  end
93
103
 
94
104
  def close
95
- @client.cancel_leaf_stage
105
+ @api.cancel_leaf_stage
96
106
  nil
97
107
  end
98
108
 
99
109
  def raise_error
100
- if @client.closed?
110
+ if @api.closed?
101
111
  raise PrestoClientError, "Query aborted by user"
102
- elsif @client.exception?
112
+ elsif @api.exception?
103
113
  # query is gone
104
- raise @client.exception
105
- elsif @client.query_failed?
106
- results = @client.current_results
114
+ raise @api.exception
115
+ elsif @api.query_failed?
116
+ results = @api.current_results
107
117
  error = results.error
108
118
  raise PrestoQueryError.new("Query #{results.id} failed: #{error.message}", results.id, error.error_code, error.failure_info)
109
119
  end
@@ -89,7 +89,7 @@ module Presto::Client
89
89
 
90
90
  body = response.body
91
91
  hash = MultiJson.load(body)
92
- @results = QueryResults.decode_hash(hash)
92
+ @results = Models::QueryResults.decode(hash)
93
93
  end
94
94
 
95
95
  private :post_query_request!
@@ -132,27 +132,36 @@ module Presto::Client
132
132
  end
133
133
  uri = @results.next_uri
134
134
 
135
+ body = faraday_get_with_retry(uri)
136
+ @results = Models::QueryResults.decode(MultiJson.load(body))
137
+
138
+ return true
139
+ end
140
+
141
+ def query_info
142
+ body = faraday_get_with_retry("/v1/query/#{@results.id}")
143
+ Models::QueryInfo.decode(MultiJson.load(body))
144
+ end
145
+
146
+ def faraday_get_with_retry(uri, &block)
135
147
  start = Time.now
136
148
  attempts = 0
137
149
 
138
150
  begin
139
151
  begin
140
- response = @faraday.get do |req|
141
- req.url uri
142
- end
152
+ response = @faraday.get(uri)
143
153
  rescue => e
144
154
  @exception = e
145
155
  raise @exception
146
156
  end
147
157
 
148
158
  if response.status == 200 && !response.body.to_s.empty?
149
- @results = QueryResults.decode_hash(MultiJson.load(response.body))
150
- return true
159
+ return response.body
151
160
  end
152
161
 
153
- if response.status != 503 # retry on 503 Service Unavailable
162
+ if response.status != 503 # retry only if 503 Service Unavailable
154
163
  # deterministic error
155
- @exception = PrestoHttpError.new(response.status, "Error fetching next at #{uri} returned #{response.status}: #{response.body}")
164
+ @exception = PrestoHttpError.new(response.status, "Presto API error at #{uri} returned #{response.status}: #{response.body}")
156
165
  raise @exception
157
166
  end
158
167
 
@@ -160,7 +169,7 @@ module Presto::Client
160
169
  sleep attempts * 0.1
161
170
  end while (Time.now - start) < 2*60*60 && !@closed
162
171
 
163
- @exception = PrestoHttpError.new(408, "Error fetching next due to timeout")
172
+ @exception = PrestoHttpError.new(408, "Presto API error due to timeout")
164
173
  raise @exception
165
174
  end
166
175
 
@@ -15,6 +15,6 @@
15
15
  #
16
16
  module Presto
17
17
  module Client
18
- VERSION = "0.3.3"
18
+ VERSION = "0.4.0"
19
19
  end
20
20
  end
@@ -0,0 +1,69 @@
1
+
2
+ if ARGV.length != 3
3
+ puts "usage: <presto-source-dir> <template.erb> <output.rb>"
4
+ end
5
+
6
+ source_dir, template_path, output_path = *ARGV
7
+
8
+ require_relative 'presto_models'
9
+
10
+ require 'erb'
11
+ erb = ERB.new(File.read(template_path))
12
+
13
+ source_path = "/Users/frsyuki/project/presto-client-ruby/presto"
14
+
15
+ predefined_simple_classes = %w[QueryId StageId TaskId PlanNodeId PlanFragmentId ConnectorSession]
16
+ predefined_models = %w[DistributionSnapshot PlanNode]
17
+
18
+ assume_primitive = %w[Object Type Symbol URI Duration DataSize DateTime ConnectorTableHandle ConnectorOutputTableHandle ConnectorIndexHandle ConnectorColumnHandle Expression FunctionCall]
19
+ enum_types = %w[QueryState StageState TaskState QueueState PlanDistribution OutputPartitioning Step SortOrder]
20
+
21
+ root_models = %w[QueryResults QueryInfo] + %w[
22
+ OutputNode
23
+ ProjectNode
24
+ TableScanNode
25
+ ValuesNode
26
+ AggregationNode
27
+ MarkDistinctNode
28
+ MaterializeSampleNode
29
+ FilterNode
30
+ WindowNode
31
+ LimitNode
32
+ DistinctLimitNode
33
+ TopNNode
34
+ SampleNode
35
+ SortNode
36
+ ExchangeNode
37
+ SinkNode
38
+ JoinNode
39
+ SemiJoinNode
40
+ IndexJoinNode
41
+ IndexSourceNode
42
+ TableWriterNode
43
+ TableCommitNode
44
+ ]
45
+
46
+ analyzer = PrestoModels::ModelAnalyzer.new(
47
+ source_path,
48
+ skip_models: predefined_models + predefined_simple_classes + assume_primitive + enum_types
49
+ )
50
+ analyzer.analyze(root_models)
51
+ models = analyzer.models
52
+ skipped_models = analyzer.skipped_models
53
+
54
+ formatter = PrestoModels::ModelFormatter.new(
55
+ base_indent_count: 2,
56
+ struct_class: "Base",
57
+ special_struct_initialize_method: "initialize_struct",
58
+ primitive_types: assume_primitive,
59
+ skip_types: skipped_models,
60
+ simple_classes: predefined_simple_classes,
61
+ enum_types: enum_types,
62
+ )
63
+ formatter.format(models)
64
+
65
+ @contents = formatter.contents
66
+
67
+ data = erb.result
68
+ File.open(output_path, 'w') {|f| f.write data }
69
+
@@ -0,0 +1,132 @@
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
+ ####
19
+ ## lib/presto/client/models.rb is automatically generated using "rake modelgen" command.
20
+ ## You should not edit this file directly. To modify the class definitions, edit
21
+ ## modelgen/models.rb file and run "rake modelgen".
22
+ ##
23
+
24
+ module Models
25
+ class Base < Struct
26
+ class << self
27
+ alias_method :new_struct, :new
28
+
29
+ def new(*args)
30
+ new_struct(*args) do
31
+ # make it immutable
32
+ undef_method :"[]="
33
+ members.each do |m|
34
+ undef_method :"#{m}="
35
+ end
36
+
37
+ # replace constructor to receive hash instead of array
38
+ alias_method :initialize_struct, :initialize
39
+
40
+ def initialize(params={})
41
+ initialize_struct(*members.map {|m| params[m] })
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ class QueryId < String
49
+ end
50
+
51
+ class StageId < String
52
+ end
53
+
54
+ class TaskId < String
55
+ end
56
+
57
+ class PlanNodeId < String
58
+ end
59
+
60
+ class PlanFragmentId < String
61
+ end
62
+
63
+ class ConnectorSession < Hash
64
+ def initialize(hash)
65
+ super()
66
+ merge!(hash)
67
+ end
68
+ end
69
+
70
+ module PlanNode
71
+ def self.decode(hash)
72
+ model_class = case hash["type"]
73
+ when "output" then OutputNode
74
+ when "project" then ProjectNode
75
+ when "tablescan" then TableScanNode
76
+ when "values" then ValuesNode
77
+ when "aggregation" then AggregationNode
78
+ when "markDistinct" then MarkDistinctNode
79
+ when "materializeSample" then MaterializeSampleNode
80
+ when "filter" then FilterNode
81
+ when "window" then WindowNode
82
+ when "limit" then LimitNode
83
+ when "distinctlimit" then DistinctLimitNode
84
+ when "topn" then TopNNode
85
+ when "sample" then SampleNode
86
+ when "sort" then SortNode
87
+ when "exchange" then ExchangeNode
88
+ when "sink" then SinkNode
89
+ when "join" then JoinNode
90
+ when "semijoin" then SemiJoinNode
91
+ when "indexjoin" then IndexJoinNode
92
+ when "indexsource" then IndexSourceNode
93
+ when "tablewriter" then TableWriterNode
94
+ when "tablecommit" then TableCommitNode
95
+ end
96
+ model_class.decode(hash) if model_class
97
+ end
98
+ end
99
+
100
+ # io.airlift.stats.Distribution.DistributionSnapshot
101
+ class << DistributionSnapshot =
102
+ Base.new(:max_error, :count, :total, :p01, :p05, :p10, :p25, :p50, :p75, :p90, :p95, :p99, :min, :max)
103
+ def decode(hash)
104
+ obj = allocate
105
+ obj.send(:initialize_struct,
106
+ hash["maxError"],
107
+ hash["count"],
108
+ hash["total"],
109
+ hash["p01"],
110
+ hash["p05"],
111
+ hash["p10"],
112
+ hash["p25"],
113
+ hash["p50"],
114
+ hash["p75"],
115
+ hash["p90"],
116
+ hash["p95"],
117
+ hash["p99"],
118
+ hash["min"],
119
+ hash["max"],
120
+ )
121
+ obj
122
+ end
123
+ end
124
+
125
+
126
+ ##
127
+ # Those model classes are automatically generated
128
+ #
129
+
130
+ <%= @contents %>
131
+ end
132
+ end
@@ -0,0 +1,210 @@
1
+
2
+ module PrestoModels
3
+ require 'find'
4
+ require 'stringio'
5
+
6
+ PRIMITIVE_TYPES = %w[String boolean long int short byte double float]
7
+
8
+ class Model < Struct.new(:name, :fields)
9
+ end
10
+
11
+ class Field < Struct.new(:key, :nullable, :array, :map, :type, :base_type, :map_value_base_type)
12
+ alias_method :nullable?, :nullable
13
+ alias_method :array?, :array
14
+ alias_method :map?, :map
15
+
16
+ def name
17
+ @name ||= key.gsub(/[A-Z]/) {|f| "_#{f.downcase}" }
18
+ end
19
+ end
20
+
21
+ class ModelAnalysisError < StandardError
22
+ end
23
+
24
+ class ModelAnalyzer
25
+ def initialize(source_path, options={})
26
+ @source_path = source_path
27
+ @ignore_types = PRIMITIVE_TYPES + (options[:skip_models] || [])
28
+ @models = {}
29
+ @skipped_models = []
30
+ end
31
+
32
+ attr_reader :skipped_models
33
+
34
+ def models
35
+ @models.values.sort_by {|model| model.name }
36
+ end
37
+
38
+ def analyze(root_models)
39
+ root_models.each {|model_name|
40
+ analyze_model(model_name)
41
+ }
42
+ end
43
+
44
+ private
45
+
46
+ PROPERTY_PATTERN = /@JsonProperty\(\"(\w+)\"\)\s+(@Nullable\s+)?([\w\<\>\,\s]+)\s+(\w+)/
47
+ CREATOR_PATTERN = /@JsonCreator[\w\s]+\((?:\s*#{PROPERTY_PATTERN}\s*,?)+\)/
48
+
49
+ def analyze_model(model_name)
50
+ return if @models[model_name] || @ignore_types.include?(model_name)
51
+
52
+ path = find_class_file(model_name)
53
+ java = File.read(path)
54
+
55
+ m = CREATOR_PATTERN.match(java)
56
+ unless m
57
+ raise ModelAnalysisError, "Can't find JsonCreator of a model class #{model_name}"
58
+ end
59
+
60
+ fields = m[0].scan(PROPERTY_PATTERN).map do |key,nullable,type,field|
61
+ map = false
62
+ array = false
63
+ nullable = !!nullable
64
+ if m = /(?:List|Set)<(\w+)>/.match(type)
65
+ base_type = m[1]
66
+ array = true
67
+ elsif m = /(?:Map)<(\w+),\s*(\w+)>/.match(type)
68
+ base_type = m[1]
69
+ map_value_base_type = m[2]
70
+ map = true
71
+ elsif m = /Optional<(\w+)>/.match(type)
72
+ base_type = m[1]
73
+ nullable = true
74
+ elsif type =~ /\w+/
75
+ base_type = type
76
+ else
77
+ raise ModelAnalysisError, "Unsupported type #{type} in model #{model_name}"
78
+ end
79
+ Field.new(key, !!nullable, array, map, type, base_type, map_value_base_type)
80
+ end
81
+
82
+ @models[model_name] = Model.new(model_name, fields)
83
+
84
+ # recursive call
85
+ fields.each do |field|
86
+ analyze_model(field.base_type)
87
+ analyze_model(field.map_value_base_type) if field.map_value_base_type
88
+ end
89
+
90
+ rescue => e
91
+ puts "Skipping model #{model_name}: #{e}"
92
+ @skipped_models << model_name
93
+ end
94
+
95
+ def find_class_file(model_name)
96
+ @source_files ||= Find.find(@source_path).to_a
97
+ pattern = /\/#{model_name}.java$/
98
+ matched = @source_files.find {|path| path =~ pattern }
99
+ unless matched
100
+ raise ModelAnalysisError, "Model class #{model_name} is not found"
101
+ end
102
+ return matched
103
+ end
104
+ end
105
+
106
+ class ModelFormatter
107
+ def initialize(options={})
108
+ @indent = options[:indent] || ' '
109
+ @base_indent_count = options[:base_indent_count] || 0
110
+ @struct_class = options[:struct_class] || 'Struct'
111
+ @special_struct_initialize_method = options[:special_struct_initialize_method]
112
+ @primitive_types = PRIMITIVE_TYPES + (options[:primitive_types] || [])
113
+ @skip_types = options[:skip_types] || []
114
+ @simple_classes = options[:simple_classes]
115
+ @enum_types = options[:enum_types]
116
+ @special_types = options[:special_types] || {}
117
+ @data = StringIO.new
118
+ end
119
+
120
+ def contents
121
+ @data.string
122
+ end
123
+
124
+ def format(models)
125
+ @models = models
126
+ models.each do |model|
127
+ @model = model
128
+
129
+ puts_with_indent 0, "class << #{model.name} ="
130
+ puts_with_indent 2, "#{@struct_class}.new(#{model.fields.map {|f| ":#{f.name}" }.join(', ')})"
131
+ format_decode
132
+ puts_with_indent 0, "end"
133
+ line
134
+ end
135
+ end
136
+
137
+ private
138
+
139
+ def line
140
+ @data.puts ""
141
+ end
142
+
143
+ def puts_with_indent(n, str)
144
+ @data.puts "#{@indent * (@base_indent_count + n)}#{str}"
145
+ end
146
+
147
+ def format_decode
148
+ puts_with_indent 1, "def decode(hash)"
149
+ if @special_struct_initialize_method
150
+ puts_with_indent 2, "obj = allocate"
151
+ puts_with_indent 2, "obj.send(:#{@special_struct_initialize_method},"
152
+ else
153
+ puts_with_indent 2, "new("
154
+ end
155
+
156
+ @model.fields.each do |field|
157
+ next if @skip_types.include?(field.base_type) || @skip_types.include?(field.map_value_base_type)
158
+
159
+ if @primitive_types.include?(field.base_type) && !field.map?
160
+ expr = "hash[\"#{field.key}\"]"
161
+ else
162
+ expr = ""
163
+ expr << "hash[\"#{field.key}\"] && " #if field.nullable?
164
+
165
+ if field.map?
166
+ key_expr = convert_expression(field.base_type, field.base_type, "k")
167
+ value_expr = convert_expression(field.map_value_base_type, field.map_value_base_type, "v")
168
+ if key_expr == 'k' && value_expr == 'v'
169
+ expr = "hash[\"#{field.key}\"]"
170
+ else
171
+ expr << "Hash[hash[\"#{field.key}\"].to_a.map! {|k,v| [#{key_expr}, #{value_expr}] }]"
172
+ end
173
+ elsif field.array?
174
+ elem_expr = convert_expression(field.base_type, field.base_type, "h")
175
+ expr << "hash[\"#{field.key}\"].map {|h| #{elem_expr} }"
176
+ else
177
+ expr << convert_expression(field.type, field.base_type, "hash[\"#{field.key}\"]")
178
+ end
179
+ end
180
+
181
+ #comment = "# #{field.base_type}#{field.array? ? '[]' : ''} #{field.key}"
182
+ #puts_with_indent 3, "#{expr}, #{comment}"
183
+ puts_with_indent 3, "#{expr},"
184
+ end
185
+
186
+ puts_with_indent 2, ")"
187
+
188
+ if @special_struct_initialize_method
189
+ puts_with_indent 2, "obj"
190
+ end
191
+
192
+ puts_with_indent 1, "end"
193
+ end
194
+
195
+ def convert_expression(type, base_type, key)
196
+ if @special_types[type]
197
+ special.call(key)
198
+ elsif @enum_types.include?(type)
199
+ "#{key}.downcase.to_sym"
200
+ elsif @primitive_types.include?(base_type)
201
+ key
202
+ elsif @simple_classes.include?(base_type)
203
+ "#{base_type}.new(#{key})"
204
+ else # model class
205
+ "#{base_type}.decode(#{key})"
206
+ end
207
+ end
208
+ end
209
+ end
210
+