google-cloud-bigquery 0.20.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.
- checksums.yaml +7 -0
- data/lib/google-cloud-bigquery.rb +122 -0
- data/lib/google/cloud/bigquery.rb +353 -0
- data/lib/google/cloud/bigquery/copy_job.rb +99 -0
- data/lib/google/cloud/bigquery/credentials.rb +31 -0
- data/lib/google/cloud/bigquery/data.rb +244 -0
- data/lib/google/cloud/bigquery/dataset.rb +758 -0
- data/lib/google/cloud/bigquery/dataset/access.rb +509 -0
- data/lib/google/cloud/bigquery/dataset/list.rb +171 -0
- data/lib/google/cloud/bigquery/extract_job.rb +120 -0
- data/lib/google/cloud/bigquery/insert_response.rb +83 -0
- data/lib/google/cloud/bigquery/job.rb +301 -0
- data/lib/google/cloud/bigquery/job/list.rb +174 -0
- data/lib/google/cloud/bigquery/load_job.rb +203 -0
- data/lib/google/cloud/bigquery/project.rb +481 -0
- data/lib/google/cloud/bigquery/query_data.rb +238 -0
- data/lib/google/cloud/bigquery/query_job.rb +139 -0
- data/lib/google/cloud/bigquery/schema.rb +361 -0
- data/lib/google/cloud/bigquery/service.rb +502 -0
- data/lib/google/cloud/bigquery/table.rb +1141 -0
- data/lib/google/cloud/bigquery/table/list.rb +182 -0
- data/lib/google/cloud/bigquery/version.rb +22 -0
- data/lib/google/cloud/bigquery/view.rb +478 -0
- metadata +208 -0
@@ -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
|