presto-client 0.3.3 → 0.4.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.
@@ -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
+