gcloud 0.2.0 → 0.3.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 +8 -8
- data/AUTHENTICATION.md +3 -3
- data/CHANGELOG.md +12 -0
- data/OVERVIEW.md +30 -0
- data/lib/gcloud.rb +126 -9
- data/lib/gcloud/bigquery.rb +399 -0
- data/lib/gcloud/bigquery/connection.rb +592 -0
- data/lib/gcloud/bigquery/copy_job.rb +98 -0
- data/lib/gcloud/bigquery/credentials.rb +29 -0
- data/lib/gcloud/bigquery/data.rb +134 -0
- data/lib/gcloud/bigquery/dataset.rb +662 -0
- data/lib/gcloud/bigquery/dataset/list.rb +51 -0
- data/lib/gcloud/bigquery/errors.rb +62 -0
- data/lib/gcloud/bigquery/extract_job.rb +117 -0
- data/lib/gcloud/bigquery/insert_response.rb +80 -0
- data/lib/gcloud/bigquery/job.rb +283 -0
- data/lib/gcloud/bigquery/job/list.rb +55 -0
- data/lib/gcloud/bigquery/load_job.rb +199 -0
- data/lib/gcloud/bigquery/project.rb +512 -0
- data/lib/gcloud/bigquery/query_data.rb +135 -0
- data/lib/gcloud/bigquery/query_job.rb +151 -0
- data/lib/gcloud/bigquery/table.rb +827 -0
- data/lib/gcloud/bigquery/table/list.rb +55 -0
- data/lib/gcloud/bigquery/view.rb +419 -0
- data/lib/gcloud/credentials.rb +3 -3
- data/lib/gcloud/datastore.rb +15 -3
- data/lib/gcloud/datastore/credentials.rb +3 -2
- data/lib/gcloud/datastore/dataset.rb +5 -1
- data/lib/gcloud/datastore/transaction.rb +1 -1
- data/lib/gcloud/pubsub.rb +14 -3
- data/lib/gcloud/pubsub/credentials.rb +4 -4
- data/lib/gcloud/pubsub/project.rb +5 -1
- data/lib/gcloud/pubsub/topic.rb +5 -0
- data/lib/gcloud/storage.rb +14 -24
- data/lib/gcloud/storage/bucket.rb +10 -4
- data/lib/gcloud/storage/credentials.rb +3 -2
- data/lib/gcloud/storage/file.rb +8 -1
- data/lib/gcloud/storage/project.rb +5 -1
- data/lib/gcloud/upload.rb +54 -0
- data/lib/gcloud/version.rb +1 -1
- metadata +78 -2
@@ -0,0 +1,135 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2015 Google Inc. All rights reserved.
|
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
|
+
require "gcloud/bigquery/data"
|
17
|
+
|
18
|
+
module Gcloud
|
19
|
+
module Bigquery
|
20
|
+
##
|
21
|
+
# = QueryData
|
22
|
+
#
|
23
|
+
# Represents Data returned from a query a a list of name/value pairs.
|
24
|
+
class QueryData < Data
|
25
|
+
##
|
26
|
+
# The Connection object.
|
27
|
+
attr_accessor :connection #:nodoc:
|
28
|
+
|
29
|
+
def initialize arr = [] #:nodoc:
|
30
|
+
@job = nil
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
# The total number of bytes processed for this query.
|
35
|
+
def total_bytes
|
36
|
+
@gapi["totalBytesProcessed"]
|
37
|
+
end
|
38
|
+
|
39
|
+
# Whether the query has completed or not. When data is present this will
|
40
|
+
# always be +true+. When +false+, +total+ will not be available.
|
41
|
+
def complete?
|
42
|
+
@gapi["jobComplete"]
|
43
|
+
end
|
44
|
+
|
45
|
+
# Whether the query result was fetched from the query cache.
|
46
|
+
def cache_hit?
|
47
|
+
@gapi["cacheHit"]
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# The schema of the data.
|
52
|
+
def schema
|
53
|
+
s = @gapi["schema"]
|
54
|
+
s = s.to_hash if s.respond_to? :to_hash
|
55
|
+
s = {} if s.nil?
|
56
|
+
s
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# The fields of the data.
|
61
|
+
def fields
|
62
|
+
f = schema["fields"]
|
63
|
+
f = f.to_hash if f.respond_to? :to_hash
|
64
|
+
f = [] if f.nil?
|
65
|
+
f
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# The name of the columns in the data.
|
70
|
+
def headers
|
71
|
+
fields.map { |f| f["name"] }
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# Is there a next page of data?
|
76
|
+
def next?
|
77
|
+
!token.nil?
|
78
|
+
end
|
79
|
+
|
80
|
+
def next
|
81
|
+
return nil unless next?
|
82
|
+
ensure_connection!
|
83
|
+
resp = connection.job_query_results job_id, token: token
|
84
|
+
if resp.success?
|
85
|
+
QueryData.from_gapi resp.data, connection
|
86
|
+
else
|
87
|
+
fail ApiError.from_response(resp)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
##
|
92
|
+
# The BigQuery Job that was created to run the query.
|
93
|
+
def job
|
94
|
+
return @job if @job
|
95
|
+
return nil unless job?
|
96
|
+
ensure_connection!
|
97
|
+
resp = connection.get_job job_id
|
98
|
+
if resp.success?
|
99
|
+
@job = Job.from_gapi resp.data, connection
|
100
|
+
else
|
101
|
+
return nil if resp.status == 404
|
102
|
+
fail ApiError.from_response(resp)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
##
|
107
|
+
# New Data from a response object.
|
108
|
+
def self.from_gapi gapi, connection #:nodoc:
|
109
|
+
formatted_rows = format_rows gapi["rows"],
|
110
|
+
gapi["schema"]["fields"]
|
111
|
+
|
112
|
+
data = new formatted_rows
|
113
|
+
data.gapi = gapi
|
114
|
+
data.connection = connection
|
115
|
+
data
|
116
|
+
end
|
117
|
+
|
118
|
+
protected
|
119
|
+
|
120
|
+
##
|
121
|
+
# Raise an error unless an active connection is available.
|
122
|
+
def ensure_connection!
|
123
|
+
fail "Must have active connection" unless connection
|
124
|
+
end
|
125
|
+
|
126
|
+
def job?
|
127
|
+
@gapi["jobReference"] && @gapi["jobReference"]["jobId"]
|
128
|
+
end
|
129
|
+
|
130
|
+
def job_id
|
131
|
+
@gapi["jobReference"]["jobId"]
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2015 Google Inc. All rights reserved.
|
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 Gcloud
|
17
|
+
module Bigquery
|
18
|
+
##
|
19
|
+
# = QueryJob
|
20
|
+
#
|
21
|
+
# A Job subclass representing a query operation that may be performed
|
22
|
+
# on a Table. A QueryJob instance is created when you call
|
23
|
+
# Project#query_job, Dataset#query_job, or View#data.
|
24
|
+
#
|
25
|
+
# See {Querying Data}[https://cloud.google.com/bigquery/querying-data]
|
26
|
+
# and the {Jobs API
|
27
|
+
# reference}[https://cloud.google.com/bigquery/docs/reference/v2/jobs]
|
28
|
+
# for details.
|
29
|
+
#
|
30
|
+
class QueryJob < Job
|
31
|
+
##
|
32
|
+
# Checks if the priority for the query is +BATCH+.
|
33
|
+
def batch?
|
34
|
+
val = config["query"]["priority"]
|
35
|
+
val == "BATCH"
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# Checks if the priority for the query is +INTERACTIVE+.
|
40
|
+
def interactive?
|
41
|
+
val = config["query"]["priority"]
|
42
|
+
return true if val.nil?
|
43
|
+
val == "INTERACTIVE"
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# Checks if the the query job allows arbitrarily large results at a slight
|
48
|
+
# cost to performance.
|
49
|
+
def large_results?
|
50
|
+
val = config["query"]["preserveNulls"]
|
51
|
+
return false if val.nil?
|
52
|
+
val
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Checks if the query job looks for an existing result in the query cache.
|
57
|
+
# For more information, see {Query
|
58
|
+
# Caching}[https://cloud.google.com/bigquery/querying-data#querycaching].
|
59
|
+
def cache?
|
60
|
+
val = config["query"]["useQueryCache"]
|
61
|
+
return false if val.nil?
|
62
|
+
val
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# Checks if the query job flattens nested and repeated fields in the query
|
67
|
+
# results. The default is +true+. If the value is +false+, #large_results?
|
68
|
+
# should return +true+.
|
69
|
+
def flatten?
|
70
|
+
val = config["query"]["flattenResults"]
|
71
|
+
return true if val.nil?
|
72
|
+
val
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Checks if the query results are from the query cache.
|
77
|
+
def cache_hit?
|
78
|
+
stats["query"]["cacheHit"]
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# The number of bytes processed by the query.
|
83
|
+
def bytes_processed
|
84
|
+
stats["query"]["totalBytesProcessed"]
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# The table in which the query results are stored.
|
89
|
+
def destination
|
90
|
+
table = config["query"]["destinationTable"]
|
91
|
+
return nil unless table
|
92
|
+
retrieve_table table["projectId"],
|
93
|
+
table["datasetId"],
|
94
|
+
table["tableId"]
|
95
|
+
end
|
96
|
+
|
97
|
+
##
|
98
|
+
# Retrieves the query results for the job.
|
99
|
+
#
|
100
|
+
# === Parameters
|
101
|
+
#
|
102
|
+
# +options+::
|
103
|
+
# An optional Hash for controlling additional behavior. (+Hash+)
|
104
|
+
# <code>options[:token]</code>::
|
105
|
+
# Page token, returned by a previous call, identifying the result set.
|
106
|
+
# (+String+)
|
107
|
+
# <code>options[:max]</code>::
|
108
|
+
# Maximum number of results to return. (+Integer+)
|
109
|
+
# <code>options[:start]</code>::
|
110
|
+
# Zero-based index of the starting row to read. (+Integer+)
|
111
|
+
# <code>options[:timeout]</code>::
|
112
|
+
# How long to wait for the query to complete, in milliseconds, before
|
113
|
+
# returning. Default is 10,000 milliseconds (10 seconds). (+Integer+)
|
114
|
+
#
|
115
|
+
# === Returns
|
116
|
+
#
|
117
|
+
# Gcloud::Bigquery::QueryData
|
118
|
+
#
|
119
|
+
# === Example
|
120
|
+
#
|
121
|
+
# require "gcloud"
|
122
|
+
#
|
123
|
+
# gcloud = Gcloud.new
|
124
|
+
# bigquery = gcloud.bigquery
|
125
|
+
#
|
126
|
+
# q = "SELECT word FROM publicdata:samples.shakespeare"
|
127
|
+
# job = bigquery.query_job q
|
128
|
+
#
|
129
|
+
# loop do
|
130
|
+
# break if job.done?
|
131
|
+
# sleep 1
|
132
|
+
# job.refresh!
|
133
|
+
# end
|
134
|
+
# data = job.query_results
|
135
|
+
# data.each do |row|
|
136
|
+
# puts row["word"]
|
137
|
+
# end
|
138
|
+
# data = data.next if data.next?
|
139
|
+
#
|
140
|
+
def query_results options = {}
|
141
|
+
ensure_connection!
|
142
|
+
resp = connection.job_query_results job_id, options
|
143
|
+
if resp.success?
|
144
|
+
QueryData.from_gapi resp.data, connection
|
145
|
+
else
|
146
|
+
fail ApiError.from_response(resp)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,827 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2015 Google Inc. All rights reserved.
|
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
|
+
require "gcloud/bigquery/view"
|
17
|
+
require "gcloud/bigquery/data"
|
18
|
+
require "gcloud/bigquery/table/list"
|
19
|
+
require "gcloud/bigquery/errors"
|
20
|
+
require "gcloud/bigquery/insert_response"
|
21
|
+
require "gcloud/upload"
|
22
|
+
|
23
|
+
module Gcloud
|
24
|
+
module Bigquery
|
25
|
+
##
|
26
|
+
# = Table
|
27
|
+
#
|
28
|
+
# A named resource representing a BigQuery table that holds zero or more
|
29
|
+
# records. Every table is defined by a schema
|
30
|
+
# that may contain nested and repeated fields. (For more information
|
31
|
+
# about nested and repeated fields, see {Preparing Data for
|
32
|
+
# BigQuery}[https://cloud.google.com/bigquery/preparing-data-for-bigquery].)
|
33
|
+
#
|
34
|
+
# require "gcloud"
|
35
|
+
#
|
36
|
+
# gcloud = Gcloud.new
|
37
|
+
# bigquery = gcloud.bigquery
|
38
|
+
# dataset = bigquery.dataset "my_dataset"
|
39
|
+
# table = dataset.create_table "my_table"
|
40
|
+
#
|
41
|
+
# schema = {
|
42
|
+
# "fields" => [
|
43
|
+
# {
|
44
|
+
# "name" => "first_name",
|
45
|
+
# "type" => "STRING",
|
46
|
+
# "mode" => "REQUIRED"
|
47
|
+
# },
|
48
|
+
# {
|
49
|
+
# "name" => "cities_lived",
|
50
|
+
# "type" => "RECORD",
|
51
|
+
# "mode" => "REPEATED",
|
52
|
+
# "fields" => [
|
53
|
+
# {
|
54
|
+
# "name" => "place",
|
55
|
+
# "type" => "STRING",
|
56
|
+
# "mode" => "REQUIRED"
|
57
|
+
# },
|
58
|
+
# {
|
59
|
+
# "name" => "number_of_years",
|
60
|
+
# "type" => "INTEGER",
|
61
|
+
# "mode" => "REQUIRED"
|
62
|
+
# }
|
63
|
+
# ]
|
64
|
+
# }
|
65
|
+
# ]
|
66
|
+
# }
|
67
|
+
# table.schema = schema
|
68
|
+
#
|
69
|
+
# row = {
|
70
|
+
# "first_name" => "Alice",
|
71
|
+
# "cities_lived" => [
|
72
|
+
# {
|
73
|
+
# "place": "Seattle",
|
74
|
+
# "number_of_years": 5
|
75
|
+
# },
|
76
|
+
# {
|
77
|
+
# "place": "Stockholm",
|
78
|
+
# "number_of_years": 6
|
79
|
+
# }
|
80
|
+
# ]
|
81
|
+
# }
|
82
|
+
# table.insert row
|
83
|
+
#
|
84
|
+
class Table
|
85
|
+
##
|
86
|
+
# The Connection object.
|
87
|
+
attr_accessor :connection #:nodoc:
|
88
|
+
|
89
|
+
##
|
90
|
+
# The Google API Client object.
|
91
|
+
attr_accessor :gapi #:nodoc:
|
92
|
+
|
93
|
+
##
|
94
|
+
# Create an empty Table object.
|
95
|
+
def initialize #:nodoc:
|
96
|
+
@connection = nil
|
97
|
+
@gapi = {}
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
# A unique ID for this table.
|
102
|
+
# The ID must contain only letters (a-z, A-Z), numbers (0-9),
|
103
|
+
# or underscores (_). The maximum length is 1,024 characters.
|
104
|
+
#
|
105
|
+
# :category: Attributes
|
106
|
+
#
|
107
|
+
def table_id
|
108
|
+
@gapi["tableReference"]["tableId"]
|
109
|
+
end
|
110
|
+
|
111
|
+
##
|
112
|
+
# The ID of the +Dataset+ containing this table.
|
113
|
+
#
|
114
|
+
# :category: Attributes
|
115
|
+
#
|
116
|
+
def dataset_id
|
117
|
+
@gapi["tableReference"]["datasetId"]
|
118
|
+
end
|
119
|
+
|
120
|
+
##
|
121
|
+
# The ID of the +Project+ containing this table.
|
122
|
+
#
|
123
|
+
# :category: Attributes
|
124
|
+
#
|
125
|
+
def project_id
|
126
|
+
@gapi["tableReference"]["projectId"]
|
127
|
+
end
|
128
|
+
|
129
|
+
##
|
130
|
+
# The name of the table.
|
131
|
+
#
|
132
|
+
# :category: Attributes
|
133
|
+
#
|
134
|
+
def name
|
135
|
+
@gapi["friendlyName"]
|
136
|
+
end
|
137
|
+
|
138
|
+
##
|
139
|
+
# Updates the name of the table.
|
140
|
+
#
|
141
|
+
# :category: Attributes
|
142
|
+
#
|
143
|
+
def name= new_name
|
144
|
+
patch_gapi! name: new_name
|
145
|
+
end
|
146
|
+
|
147
|
+
##
|
148
|
+
# A string hash of the dataset.
|
149
|
+
#
|
150
|
+
# :category: Attributes
|
151
|
+
#
|
152
|
+
def etag
|
153
|
+
ensure_full_data!
|
154
|
+
@gapi["etag"]
|
155
|
+
end
|
156
|
+
|
157
|
+
##
|
158
|
+
# A URL that can be used to access the dataset using the REST API.
|
159
|
+
#
|
160
|
+
# :category: Attributes
|
161
|
+
#
|
162
|
+
def url
|
163
|
+
ensure_full_data!
|
164
|
+
@gapi["selfLink"]
|
165
|
+
end
|
166
|
+
|
167
|
+
##
|
168
|
+
# The description of the table.
|
169
|
+
#
|
170
|
+
# :category: Attributes
|
171
|
+
#
|
172
|
+
def description
|
173
|
+
ensure_full_data!
|
174
|
+
@gapi["description"]
|
175
|
+
end
|
176
|
+
|
177
|
+
##
|
178
|
+
# Updates the description of the table.
|
179
|
+
#
|
180
|
+
# :category: Attributes
|
181
|
+
#
|
182
|
+
def description= new_description
|
183
|
+
patch_gapi! description: new_description
|
184
|
+
end
|
185
|
+
|
186
|
+
##
|
187
|
+
# The number of bytes in the table.
|
188
|
+
#
|
189
|
+
# :category: Data
|
190
|
+
#
|
191
|
+
def bytes_count
|
192
|
+
ensure_full_data!
|
193
|
+
@gapi["numBytes"]
|
194
|
+
end
|
195
|
+
|
196
|
+
##
|
197
|
+
# The number of rows in the table.
|
198
|
+
#
|
199
|
+
# :category: Data
|
200
|
+
#
|
201
|
+
def rows_count
|
202
|
+
ensure_full_data!
|
203
|
+
@gapi["numRows"]
|
204
|
+
end
|
205
|
+
|
206
|
+
##
|
207
|
+
# The time when this table was created.
|
208
|
+
#
|
209
|
+
# :category: Attributes
|
210
|
+
#
|
211
|
+
def created_at
|
212
|
+
ensure_full_data!
|
213
|
+
Time.at(@gapi["creationTime"] / 1000.0)
|
214
|
+
end
|
215
|
+
|
216
|
+
##
|
217
|
+
# The time when this table expires.
|
218
|
+
# If not present, the table will persist indefinitely.
|
219
|
+
# Expired tables will be deleted and their storage reclaimed.
|
220
|
+
#
|
221
|
+
# :category: Attributes
|
222
|
+
#
|
223
|
+
def expires_at
|
224
|
+
ensure_full_data!
|
225
|
+
return nil if @gapi["expirationTime"].nil?
|
226
|
+
Time.at(@gapi["expirationTime"] / 1000.0)
|
227
|
+
end
|
228
|
+
|
229
|
+
##
|
230
|
+
# The date when this table was last modified.
|
231
|
+
#
|
232
|
+
# :category: Attributes
|
233
|
+
#
|
234
|
+
def modified_at
|
235
|
+
ensure_full_data!
|
236
|
+
Time.at(@gapi["lastModifiedTime"] / 1000.0)
|
237
|
+
end
|
238
|
+
|
239
|
+
##
|
240
|
+
# Checks if the table's type is "TABLE".
|
241
|
+
#
|
242
|
+
# :category: Attributes
|
243
|
+
#
|
244
|
+
def table?
|
245
|
+
@gapi["type"] == "TABLE"
|
246
|
+
end
|
247
|
+
|
248
|
+
##
|
249
|
+
# Checks if the table's type is "VIEW".
|
250
|
+
#
|
251
|
+
# :category: Attributes
|
252
|
+
#
|
253
|
+
def view?
|
254
|
+
@gapi["type"] == "VIEW"
|
255
|
+
end
|
256
|
+
|
257
|
+
##
|
258
|
+
# The geographic location where the table should reside. Possible
|
259
|
+
# values include EU and US. The default value is US.
|
260
|
+
#
|
261
|
+
# :category: Attributes
|
262
|
+
#
|
263
|
+
def location
|
264
|
+
ensure_full_data!
|
265
|
+
@gapi["location"]
|
266
|
+
end
|
267
|
+
|
268
|
+
##
|
269
|
+
# The schema of the table.
|
270
|
+
#
|
271
|
+
# :category: Attributes
|
272
|
+
#
|
273
|
+
def schema
|
274
|
+
ensure_full_data!
|
275
|
+
s = @gapi["schema"]
|
276
|
+
s = s.to_hash if s.respond_to? :to_hash
|
277
|
+
s = {} if s.nil?
|
278
|
+
s
|
279
|
+
end
|
280
|
+
|
281
|
+
##
|
282
|
+
# Updates the schema of the table.
|
283
|
+
#
|
284
|
+
# === Example
|
285
|
+
#
|
286
|
+
# require "gcloud"
|
287
|
+
#
|
288
|
+
# gcloud = Gcloud.new
|
289
|
+
# bigquery = gcloud.bigquery
|
290
|
+
# dataset = bigquery.dataset "my_dataset"
|
291
|
+
# table = dataset.create_table "my_table"
|
292
|
+
#
|
293
|
+
# schema = {
|
294
|
+
# "fields" => [
|
295
|
+
# {
|
296
|
+
# "name" => "first_name",
|
297
|
+
# "type" => "STRING",
|
298
|
+
# "mode" => "REQUIRED"
|
299
|
+
# },
|
300
|
+
# {
|
301
|
+
# "name" => "age",
|
302
|
+
# "type" => "INTEGER",
|
303
|
+
# "mode" => "REQUIRED"
|
304
|
+
# }
|
305
|
+
# ]
|
306
|
+
# }
|
307
|
+
# table.schema = schema
|
308
|
+
#
|
309
|
+
# :category: Attributes
|
310
|
+
#
|
311
|
+
def schema= new_schema
|
312
|
+
patch_gapi! schema: new_schema
|
313
|
+
end
|
314
|
+
|
315
|
+
##
|
316
|
+
# The fields of the table.
|
317
|
+
#
|
318
|
+
# :category: Attributes
|
319
|
+
#
|
320
|
+
def fields
|
321
|
+
f = schema["fields"]
|
322
|
+
f = f.to_hash if f.respond_to? :to_hash
|
323
|
+
f = [] if f.nil?
|
324
|
+
f
|
325
|
+
end
|
326
|
+
|
327
|
+
##
|
328
|
+
# The names of the columns in the table.
|
329
|
+
#
|
330
|
+
# :category: Attributes
|
331
|
+
#
|
332
|
+
def headers
|
333
|
+
fields.map { |f| f["name"] }
|
334
|
+
end
|
335
|
+
|
336
|
+
##
|
337
|
+
# Retrieves data from the table.
|
338
|
+
#
|
339
|
+
# === Parameters
|
340
|
+
#
|
341
|
+
# +options+::
|
342
|
+
# An optional Hash for controlling additional behavior. (+Hash+)
|
343
|
+
# <code>options[:token]</code>::
|
344
|
+
# Page token, returned by a previous call, identifying the result set.
|
345
|
+
# (+String+)
|
346
|
+
# <code>options[:max]</code>::
|
347
|
+
# Maximum number of results to return. (+Integer+)
|
348
|
+
# <code>options[:start]</code>::
|
349
|
+
# Zero-based index of the starting row to read. (+Integer+)
|
350
|
+
#
|
351
|
+
# === Returns
|
352
|
+
#
|
353
|
+
# Gcloud::Bigquery::Data
|
354
|
+
#
|
355
|
+
# === Example
|
356
|
+
#
|
357
|
+
# require "gcloud"
|
358
|
+
#
|
359
|
+
# gcloud = Gcloud.new
|
360
|
+
# bigquery = gcloud.bigquery
|
361
|
+
# dataset = bigquery.dataset "my_dataset"
|
362
|
+
# table = dataset.table "my_table"
|
363
|
+
#
|
364
|
+
# data = table.data
|
365
|
+
# data.each do |row|
|
366
|
+
# puts row["first_name"]
|
367
|
+
# end
|
368
|
+
# more_data = table.data token: data.token
|
369
|
+
#
|
370
|
+
# :category: Data
|
371
|
+
#
|
372
|
+
def data options = {}
|
373
|
+
ensure_connection!
|
374
|
+
resp = connection.list_tabledata dataset_id, table_id, options
|
375
|
+
if resp.success?
|
376
|
+
Data.from_response resp, self
|
377
|
+
else
|
378
|
+
fail ApiError.from_response(resp)
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
##
|
383
|
+
# Copies the data from the table to another table.
|
384
|
+
#
|
385
|
+
# === Parameters
|
386
|
+
#
|
387
|
+
# +destination_table+::
|
388
|
+
# The destination for the copied data. (+Table+)
|
389
|
+
# +options+::
|
390
|
+
# An optional Hash for controlling additional behavior. (+Hash+)
|
391
|
+
# <code>options[:create]</code>::
|
392
|
+
# Specifies whether the job is allowed to create new tables. (+String+)
|
393
|
+
#
|
394
|
+
# The following values are supported:
|
395
|
+
# * +needed+ - Create the table if it does not exist.
|
396
|
+
# * +never+ - The table must already exist. A 'notFound' error is
|
397
|
+
# raised if the table does not exist.
|
398
|
+
# <code>options[:write]</code>::
|
399
|
+
# Specifies how to handle data already present in the destination table.
|
400
|
+
# The default value is +empty+. (+String+)
|
401
|
+
#
|
402
|
+
# The following values are supported:
|
403
|
+
# * +truncate+ - BigQuery overwrites the table data.
|
404
|
+
# * +append+ - BigQuery appends the data to the table.
|
405
|
+
# * +empty+ - An error will be returned if the destination table already
|
406
|
+
# contains data.
|
407
|
+
#
|
408
|
+
# === Returns
|
409
|
+
#
|
410
|
+
# Gcloud::Bigquery::CopyJob
|
411
|
+
#
|
412
|
+
# === Example
|
413
|
+
#
|
414
|
+
# require "gcloud"
|
415
|
+
#
|
416
|
+
# gcloud = Gcloud.new
|
417
|
+
# bigquery = gcloud.bigquery
|
418
|
+
# dataset = bigquery.dataset "my_dataset"
|
419
|
+
# table = dataset.table "my_table"
|
420
|
+
# destination_table = dataset.table "my_destination_table"
|
421
|
+
#
|
422
|
+
# copy_job = table.copy destination_table
|
423
|
+
#
|
424
|
+
# :category: Data
|
425
|
+
#
|
426
|
+
def copy destination_table, options = {}
|
427
|
+
ensure_connection!
|
428
|
+
resp = connection.copy_table gapi, destination_table.gapi, options
|
429
|
+
if resp.success?
|
430
|
+
Job.from_gapi resp.data, connection
|
431
|
+
else
|
432
|
+
fail ApiError.from_response(resp)
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
##
|
437
|
+
# Links the table to a source table identified by a URI.
|
438
|
+
#
|
439
|
+
# === Parameters
|
440
|
+
#
|
441
|
+
# +source_url+::
|
442
|
+
# The URI of source table to link. (+String+)
|
443
|
+
# +options+::
|
444
|
+
# An optional Hash for controlling additional behavior. (+Hash+)
|
445
|
+
# <code>options[:create]</code>::
|
446
|
+
# Specifies whether the job is allowed to create new tables. (+String+)
|
447
|
+
#
|
448
|
+
# The following values are supported:
|
449
|
+
# * +needed+ - Create the table if it does not exist.
|
450
|
+
# * +never+ - The table must already exist. A 'notFound' error is
|
451
|
+
# raised if the table does not exist.
|
452
|
+
# <code>options[:write]</code>::
|
453
|
+
# Specifies how to handle data already present in the table.
|
454
|
+
# The default value is +empty+. (+String+)
|
455
|
+
#
|
456
|
+
# The following values are supported:
|
457
|
+
# * +truncate+ - BigQuery overwrites the table data.
|
458
|
+
# * +append+ - BigQuery appends the data to the table.
|
459
|
+
# * +empty+ - An error will be returned if the table already contains
|
460
|
+
# data.
|
461
|
+
#
|
462
|
+
# === Returns
|
463
|
+
#
|
464
|
+
# Gcloud::Bigquery::Job
|
465
|
+
#
|
466
|
+
# :category: Data
|
467
|
+
#
|
468
|
+
def link source_url, options = {} #:nodoc:
|
469
|
+
ensure_connection!
|
470
|
+
resp = connection.link_table gapi, source_url, options
|
471
|
+
if resp.success?
|
472
|
+
Job.from_gapi resp.data, connection
|
473
|
+
else
|
474
|
+
fail ApiError.from_response(resp)
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
##
|
479
|
+
# Extract the data from the table to a Google Cloud Storage file. For
|
480
|
+
# more information, see {Exporting Data From BigQuery
|
481
|
+
# }[https://cloud.google.com/bigquery/exporting-data-from-bigquery].
|
482
|
+
#
|
483
|
+
# === Parameters
|
484
|
+
#
|
485
|
+
# +extract_url+::
|
486
|
+
# The Google Storage file or file URI pattern(s) to which BigQuery
|
487
|
+
# should extract the table data.
|
488
|
+
# (+Gcloud::Storage::File+ or +String+ or +Array+)
|
489
|
+
# +options+::
|
490
|
+
# An optional Hash for controlling additional behavior. (+Hash+)
|
491
|
+
# <code>options[:format]</code>::
|
492
|
+
# The exported file format. The default value is +csv+. (+String+)
|
493
|
+
#
|
494
|
+
# The following values are supported:
|
495
|
+
# * +csv+ - CSV
|
496
|
+
# * +json+ - {Newline-delimited JSON}[http://jsonlines.org/]
|
497
|
+
# * +avro+ - {Avro}[http://avro.apache.org/]
|
498
|
+
#
|
499
|
+
# === Returns
|
500
|
+
#
|
501
|
+
# Gcloud::Bigquery::ExtractJob
|
502
|
+
#
|
503
|
+
# === Example
|
504
|
+
#
|
505
|
+
# require "gcloud"
|
506
|
+
#
|
507
|
+
# gcloud = Gcloud.new
|
508
|
+
# bigquery = gcloud.bigquery
|
509
|
+
# dataset = bigquery.dataset "my_dataset"
|
510
|
+
# table = dataset.table "my_table"
|
511
|
+
#
|
512
|
+
# extract_job = table.extract "gs://my-bucket/file-name.json",
|
513
|
+
# format: "json"
|
514
|
+
#
|
515
|
+
# :category: Data
|
516
|
+
#
|
517
|
+
def extract extract_url, options = {}
|
518
|
+
ensure_connection!
|
519
|
+
resp = connection.extract_table gapi, extract_url, options
|
520
|
+
if resp.success?
|
521
|
+
Job.from_gapi resp.data, connection
|
522
|
+
else
|
523
|
+
fail ApiError.from_response(resp)
|
524
|
+
end
|
525
|
+
end
|
526
|
+
|
527
|
+
##
|
528
|
+
# Loads data into the table.
|
529
|
+
#
|
530
|
+
# === Parameters
|
531
|
+
#
|
532
|
+
# +file+::
|
533
|
+
# A file or the URI of a Google Cloud Storage file containing
|
534
|
+
# data to load into the table.
|
535
|
+
# (+File+ or +Gcloud::Storage::File+ or +String+)
|
536
|
+
# +options+::
|
537
|
+
# An optional Hash for controlling additional behavior. (+Hash+)
|
538
|
+
# <code>options[:format]</code>::
|
539
|
+
# The exported file format. The default value is +csv+. (+String+)
|
540
|
+
#
|
541
|
+
# The following values are supported:
|
542
|
+
# * +csv+ - CSV
|
543
|
+
# * +json+ - {Newline-delimited JSON}[http://jsonlines.org/]
|
544
|
+
# * +avro+ - {Avro}[http://avro.apache.org/]
|
545
|
+
# <code>options[:create]</code>::
|
546
|
+
# Specifies whether the job is allowed to create new tables. (+String+)
|
547
|
+
#
|
548
|
+
# The following values are supported:
|
549
|
+
# * +needed+ - Create the table if it does not exist.
|
550
|
+
# * +never+ - The table must already exist. A 'notFound' error is
|
551
|
+
# raised if the table does not exist.
|
552
|
+
# <code>options[:write]</code>::
|
553
|
+
# Specifies how to handle data already present in the table.
|
554
|
+
# The default value is +empty+. (+String+)
|
555
|
+
#
|
556
|
+
# The following values are supported:
|
557
|
+
# * +truncate+ - BigQuery overwrites the table data.
|
558
|
+
# * +append+ - BigQuery appends the data to the table.
|
559
|
+
# * +empty+ - An error will be returned if the table already contains
|
560
|
+
# data.
|
561
|
+
#
|
562
|
+
# === Returns
|
563
|
+
#
|
564
|
+
# Gcloud::Bigquery::LoadJob
|
565
|
+
#
|
566
|
+
# === Examples
|
567
|
+
#
|
568
|
+
# require "gcloud"
|
569
|
+
#
|
570
|
+
# gcloud = Gcloud.new
|
571
|
+
# bigquery = gcloud.bigquery
|
572
|
+
# dataset = bigquery.dataset "my_dataset"
|
573
|
+
# table = dataset.table "my_table"
|
574
|
+
#
|
575
|
+
# load_job = table.load "gs://my-bucket/file-name.csv"
|
576
|
+
#
|
577
|
+
# You can also pass a gcloud storage file instance.
|
578
|
+
#
|
579
|
+
# require "gcloud"
|
580
|
+
# require "gcloud/storage"
|
581
|
+
#
|
582
|
+
# gcloud = Gcloud.new
|
583
|
+
# bigquery = gcloud.bigquery
|
584
|
+
# dataset = bigquery.dataset "my_dataset"
|
585
|
+
# table = dataset.table "my_table"
|
586
|
+
#
|
587
|
+
# storage = gcloud.storage
|
588
|
+
# bucket = storage.bucket "my-bucket"
|
589
|
+
# file = bucket.file "file-name.csv"
|
590
|
+
# load_job = table.load file
|
591
|
+
#
|
592
|
+
# Or, you can upload a smaller file directly.
|
593
|
+
# See {Loading Data with a POST Request}[
|
594
|
+
# https://cloud.google.com/bigquery/loading-data-post-request#multipart].
|
595
|
+
#
|
596
|
+
# require "gcloud"
|
597
|
+
#
|
598
|
+
# gcloud = Gcloud.new
|
599
|
+
# bigquery = gcloud.bigquery
|
600
|
+
# dataset = bigquery.dataset "my_dataset"
|
601
|
+
# table = dataset.table "my_table"
|
602
|
+
#
|
603
|
+
# file = File.open "my_data.csv"
|
604
|
+
# load_job = table.load file
|
605
|
+
#
|
606
|
+
# :category: Data
|
607
|
+
#
|
608
|
+
def load file, options = {}
|
609
|
+
ensure_connection!
|
610
|
+
if storage_url? file
|
611
|
+
load_storage file, options
|
612
|
+
elsif local_file? file
|
613
|
+
load_local file, options
|
614
|
+
else
|
615
|
+
fail Gcloud::Bigquery::Error, "Don't know how to load #{file}"
|
616
|
+
end
|
617
|
+
end
|
618
|
+
|
619
|
+
##
|
620
|
+
# Inserts data into the table for near-immediate querying, without the
|
621
|
+
# need to complete a #load operation before the data can appear in query
|
622
|
+
# results. See {Streaming Data Into BigQuery
|
623
|
+
# }[https://cloud.google.com/bigquery/streaming-data-into-bigquery].
|
624
|
+
#
|
625
|
+
# === Parameters
|
626
|
+
#
|
627
|
+
# +rows+::
|
628
|
+
# A hash object or array of hash objects containing the data.
|
629
|
+
# (+Array+ or +Hash+)
|
630
|
+
# +options+::
|
631
|
+
# An optional Hash for controlling additional behavior. (+Hash+)
|
632
|
+
# <code>options[:skip_invalid]</code>::
|
633
|
+
# Insert all valid rows of a request, even if invalid rows exist. The
|
634
|
+
# default value is +false+, which causes the entire request to fail if
|
635
|
+
# any invalid rows exist. (+Boolean+)
|
636
|
+
# <code>options[:ignore_unknown]</code>::
|
637
|
+
# Accept rows that contain values that do not match the schema. The
|
638
|
+
# unknown values are ignored. Default is false, which treats unknown
|
639
|
+
# values as errors. (+Boolean+)
|
640
|
+
#
|
641
|
+
# === Returns
|
642
|
+
#
|
643
|
+
# Gcloud::Bigquery::InsertResponse
|
644
|
+
#
|
645
|
+
# === Example
|
646
|
+
#
|
647
|
+
# require "gcloud"
|
648
|
+
#
|
649
|
+
# gcloud = Gcloud.new
|
650
|
+
# bigquery = gcloud.bigquery
|
651
|
+
# dataset = bigquery.dataset "my_dataset"
|
652
|
+
# table = dataset.table "my_table"
|
653
|
+
#
|
654
|
+
# rows = [
|
655
|
+
# { "first_name" => "Alice", "age" => 21 },
|
656
|
+
# { "first_name" => "Bob", "age" => 22 }
|
657
|
+
# ]
|
658
|
+
# table.insert rows
|
659
|
+
#
|
660
|
+
# :category: Data
|
661
|
+
#
|
662
|
+
def insert rows, options = {}
|
663
|
+
rows = [rows] if rows.is_a? Hash
|
664
|
+
ensure_connection!
|
665
|
+
resp = connection.insert_tabledata dataset_id, table_id, rows, options
|
666
|
+
if resp.success?
|
667
|
+
InsertResponse.from_gapi rows, resp.data
|
668
|
+
else
|
669
|
+
fail ApiError.from_response(resp)
|
670
|
+
end
|
671
|
+
end
|
672
|
+
|
673
|
+
##
|
674
|
+
# Permanently deletes the table.
|
675
|
+
#
|
676
|
+
# === Returns
|
677
|
+
#
|
678
|
+
# +true+ if the table was deleted.
|
679
|
+
#
|
680
|
+
# === Example
|
681
|
+
#
|
682
|
+
# require "gcloud"
|
683
|
+
#
|
684
|
+
# gcloud = Gcloud.new
|
685
|
+
# bigquery = gcloud.bigquery
|
686
|
+
# dataset = bigquery.dataset "my_dataset"
|
687
|
+
# table = dataset.table "my_table"
|
688
|
+
#
|
689
|
+
# table.delete
|
690
|
+
#
|
691
|
+
# :category: Lifecycle
|
692
|
+
#
|
693
|
+
def delete
|
694
|
+
ensure_connection!
|
695
|
+
resp = connection.delete_table dataset_id, table_id
|
696
|
+
if resp.success?
|
697
|
+
true
|
698
|
+
else
|
699
|
+
fail ApiError.from_response(resp)
|
700
|
+
end
|
701
|
+
end
|
702
|
+
|
703
|
+
##
|
704
|
+
# New Table from a Google API Client object.
|
705
|
+
def self.from_gapi gapi, conn #:nodoc:
|
706
|
+
klass = class_for gapi
|
707
|
+
klass.new.tap do |f|
|
708
|
+
f.gapi = gapi
|
709
|
+
f.connection = conn
|
710
|
+
end
|
711
|
+
end
|
712
|
+
|
713
|
+
protected
|
714
|
+
|
715
|
+
##
|
716
|
+
# Raise an error unless an active connection is available.
|
717
|
+
def ensure_connection!
|
718
|
+
fail "Must have active connection" unless connection
|
719
|
+
end
|
720
|
+
|
721
|
+
def patch_gapi! options = {}
|
722
|
+
ensure_connection!
|
723
|
+
resp = connection.patch_table dataset_id, table_id, options
|
724
|
+
if resp.success?
|
725
|
+
@gapi = resp.data
|
726
|
+
else
|
727
|
+
fail ApiError.from_response(resp)
|
728
|
+
end
|
729
|
+
end
|
730
|
+
|
731
|
+
def self.class_for gapi
|
732
|
+
return View if gapi["type"] == "VIEW"
|
733
|
+
self
|
734
|
+
end
|
735
|
+
|
736
|
+
def load_storage file, options = {}
|
737
|
+
# Convert to storage URL
|
738
|
+
file = file.to_gs_url if file.respond_to? :to_gs_url
|
739
|
+
|
740
|
+
resp = connection.load_table gapi, file, options
|
741
|
+
if resp.success?
|
742
|
+
Job.from_gapi resp.data, connection
|
743
|
+
else
|
744
|
+
fail ApiError.from_response(resp)
|
745
|
+
end
|
746
|
+
end
|
747
|
+
|
748
|
+
def load_local file, options = {}
|
749
|
+
if resumable_upload? file
|
750
|
+
load_resumable file, options
|
751
|
+
else
|
752
|
+
load_multipart file, options
|
753
|
+
end
|
754
|
+
end
|
755
|
+
|
756
|
+
def load_resumable file, options = {}
|
757
|
+
chunk_size = verify_chunk_size! options[:chunk_size]
|
758
|
+
resp = connection.load_resumable gapi, file, chunk_size, options
|
759
|
+
if resp.success?
|
760
|
+
Job.from_gapi resp.data, connection
|
761
|
+
else
|
762
|
+
fail ApiError.from_response(resp)
|
763
|
+
end
|
764
|
+
end
|
765
|
+
|
766
|
+
def load_multipart file, options = {}
|
767
|
+
resp = connection.load_multipart gapi, file, options
|
768
|
+
if resp.success?
|
769
|
+
Job.from_gapi resp.data, connection
|
770
|
+
else
|
771
|
+
fail ApiError.from_response(resp)
|
772
|
+
end
|
773
|
+
end
|
774
|
+
|
775
|
+
##
|
776
|
+
# Determines if a resumable upload should be used.
|
777
|
+
def resumable_upload? file #:nodoc:
|
778
|
+
::File.size?(file).to_i > Upload.resumable_threshold
|
779
|
+
end
|
780
|
+
|
781
|
+
def storage_url? file
|
782
|
+
file.respond_to?(:to_gs_url) ||
|
783
|
+
(file.respond_to?(:to_str) &&
|
784
|
+
file.to_str.downcase.start_with?("gs://"))
|
785
|
+
end
|
786
|
+
|
787
|
+
def local_file? file
|
788
|
+
::File.file? file
|
789
|
+
rescue
|
790
|
+
false
|
791
|
+
end
|
792
|
+
|
793
|
+
##
|
794
|
+
# Determines if a chunk_size is valid.
|
795
|
+
def verify_chunk_size! chunk_size
|
796
|
+
chunk_size = chunk_size.to_i
|
797
|
+
chunk_mod = 256 * 1024 # 256KB
|
798
|
+
if (chunk_size.to_i % chunk_mod) != 0
|
799
|
+
chunk_size = (chunk_size / chunk_mod) * chunk_mod
|
800
|
+
end
|
801
|
+
return if chunk_size.zero?
|
802
|
+
chunk_size
|
803
|
+
end
|
804
|
+
|
805
|
+
##
|
806
|
+
# Load the complete representation of the table if it has been
|
807
|
+
# only partially loaded by a request to the API list method.
|
808
|
+
def ensure_full_data!
|
809
|
+
reload_gapi! unless data_complete?
|
810
|
+
end
|
811
|
+
|
812
|
+
def reload_gapi!
|
813
|
+
ensure_connection!
|
814
|
+
resp = connection.get_table dataset_id, table_id
|
815
|
+
if resp.success?
|
816
|
+
@gapi = resp.data
|
817
|
+
else
|
818
|
+
fail ApiError.from_response(resp)
|
819
|
+
end
|
820
|
+
end
|
821
|
+
|
822
|
+
def data_complete?
|
823
|
+
!@gapi["creationTime"].nil?
|
824
|
+
end
|
825
|
+
end
|
826
|
+
end
|
827
|
+
end
|