trino-client 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.github/CODEOWNERS +1 -0
  3. data/.github/PULL_REQUEST_TEMPLATE.md +18 -0
  4. data/.github/workflows/ruby.yml +30 -0
  5. data/.gitignore +4 -0
  6. data/ChangeLog.md +168 -0
  7. data/Gemfile +7 -0
  8. data/LICENSE +202 -0
  9. data/README.md +131 -0
  10. data/Rakefile +45 -0
  11. data/lib/trino-client.rb +1 -0
  12. data/lib/trino/client.rb +23 -0
  13. data/lib/trino/client/client.rb +78 -0
  14. data/lib/trino/client/errors.rb +46 -0
  15. data/lib/trino/client/faraday_client.rb +242 -0
  16. data/lib/trino/client/model_versions/0.149.rb +1683 -0
  17. data/lib/trino/client/model_versions/0.153.rb +1719 -0
  18. data/lib/trino/client/model_versions/0.173.rb +1685 -0
  19. data/lib/trino/client/model_versions/0.178.rb +1964 -0
  20. data/lib/trino/client/model_versions/0.205.rb +2169 -0
  21. data/lib/trino/client/model_versions/303.rb +2574 -0
  22. data/lib/trino/client/model_versions/316.rb +2595 -0
  23. data/lib/trino/client/model_versions/351.rb +2726 -0
  24. data/lib/trino/client/models.rb +38 -0
  25. data/lib/trino/client/query.rb +144 -0
  26. data/lib/trino/client/statement_client.rb +279 -0
  27. data/lib/trino/client/version.rb +20 -0
  28. data/modelgen/model_versions.rb +280 -0
  29. data/modelgen/modelgen.rb +119 -0
  30. data/modelgen/models.rb +31 -0
  31. data/modelgen/trino_models.rb +270 -0
  32. data/release.rb +56 -0
  33. data/spec/basic_query_spec.rb +82 -0
  34. data/spec/client_spec.rb +75 -0
  35. data/spec/gzip_spec.rb +40 -0
  36. data/spec/model_spec.rb +35 -0
  37. data/spec/spec_helper.rb +42 -0
  38. data/spec/statement_client_spec.rb +637 -0
  39. data/spec/tpch/q01.sql +21 -0
  40. data/spec/tpch/q02.sql +43 -0
  41. data/spec/tpch_query_spec.rb +41 -0
  42. data/trino-client.gemspec +31 -0
  43. metadata +211 -0
@@ -0,0 +1,270 @@
1
+
2
+ module TrinoModels
3
+ require 'find'
4
+ require 'stringio'
5
+
6
+ PRIMITIVE_TYPES = %w[String boolean long int short byte double float Integer Double Boolean]
7
+ ARRAY_PRIMITIVE_TYPES = PRIMITIVE_TYPES.map { |t| "#{t}[]" }
8
+
9
+ class Model < Struct.new(:name, :fields)
10
+ end
11
+
12
+ class Field < Struct.new(:key, :nullable, :array, :map, :type, :base_type, :map_value_base_type, :base_type_alias)
13
+ alias_method :nullable?, :nullable
14
+ alias_method :array?, :array
15
+ alias_method :map?, :map
16
+
17
+ def name
18
+ @name ||= key.gsub(/[A-Z]/) {|f| "_#{f.downcase}" }
19
+ end
20
+ end
21
+
22
+ class ModelAnalysisError < StandardError
23
+ end
24
+
25
+ class ModelAnalyzer
26
+ def initialize(source_path, options={})
27
+ @source_path = source_path
28
+ @ignore_types = PRIMITIVE_TYPES + ARRAY_PRIMITIVE_TYPES + (options[:skip_models] || [])
29
+ @path_mapping = options[:path_mapping] || {}
30
+ @name_mapping = options[:name_mapping] || {}
31
+ @extra_fields = options[:extra_fields] || {}
32
+ @models = {}
33
+ @skipped_models = []
34
+ end
35
+
36
+ attr_reader :skipped_models
37
+
38
+ def models
39
+ @models.values.sort_by {|model| model.name }
40
+ end
41
+
42
+ def analyze(root_models)
43
+ root_models.each {|model_name|
44
+ analyze_model(model_name)
45
+ }
46
+ end
47
+
48
+ private
49
+
50
+ PROPERTY_PATTERN = /@JsonProperty\(\"(\w+)\"\)\s+(@Nullable\s+)?([\w\<\>\[\]\,\s\.]+)\s+\w+/
51
+ CREATOR_PATTERN = /@JsonCreator[\s]+public[\s]+(static\s+)?(\w+)[\w\s]*\((?:\s*#{PROPERTY_PATTERN}\s*,?)+\)/
52
+ GENERIC_PATTERN = /(\w+)\<(\w+)\>/
53
+
54
+ def analyze_fields(model_name, creator_block, generic: nil)
55
+ model_name = "#{model_name}_#{generic}" if generic
56
+ extra = @extra_fields[model_name] || []
57
+ fields = creator_block.scan(PROPERTY_PATTERN).concat(extra).map do |key,nullable,type|
58
+ map = false
59
+ array = false
60
+ nullable = !!nullable
61
+ if m = /(?:List|Set)<(\w+)>/.match(type)
62
+ base_type = m[1]
63
+ array = true
64
+ elsif m = /(?:Map|ListMultimap)<(\w+),\s*(\w+)>/.match(type)
65
+ base_type = m[1]
66
+ map_value_base_type = m[2]
67
+ map = true
68
+ elsif m = /Optional<([\w\[\]\<\>]+)>/.match(type)
69
+ base_type = m[1]
70
+ nullable = true
71
+ elsif m = /OptionalInt/.match(type)
72
+ base_type = 'Integer'
73
+ nullable = true
74
+ elsif m = /OptionalLong/.match(type)
75
+ base_type = 'Long'
76
+ nullable = true
77
+ elsif m = /OptionalDouble/.match(type)
78
+ base_type = 'Double'
79
+ nullable = true
80
+ elsif type =~ /\w+/
81
+ base_type = type
82
+ else
83
+ raise ModelAnalysisError, "Unsupported type #{type} in model #{model_name}"
84
+ end
85
+ base_type = @name_mapping[[model_name, base_type]] || base_type
86
+ map_value_base_type = @name_mapping[[model_name, map_value_base_type]] || map_value_base_type
87
+
88
+ if generic
89
+ base_type = generic if base_type == 'T'
90
+ map_value_base_type = generic if map_value_base_type == 'T'
91
+ end
92
+ if m = GENERIC_PATTERN.match(base_type)
93
+ base_type_alias = "#{m[1]}_#{m[2]}"
94
+ end
95
+
96
+ Field.new(key, !!nullable, array, map, type, base_type, map_value_base_type, base_type_alias)
97
+ end
98
+
99
+ @models[model_name] = Model.new(model_name, fields)
100
+ # recursive call
101
+ fields.each do |field|
102
+ analyze_model(field.base_type, model_name)
103
+ analyze_model(field.map_value_base_type, model_name) if field.map_value_base_type
104
+ end
105
+
106
+ return fields
107
+ end
108
+
109
+ def analyze_model(model_name, parent_model= nil, generic: nil)
110
+ return if @models[model_name] || @ignore_types.include?(model_name)
111
+
112
+ if m = GENERIC_PATTERN.match(model_name)
113
+ analyze_model(m[1], generic: m[2])
114
+ analyze_model(m[2])
115
+ return
116
+ end
117
+
118
+ path = find_class_file(model_name, parent_model)
119
+ java = File.read(path)
120
+
121
+ m = CREATOR_PATTERN.match(java)
122
+ unless m
123
+ raise ModelAnalysisError, "Can't find JsonCreator of a model class #{model_name} of #{parent_model} at #{path}"
124
+ end
125
+
126
+ body = m[0]
127
+ # check inner class first
128
+ while true
129
+ offset = m.end(0)
130
+ m = CREATOR_PATTERN.match(java, offset)
131
+ break unless m
132
+ inner_model_name = m[2]
133
+ next if @models[inner_model_name] || @ignore_types.include?(inner_model_name)
134
+ fields = analyze_fields(inner_model_name, m[0])
135
+ end
136
+
137
+ fields = analyze_fields(model_name, body, generic: generic)
138
+
139
+ rescue => e
140
+ puts "Skipping model #{parent_model}/#{model_name}: #{e}"
141
+ @skipped_models << model_name
142
+ end
143
+
144
+ def find_class_file(model_name, parent_model)
145
+ return @path_mapping[model_name] if @path_mapping.has_key? model_name
146
+
147
+ @source_files ||= Find.find(@source_path).to_a
148
+ pattern = /\/#{model_name}.java$/
149
+ matched = @source_files.find_all {|path| path =~ pattern && !path.include?('/test/') && !path.include?('/verifier/')}
150
+ if matched.empty?
151
+ raise ModelAnalysisError, "Model class #{model_name} is not found"
152
+ end
153
+ if matched.size == 1
154
+ return matched.first
155
+ else
156
+ raise ModelAnalysisError, "Model class #{model_name} of #{parent_model} found multiple match #{matched}"
157
+ end
158
+ end
159
+ end
160
+
161
+ class ModelFormatter
162
+ def initialize(options={})
163
+ @indent = options[:indent] || ' '
164
+ @base_indent_count = options[:base_indent_count] || 0
165
+ @struct_class = options[:struct_class] || 'Struct'
166
+ @special_struct_initialize_method = options[:special_struct_initialize_method]
167
+ @primitive_types = PRIMITIVE_TYPES + ARRAY_PRIMITIVE_TYPES + (options[:primitive_types] || [])
168
+ @skip_types = options[:skip_types] || []
169
+ @simple_classes = options[:simple_classes]
170
+ @enum_types = options[:enum_types]
171
+ @special_types = options[:special_types] || {}
172
+ @data = StringIO.new
173
+ end
174
+
175
+ def contents
176
+ @data.string
177
+ end
178
+
179
+ def format(models)
180
+ @models = models
181
+ models.each do |model|
182
+ @model = model
183
+
184
+ puts_with_indent 0, "class << #{model.name} ="
185
+ puts_with_indent 2, "#{@struct_class}.new(#{model.fields.map {|f| ":#{f.name}" }.join(', ')})"
186
+ format_decode
187
+ puts_with_indent 0, "end"
188
+ line
189
+ end
190
+ end
191
+
192
+ private
193
+
194
+ def line
195
+ @data.puts ""
196
+ end
197
+
198
+ def puts_with_indent(n, str)
199
+ @data.puts "#{@indent * (@base_indent_count + n)}#{str}"
200
+ end
201
+
202
+ def format_decode
203
+ puts_with_indent 1, "def decode(hash)"
204
+
205
+ puts_with_indent 2, "unless hash.is_a?(Hash)"
206
+ puts_with_indent 3, "raise TypeError, \"Can't convert \#{hash.class} to Hash\""
207
+ puts_with_indent 2, "end"
208
+
209
+ if @special_struct_initialize_method
210
+ puts_with_indent 2, "obj = allocate"
211
+ puts_with_indent 2, "obj.send(:#{@special_struct_initialize_method},"
212
+ else
213
+ puts_with_indent 2, "new("
214
+ end
215
+
216
+ @model.fields.each do |field|
217
+ next if @skip_types.include?(field.base_type) || @skip_types.include?(field.map_value_base_type)
218
+
219
+ if @primitive_types.include?(field.base_type) && !field.map?
220
+ expr = "hash[\"#{field.key}\"]"
221
+ else
222
+ expr = ""
223
+ expr << "hash[\"#{field.key}\"] && " #if field.nullable?
224
+
225
+ if field.map?
226
+ key_expr = convert_expression(field.base_type, field.base_type, "k")
227
+ value_expr = convert_expression(field.map_value_base_type, field.map_value_base_type, "v")
228
+ if key_expr == 'k' && value_expr == 'v'
229
+ expr = "hash[\"#{field.key}\"]"
230
+ else
231
+ expr << "Hash[hash[\"#{field.key}\"].to_a.map! {|k,v| [#{key_expr}, #{value_expr}] }]"
232
+ end
233
+ elsif field.array?
234
+ elem_expr = convert_expression(field.base_type, field.base_type, "h")
235
+ expr << "hash[\"#{field.key}\"].map {|h| #{elem_expr} }"
236
+ else
237
+ expr << convert_expression(field.type, field.base_type_alias || field.base_type, "hash[\"#{field.key}\"]")
238
+ end
239
+ end
240
+
241
+ #comment = "# #{field.base_type}#{field.array? ? '[]' : ''} #{field.key}"
242
+ #puts_with_indent 3, "#{expr}, #{comment}"
243
+ puts_with_indent 3, "#{expr},"
244
+ end
245
+
246
+ puts_with_indent 2, ")"
247
+
248
+ if @special_struct_initialize_method
249
+ puts_with_indent 2, "obj"
250
+ end
251
+
252
+ puts_with_indent 1, "end"
253
+ end
254
+
255
+ def convert_expression(type, base_type, key)
256
+ if @special_types[type]
257
+ special.call(key)
258
+ elsif @enum_types.include?(type) || @enum_types.include?(base_type)
259
+ "#{key}.downcase.to_sym"
260
+ elsif @primitive_types.include?(base_type)
261
+ key
262
+ elsif @simple_classes.include?(base_type)
263
+ "#{base_type}.new(#{key})"
264
+ else # model class
265
+ "#{base_type}.decode(#{key})"
266
+ end
267
+ end
268
+ end
269
+ end
270
+
data/release.rb ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'fileutils'
4
+
5
+ PREFIX = 'https://github.com/treasure-data/trino-client-ruby'
6
+ RELEASE_NOTES_FILE = "ChangeLog.md"
7
+
8
+ last_tag = `git describe --tags --abbrev=0`.chomp
9
+ last_version = last_tag.sub("v", "")
10
+ puts "last version: #{last_version}"
11
+
12
+ print "next version? "
13
+ next_version = STDIN.gets.chomp
14
+
15
+ abort("Can't use empty version string") if next_version.empty?
16
+
17
+ logs = `git log #{last_tag}..HEAD --pretty=format:'%h %s'`
18
+ # Add links to GitHub issues
19
+ logs = logs.gsub(/\#([0-9]+)/, "[#\\1](#{PREFIX}/issues/\\1)")
20
+
21
+ new_release_notes = []
22
+ new_release_notes <<= "\#\# #{next_version}\n"
23
+ new_release_notes <<= logs.split(/\n/)
24
+ .reject{|line| line.include?("#{last_version} release notes")}
25
+ .map{|x|
26
+ rev = x[0..6]
27
+ "- #{x[8..-1]} [[#{rev}](#{PREFIX}/commit/#{rev})]\n"
28
+ }
29
+
30
+ release_notes = []
31
+ notes = File.readlines(RELEASE_NOTES_FILE)
32
+
33
+ release_notes <<= notes[0..1]
34
+ release_notes <<= new_release_notes
35
+ release_notes <<= "\n"
36
+ release_notes <<= notes[2..-1]
37
+
38
+ TMP_RELEASE_NOTES_FILE = "#{RELEASE_NOTES_FILE}.tmp"
39
+ File.delete(TMP_RELEASE_NOTES_FILE) if File.exists?(TMP_RELEASE_NOTES_FILE)
40
+ File.write("#{TMP_RELEASE_NOTES_FILE}", release_notes.join)
41
+ system("cat #{TMP_RELEASE_NOTES_FILE} | vim - -c ':f #{TMP_RELEASE_NOTES_FILE}' -c ':9'")
42
+
43
+ abort("The release note file is not saved. Aborted") unless File.exists?(TMP_RELEASE_NOTES_FILE)
44
+
45
+ def run(cmd)
46
+ puts cmd
47
+ system cmd
48
+ end
49
+
50
+ FileUtils.cp(TMP_RELEASE_NOTES_FILE, RELEASE_NOTES_FILE)
51
+ File.delete(TMP_RELEASE_NOTES_FILE)
52
+
53
+ # run "git commit #{RELEASE_NOTES_FILE} -m \"Add #{next_version} release notes\""
54
+ # run "git tag v#{next_version}"
55
+ # run "git push"
56
+ # run "git push --tags"
@@ -0,0 +1,82 @@
1
+ require 'spec_helper'
2
+
3
+ describe Trino::Client::Client do
4
+ before(:all) do
5
+ WebMock.disable!
6
+ @cluster = TinyPresto::Cluster.new()
7
+ @container = @cluster.run
8
+ @client = Trino::Client.new(server: 'localhost:8080', catalog: 'memory', user: 'test-user', schema: 'default')
9
+ loop do
10
+ begin
11
+ @client.run('show schemas')
12
+ break
13
+ rescue StandardError => exception
14
+ puts "Waiting for cluster ready... #{exception}"
15
+ sleep(3)
16
+ end
17
+ end
18
+ puts 'Cluster is ready'
19
+ end
20
+
21
+ after(:all) do
22
+ @cluster.stop
23
+ WebMock.enable!
24
+ end
25
+
26
+ it 'show schemas' do
27
+ columns, rows = run_with_retry(@client, 'show schemas')
28
+ expect(columns.length).to be(1)
29
+ expect(rows.length).to be(2)
30
+ end
31
+
32
+ it 'ctas' do
33
+ expected = [[1, 'a'], [2, 'b']]
34
+ run_with_retry(@client, "create table ctas1 as select * from (values (1, 'a'), (2, 'b')) t(c1, c2)")
35
+ columns, rows = run_with_retry(@client, 'select * from ctas1')
36
+ expect(columns.map(&:name)).to match_array(%w[c1 c2])
37
+ expect(rows).to eq(expected)
38
+ end
39
+
40
+ it 'next_uri' do
41
+ @client.query('show schemas') do |q|
42
+ expect(q.next_uri).to start_with('http://localhost:8080/v1/statement/')
43
+ end
44
+ end
45
+
46
+ it 'advance' do
47
+ @client.query('show schemas') do |q|
48
+ expect(q.advance).to be(true)
49
+ end
50
+ end
51
+
52
+ it 'current query result' do
53
+ @client.query('show schemas') do |q|
54
+ expect(q.current_results.info_uri).to start_with('http://localhost:8080/ui/query.html')
55
+ end
56
+ end
57
+
58
+ it 'statement stats' do
59
+ @client.query('show schemas') do |q|
60
+ stats = q.current_results.stats
61
+ # Immediate subsequent request should get queued result
62
+ expect(stats.queued).to be(true)
63
+ expect(stats.scheduled).to be(false)
64
+ end
65
+ end
66
+
67
+ it 'partial cancel' do
68
+ @client.query('show schemas') do |q|
69
+ q.cancel
70
+ expect { q.query_info }.to raise_error(Trino::Client::TrinoHttpError, /Error 410 Gone/)
71
+ end
72
+ end
73
+
74
+ it 'row chunk' do
75
+ expected_schemas = %w[default information_schema]
76
+ @client.query('show schemas') do |q|
77
+ q.each_row do |r|
78
+ expect(expected_schemas).to include(r[0])
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+
3
+ describe Trino::Client::Client do
4
+ let(:client) { Trino::Client.new({}) }
5
+
6
+ describe 'rehashes' do
7
+ let(:columns) do
8
+ [
9
+ Models::Column.new(name: 'animal', type: 'string'),
10
+ Models::Column.new(name: 'score', type: 'integer'),
11
+ Models::Column.new(name: 'name', type: 'string')
12
+ ]
13
+ end
14
+
15
+ it 'multiple rows' do
16
+ rows = [
17
+ ['dog', 1, 'Lassie'],
18
+ ['horse', 5, 'Mr. Ed'],
19
+ ['t-rex', 37, 'Doug']
20
+ ]
21
+ client.stub(:run).and_return([columns, rows])
22
+
23
+ rehashed = client.run_with_names('fake query')
24
+
25
+ rehashed.length.should == 3
26
+
27
+ rehashed[0]['animal'].should == 'dog'
28
+ rehashed[0]['score'].should == 1
29
+ rehashed[0]['name'].should == 'Lassie'
30
+
31
+ rehashed[0].values[0].should == 'dog'
32
+ rehashed[0].values[1].should == 1
33
+ rehashed[0].values[2].should == 'Lassie'
34
+
35
+ rehashed[1]['animal'].should == 'horse'
36
+ rehashed[1]['score'].should == 5
37
+ rehashed[1]['name'].should == 'Mr. Ed'
38
+
39
+ rehashed[1].values[0].should == 'horse'
40
+ rehashed[1].values[1].should == 5
41
+ rehashed[1].values[2].should == 'Mr. Ed'
42
+ end
43
+
44
+ it 'empty results' do
45
+ rows = []
46
+ client.stub(:run).and_return([columns, rows])
47
+
48
+ rehashed = client.run_with_names('fake query')
49
+
50
+ rehashed.length.should == 0
51
+ end
52
+
53
+ it 'handles too few result columns' do
54
+ rows = [['wrong', 'count']]
55
+ client.stub(:run).and_return([columns, rows])
56
+
57
+ client.run_with_names('fake query').should == [{
58
+ "animal" => "wrong",
59
+ "score" => "count",
60
+ "name" => nil,
61
+ }]
62
+ end
63
+
64
+ it 'handles too many result columns' do
65
+ rows = [['wrong', 'count', 'too', 'much', 'columns']]
66
+ client.stub(:run).and_return([columns, rows])
67
+
68
+ client.run_with_names('fake query').should == [{
69
+ "animal" => "wrong",
70
+ "score" => "count",
71
+ "name" => 'too',
72
+ }]
73
+ end
74
+ end
75
+ end