google-cloud-bigquery 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,238 @@
1
+ # Copyright 2015 Google Inc. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ require "google/cloud/bigquery/service"
17
+ require "google/cloud/bigquery/data"
18
+
19
+ module Google
20
+ module Cloud
21
+ module Bigquery
22
+ ##
23
+ # # QueryData
24
+ #
25
+ # Represents Data returned from a query a a list of name/value pairs.
26
+ class QueryData < Data
27
+ ##
28
+ # @private The Service object.
29
+ attr_accessor :service
30
+
31
+ # @private
32
+ def initialize arr = []
33
+ @job = nil
34
+ super
35
+ end
36
+
37
+ # The total number of bytes processed for this query.
38
+ def total_bytes
39
+ Integer @gapi.total_bytes_processed
40
+ rescue
41
+ nil
42
+ end
43
+
44
+ # Whether the query has completed or not. When data is present this will
45
+ # always be `true`. When `false`, `total` will not be available.
46
+ def complete?
47
+ @gapi.job_complete
48
+ end
49
+
50
+ # Whether the query result was fetched from the query cache.
51
+ def cache_hit?
52
+ @gapi.cache_hit
53
+ end
54
+
55
+ ##
56
+ # The schema of the data.
57
+ def schema
58
+ Schema.from_gapi(@gapi.schema).freeze
59
+ end
60
+
61
+ ##
62
+ # The fields of the data.
63
+ def fields
64
+ f = schema.fields
65
+ f = f.to_hash if f.respond_to? :to_hash
66
+ f = [] if f.nil?
67
+ f
68
+ end
69
+
70
+ ##
71
+ # The name of the columns in the data.
72
+ def headers
73
+ fields.map(&:name)
74
+ end
75
+
76
+ ##
77
+ # Whether there is a next page of query data.
78
+ #
79
+ # @return [Boolean]
80
+ #
81
+ # @example
82
+ # require "google/cloud"
83
+ #
84
+ # gcloud = Google::Cloud.new
85
+ # bigquery = gcloud.bigquery
86
+ # job = bigquery.job "my_job"
87
+ #
88
+ # data = job.query_results
89
+ # if data.next?
90
+ # next_data = data.next
91
+ # end
92
+ #
93
+ def next?
94
+ !token.nil?
95
+ end
96
+
97
+ ##
98
+ # Retrieve the next page of query data.
99
+ #
100
+ # @return [QueryData]
101
+ #
102
+ # @example
103
+ # require "google/cloud"
104
+ #
105
+ # gcloud = Google::Cloud.new
106
+ # bigquery = gcloud.bigquery
107
+ # job = bigquery.job "my_job"
108
+ #
109
+ # data = job.query_results
110
+ # if data.next?
111
+ # next_data = data.next
112
+ # end
113
+ #
114
+ def next
115
+ return nil unless next?
116
+ ensure_service!
117
+ gapi = service.job_query_results job_id, token: token
118
+ QueryData.from_gapi gapi, service
119
+ end
120
+
121
+ ##
122
+ # Retrieves all rows by repeatedly loading {#next} until {#next?}
123
+ # returns `false`. Calls the given block once for each row, which is
124
+ # passed as the parameter.
125
+ #
126
+ # An Enumerator is returned if no block is given.
127
+ #
128
+ # This method may make several API calls until all rows are retrieved.
129
+ # Be sure to use as narrow a search criteria as possible. Please use
130
+ # with caution.
131
+ #
132
+ # @param [Integer] request_limit The upper limit of API requests to make
133
+ # to load all data. Default is no limit.
134
+ # @yield [row] The block for accessing each row of data.
135
+ # @yieldparam [Hash] row The row object.
136
+ #
137
+ # @return [Enumerator]
138
+ #
139
+ # @example Iterating each row by passing a block:
140
+ # require "google/cloud"
141
+ #
142
+ # gcloud = Google::Cloud.new
143
+ # bigquery = gcloud.bigquery
144
+ # job = bigquery.job "my_job"
145
+ #
146
+ # data = job.query_results
147
+ # data.all do |row|
148
+ # puts row["word"]
149
+ # end
150
+ #
151
+ # @example Using the enumerator by not passing a block:
152
+ # require "google/cloud"
153
+ #
154
+ # gcloud = Google::Cloud.new
155
+ # bigquery = gcloud.bigquery
156
+ # job = bigquery.job "my_job"
157
+ #
158
+ # data = job.query_results
159
+ # words = data.all.map do |row|
160
+ # row["word"]
161
+ # end
162
+ #
163
+ # @example Limit the number of API calls made:
164
+ # require "google/cloud"
165
+ #
166
+ # gcloud = Google::Cloud.new
167
+ # bigquery = gcloud.bigquery
168
+ # job = bigquery.job "my_job"
169
+ #
170
+ # data = job.query_results
171
+ # data.all(request_limit: 10) do |row|
172
+ # puts row["word"]
173
+ # end
174
+ #
175
+ def all request_limit: nil
176
+ request_limit = request_limit.to_i if request_limit
177
+ unless block_given?
178
+ return enum_for(:all, request_limit: request_limit)
179
+ end
180
+ results = self
181
+ loop do
182
+ results.each { |r| yield r }
183
+ if request_limit
184
+ request_limit -= 1
185
+ break if request_limit < 0
186
+ end
187
+ break unless results.next?
188
+ results = results.next
189
+ end
190
+ end
191
+
192
+ ##
193
+ # The BigQuery {Job} that was created to run the query.
194
+ def job
195
+ return @job if @job
196
+ return nil unless job?
197
+ ensure_service!
198
+ gapi = service.get_job job_id
199
+ @job = Job.from_gapi gapi, service
200
+ rescue Google::Cloud::NotFoundError
201
+ nil
202
+ end
203
+
204
+ ##
205
+ # @private New Data from a response object.
206
+ def self.from_gapi gapi, service
207
+ if gapi.schema.nil?
208
+ formatted_rows = []
209
+ else
210
+ formatted_rows = format_rows gapi.rows,
211
+ gapi.schema.fields
212
+ end
213
+
214
+ data = new formatted_rows
215
+ data.gapi = gapi
216
+ data.service = service
217
+ data
218
+ end
219
+
220
+ protected
221
+
222
+ ##
223
+ # Raise an error unless an active connection is available.
224
+ def ensure_service!
225
+ fail "Must have active connection" unless service
226
+ end
227
+
228
+ def job?
229
+ @gapi.job_reference && @gapi.job_reference.job_id
230
+ end
231
+
232
+ def job_id
233
+ @gapi.job_reference.job_id
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,139 @@
1
+ # Copyright 2015 Google Inc. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ require "google/cloud/bigquery/service"
17
+
18
+ module Google
19
+ module Cloud
20
+ module Bigquery
21
+ ##
22
+ # # QueryJob
23
+ #
24
+ # A {Job} subclass representing a query operation that may be performed
25
+ # on a {Table}. A QueryJob instance is created when you call
26
+ # {Project#query_job}, {Dataset#query_job}, or {View#data}.
27
+ #
28
+ # @see https://cloud.google.com/bigquery/querying-data Querying Data
29
+ # @see https://cloud.google.com/bigquery/docs/reference/v2/jobs Jobs API
30
+ # reference
31
+ #
32
+ class QueryJob < Job
33
+ ##
34
+ # Checks if the priority for the query is `BATCH`.
35
+ def batch?
36
+ val = @gapi.configuration.query.priority
37
+ val == "BATCH"
38
+ end
39
+
40
+ ##
41
+ # Checks if the priority for the query is `INTERACTIVE`.
42
+ def interactive?
43
+ val = @gapi.configuration.query.priority
44
+ return true if val.nil?
45
+ val == "INTERACTIVE"
46
+ end
47
+
48
+ ##
49
+ # Checks if the the query job allows arbitrarily large results at a
50
+ # slight cost to performance.
51
+ def large_results?
52
+ val = @gapi.configuration.query.allow_large_results
53
+ return false if val.nil?
54
+ val
55
+ end
56
+
57
+ ##
58
+ # Checks if the query job looks for an existing result in the query
59
+ # cache. For more information, see [Query
60
+ # Caching](https://cloud.google.com/bigquery/querying-data#querycaching).
61
+ def cache?
62
+ val = @gapi.configuration.query.use_query_cache
63
+ return false if val.nil?
64
+ val
65
+ end
66
+
67
+ ##
68
+ # Checks if the query job flattens nested and repeated fields in the
69
+ # query results. The default is `true`. If the value is `false`,
70
+ # #large_results? should return `true`.
71
+ def flatten?
72
+ val = @gapi.configuration.query.flatten_results
73
+ return true if val.nil?
74
+ val
75
+ end
76
+
77
+ ##
78
+ # Checks if the query results are from the query cache.
79
+ def cache_hit?
80
+ @gapi.statistics.query.cache_hit
81
+ end
82
+
83
+ ##
84
+ # The number of bytes processed by the query.
85
+ def bytes_processed
86
+ Integer @gapi.statistics.query.total_bytes_processed
87
+ rescue
88
+ nil
89
+ end
90
+
91
+ ##
92
+ # The table in which the query results are stored.
93
+ def destination
94
+ table = @gapi.configuration.query.destination_table
95
+ return nil unless table
96
+ retrieve_table table.project_id,
97
+ table.dataset_id,
98
+ table.table_id
99
+ end
100
+
101
+ ##
102
+ # Retrieves the query results for the job.
103
+ #
104
+ # @param [String] token Page token, returned by a previous call,
105
+ # identifying the result set.
106
+ # @param [Integer] max Maximum number of results to return.
107
+ # @param [Integer] start Zero-based index of the starting row to read.
108
+ # @param [Integer] timeout How long to wait for the query to complete,
109
+ # in milliseconds, before returning. Default is 10,000 milliseconds
110
+ # (10 seconds).
111
+ #
112
+ # @return [Google::Cloud::Bigquery::QueryData]
113
+ #
114
+ # @example
115
+ # require "google/cloud"
116
+ #
117
+ # gcloud = Google::Cloud.new
118
+ # bigquery = gcloud.bigquery
119
+ #
120
+ # q = "SELECT word FROM publicdata:samples.shakespeare"
121
+ # job = bigquery.query_job q
122
+ #
123
+ # job.wait_until_done!
124
+ # data = job.query_results
125
+ # data.each do |row|
126
+ # puts row["word"]
127
+ # end
128
+ # data = data.next if data.next?
129
+ #
130
+ def query_results token: nil, max: nil, start: nil, timeout: nil
131
+ ensure_service!
132
+ options = { token: token, max: max, start: start, timeout: timeout }
133
+ gapi = service.job_query_results job_id, options
134
+ QueryData.from_gapi gapi, service
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,361 @@
1
+ # Copyright 2015 Google Inc. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ module Google
17
+ module Cloud
18
+ module Bigquery
19
+ ##
20
+ # # Table Schema
21
+ #
22
+ # A builder for BigQuery table schemas, passed to block arguments to
23
+ # {Dataset#create_table} and {Table#schema}. Supports nested and
24
+ # repeated fields via a nested block.
25
+ #
26
+ # @see https://cloud.google.com/bigquery/preparing-data-for-bigquery
27
+ # Preparing Data for BigQuery
28
+ #
29
+ # @example
30
+ # require "google/cloud"
31
+ #
32
+ # gcloud = Google::Cloud.new
33
+ # bigquery = gcloud.bigquery
34
+ # dataset = bigquery.dataset "my_dataset"
35
+ # table = dataset.create_table "my_table"
36
+ #
37
+ # table.schema do |schema|
38
+ # schema.string "first_name", mode: :required
39
+ # schema.record "cities_lived", mode: :repeated do |cities_lived|
40
+ # cities_lived.string "place", mode: :required
41
+ # cities_lived.integer "number_of_years", mode: :required
42
+ # end
43
+ # end
44
+ #
45
+ class Schema
46
+ def initialize
47
+ @nested = nil
48
+ end
49
+
50
+ def fields
51
+ @fields ||= @gapi.fields.map { |f| Field.from_gapi f }
52
+ end
53
+
54
+ def fields= new_fields
55
+ @gapi.fields = Array(new_fields).map(&:to_gapi)
56
+ @fields = @gapi.fields.map { |f| Field.from_gapi f }
57
+ end
58
+
59
+ def empty?
60
+ fields.empty?
61
+ end
62
+
63
+ # @private
64
+ def changed?
65
+ return false if frozen?
66
+ check_for_mutated_schema!
67
+ @original_json != @gapi.to_json
68
+ end
69
+
70
+ # @private
71
+ def freeze
72
+ @gapi = @gapi.dup.freeze
73
+ @gapi.fields.freeze
74
+ @fields = @gapi.fields.map { |f| Field.from_gapi(f).freeze }
75
+ @fields.freeze
76
+ super
77
+ end
78
+
79
+ ##
80
+ # @private Make sure any changes are saved.
81
+ def check_for_mutated_schema!
82
+ return if frozen?
83
+ return if @gapi.frozen?
84
+ return if @fields.nil?
85
+ gapi_fields = Array(@fields).map(&:to_gapi)
86
+ @gapi.update! fields: gapi_fields
87
+ end
88
+
89
+ # @private
90
+ def self.from_gapi gapi
91
+ gapi ||= Google::Apis::BigqueryV2::TableSchema.new fields: []
92
+ gapi.fields ||= []
93
+ new.tap do |s|
94
+ s.instance_variable_set :@gapi, gapi
95
+ s.instance_variable_set :@original_json, gapi.to_json
96
+ end
97
+ end
98
+
99
+ # @private
100
+ def to_gapi
101
+ check_for_mutated_schema!
102
+ @gapi
103
+ end
104
+
105
+ # @private
106
+ def == other
107
+ return false unless other.is_a? Schema
108
+ to_gapi.to_h == other.to_gapi.to_h
109
+ end
110
+
111
+ ##
112
+ # Adds a string field to the schema.
113
+ #
114
+ # @param [String] name The field name. The name must contain only
115
+ # letters (a-z, A-Z), numbers (0-9), or underscores (_), and must
116
+ # start with a letter or underscore. The maximum length is 128
117
+ # characters.
118
+ # @param [String] description A description of the field.
119
+ # @param [Symbol] mode The field's mode. The possible values are
120
+ # `:nullable`, `:required`, and `:repeated`. The default value is
121
+ # `:nullable`.
122
+ def string name, description: nil, mode: :nullable
123
+ add_field name, :string, nil, description: description, mode: mode
124
+ end
125
+
126
+ ##
127
+ # Adds an integer field to the schema.
128
+ #
129
+ # @param [String] name The field name. The name must contain only
130
+ # letters (a-z, A-Z), numbers (0-9), or underscores (_), and must
131
+ # start with a letter or underscore. The maximum length is 128
132
+ # characters.
133
+ # @param [String] description A description of the field.
134
+ # @param [Symbol] mode The field's mode. The possible values are
135
+ # `:nullable`, `:required`, and `:repeated`. The default value is
136
+ # `:nullable`.
137
+ def integer name, description: nil, mode: :nullable
138
+ add_field name, :integer, nil, description: description, mode: mode
139
+ end
140
+
141
+ ##
142
+ # Adds a floating-point number field to the schema.
143
+ #
144
+ # @param [String] name The field name. The name must contain only
145
+ # letters (a-z, A-Z), numbers (0-9), or underscores (_), and must
146
+ # start with a letter or underscore. The maximum length is 128
147
+ # characters.
148
+ # @param [String] description A description of the field.
149
+ # @param [Symbol] mode The field's mode. The possible values are
150
+ # `:nullable`, `:required`, and `:repeated`. The default value is
151
+ # `:nullable`.
152
+ def float name, description: nil, mode: :nullable
153
+ add_field name, :float, nil, description: description, mode: mode
154
+ end
155
+
156
+ ##
157
+ # Adds a boolean field to the schema.
158
+ #
159
+ # @param [String] name The field name. The name must contain only
160
+ # letters (a-z, A-Z), numbers (0-9), or underscores (_), and must
161
+ # start with a letter or underscore. The maximum length is 128
162
+ # characters.
163
+ # @param [String] description A description of the field.
164
+ # @param [Symbol] mode The field's mode. The possible values are
165
+ # `:nullable`, `:required`, and `:repeated`. The default value is
166
+ # `:nullable`.
167
+ def boolean name, description: nil, mode: :nullable
168
+ add_field name, :boolean, nil, description: description, mode: mode
169
+ end
170
+
171
+ ##
172
+ # Adds a timestamp field to the schema.
173
+ #
174
+ # @param [String] name The field name. The name must contain only
175
+ # letters (a-z, A-Z), numbers (0-9), or underscores (_), and must
176
+ # start with a letter or underscore. The maximum length is 128
177
+ # characters.
178
+ # @param [String] description A description of the field.
179
+ # @param [Symbol] mode The field's mode. The possible values are
180
+ # `:nullable`, `:required`, and `:repeated`. The default value is
181
+ # `:nullable`.
182
+ def timestamp name, description: nil, mode: :nullable
183
+ add_field name, :timestamp, nil, description: description, mode: mode
184
+ end
185
+
186
+ ##
187
+ # Adds a record field to the schema. A block must be passed describing
188
+ # the nested fields of the record. For more information about nested
189
+ # and repeated records, see [Preparing Data for BigQuery
190
+ # ](https://cloud.google.com/bigquery/preparing-data-for-bigquery).
191
+ #
192
+ # @param [String] name The field name. The name must contain only
193
+ # letters (a-z, A-Z), numbers (0-9), or underscores (_), and must
194
+ # start with a letter or underscore. The maximum length is 128
195
+ # characters.
196
+ # @param [String] description A description of the field.
197
+ # @param [Symbol] mode The field's mode. The possible values are
198
+ # `:nullable`, `:required`, and `:repeated`. The default value is
199
+ # `:nullable`.
200
+ # @yield [nested_schema] a block for setting the nested schema
201
+ # @yieldparam [Schema] nested_schema the object accepting the
202
+ # nested schema
203
+ #
204
+ # @example
205
+ # require "google/cloud"
206
+ #
207
+ # gcloud = Google::Cloud.new
208
+ # bigquery = gcloud.bigquery
209
+ # dataset = bigquery.dataset "my_dataset"
210
+ # table = dataset.create_table "my_table"
211
+ #
212
+ # table.schema do |schema|
213
+ # schema.string "first_name", mode: :required
214
+ # schema.record "cities_lived", mode: :repeated do |cities_lived|
215
+ # cities_lived.string "place", mode: :required
216
+ # cities_lived.integer "number_of_years", mode: :required
217
+ # end
218
+ # end
219
+ #
220
+ def record name, description: nil, mode: nil
221
+ fail ArgumentError, "nested RECORD type is not permitted" if @nested
222
+ fail ArgumentError, "a block is required" unless block_given?
223
+ empty_schema = Google::Apis::BigqueryV2::TableSchema.new fields: []
224
+ nested_schema = self.class.from_gapi(empty_schema).tap do |s|
225
+ s.instance_variable_set :@nested, true
226
+ end
227
+ yield nested_schema
228
+ add_field name, :record, nested_schema.fields,
229
+ description: description, mode: mode
230
+ end
231
+
232
+ protected
233
+
234
+ def add_field name, type, nested_fields, description: nil,
235
+ mode: :nullable
236
+ # Remove any existing field of this name
237
+ fields.reject! { |f| f.name == name }
238
+ fields << Field.new(name, type, description: description,
239
+ mode: mode, fields: nested_fields)
240
+ end
241
+
242
+ class Field
243
+ # @private
244
+ MODES = %w( NULLABLE REQUIRED REPEATED )
245
+
246
+ # @private
247
+ TYPES = %w( STRING INTEGER FLOAT BOOLEAN TIMESTAMP RECORD )
248
+
249
+ def initialize name, type, description: nil,
250
+ mode: :nullable, fields: nil
251
+ @gapi = Google::Apis::BigqueryV2::TableFieldSchema.new
252
+ @gapi.update! name: name
253
+ @gapi.update! type: verify_type(type)
254
+ @gapi.update! description: description if description
255
+ @gapi.update! mode: verify_mode(mode) if mode
256
+ if fields
257
+ @fields = fields
258
+ check_for_changed_fields!
259
+ end
260
+ @original_json = @gapi.to_json
261
+ end
262
+
263
+ def name
264
+ @gapi.name
265
+ end
266
+
267
+ def name= new_name
268
+ @gapi.update! name: new_name
269
+ end
270
+
271
+ def type
272
+ @gapi.type
273
+ end
274
+
275
+ def type= new_type
276
+ @gapi.update! type: verify_type(new_type)
277
+ end
278
+
279
+ def description
280
+ @gapi.description
281
+ end
282
+
283
+ def description= new_description
284
+ @gapi.update! description: new_description
285
+ end
286
+
287
+ def mode
288
+ @gapi.mode
289
+ end
290
+
291
+ def mode= new_mode
292
+ @gapi.update! mode: verify_mode(new_mode)
293
+ end
294
+
295
+ def fields
296
+ @fields ||= Array(@gapi.fields).map { |f| Field.from_gapi f }
297
+ end
298
+
299
+ def fields= new_fields
300
+ @fields = new_fields
301
+ end
302
+
303
+ ##
304
+ # @private Make sure any fields are saved.
305
+ def check_for_changed_fields!
306
+ return if frozen?
307
+ fields.each(&:check_for_changed_fields!)
308
+ gapi_fields = Array(fields).map(&:to_gapi)
309
+ gapi_fields = nil if gapi_fields.empty?
310
+ @gapi.update! fields: gapi_fields
311
+ end
312
+
313
+ # @private
314
+ def changed?
315
+ @original_json == to_gapi.to_json
316
+ end
317
+
318
+ # @private
319
+ def self.from_gapi gapi
320
+ new("to-be-replaced", "STRING").tap do |f|
321
+ f.instance_variable_set :@gapi, gapi
322
+ f.instance_variable_set :@original_json, gapi.to_json
323
+ end
324
+ end
325
+
326
+ # @private
327
+ def to_gapi
328
+ # make sure any changes are saved.
329
+ check_for_changed_fields!
330
+ @gapi
331
+ end
332
+
333
+ # @private
334
+ def == other
335
+ return false unless other.is_a? Field
336
+ to_gapi.to_h == other.to_gapi.to_h
337
+ end
338
+
339
+ protected
340
+
341
+ def verify_type type
342
+ upcase_type = type.to_s.upcase
343
+ unless TYPES.include? upcase_type
344
+ fail ArgumentError,
345
+ "Type '#{upcase_type}' not found in #{TYPES.inspect}"
346
+ end
347
+ upcase_type
348
+ end
349
+
350
+ def verify_mode mode
351
+ upcase_mode = mode.to_s.upcase
352
+ unless MODES.include? upcase_mode
353
+ fail ArgumentError "Unable to determine mode for '#{mode}'"
354
+ end
355
+ upcase_mode
356
+ end
357
+ end
358
+ end
359
+ end
360
+ end
361
+ end