Skip to content

Commit 343fbe4

Browse files
authored
Add DDL/DML support to QueryJob and Data (#2530)
* Add #num_dml_affected_rows to QueryJob * Update QueryJob#data to conditionally return empty Data * Add DDL/DML attrs to Data * Add DDL/DML examples to #query_job and #query [closes #2141]
1 parent 1de409d commit 343fbe4

13 files changed

Lines changed: 573 additions & 129 deletions

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Copyright 2018 Google LLC
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+
# https://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+
require "bigquery_helper"
16+
17+
describe Google::Cloud::Bigquery::Dataset, :ddl_dml, :bigquery do
18+
let(:dataset_id) { "#{prefix}_dataset" }
19+
let(:dataset) do
20+
d = bigquery.dataset dataset_id
21+
if d.nil?
22+
d = bigquery.create_dataset dataset_id
23+
end
24+
d
25+
end
26+
let(:table_id) { "dataset_ddl_table_#{SecureRandom.hex(16)}" }
27+
let(:table_id_2) { "dataset_ddl_table_#{SecureRandom.hex(16)}" }
28+
29+
it "creates and populates and drops a table with ddl/dml query jobs" do
30+
create_job = dataset.query_job "CREATE TABLE #{table_id} (x INT64)"
31+
create_job.wait_until_done!
32+
create_job.wont_be :failed?
33+
34+
create_job.statement_type.must_equal "CREATE_TABLE"
35+
create_job.ddl_operation_performed.must_equal "CREATE"
36+
assert_table_ref create_job.ddl_target_table, table_id
37+
create_job.num_dml_affected_rows.must_be :nil?
38+
39+
insert_job = dataset.query_job "INSERT #{table_id} (x) VALUES(101),(102)"
40+
insert_job.wait_until_done!
41+
insert_job.wont_be :failed?
42+
insert_job.statement_type.must_equal "INSERT"
43+
insert_job.num_dml_affected_rows.must_equal 2
44+
45+
update_job = dataset.query_job "UPDATE #{table_id} SET x = x + 1 WHERE x IS NOT NULL"
46+
update_job.wait_until_done!
47+
update_job.wont_be :failed?
48+
update_job.statement_type.must_equal "UPDATE"
49+
update_job.num_dml_affected_rows.must_equal 2
50+
51+
delete_job = dataset.query_job "DELETE #{table_id} WHERE x = 103"
52+
delete_job.wait_until_done!
53+
delete_job.wont_be :failed?
54+
delete_job.statement_type.must_equal "DELETE"
55+
delete_job.num_dml_affected_rows.must_equal 1
56+
57+
drop_job = dataset.query_job "DROP TABLE #{table_id}"
58+
drop_job.wait_until_done!
59+
drop_job.wont_be :failed?
60+
drop_job.statement_type.must_equal "DROP_TABLE"
61+
drop_job.ddl_operation_performed.must_equal "DROP"
62+
assert_table_ref drop_job.ddl_target_table, table_id, exists: false
63+
drop_job.num_dml_affected_rows.must_be :nil?
64+
end
65+
66+
it "creates and populates and drops a table with ddl/dml queries" do
67+
create_data = dataset.query "CREATE TABLE #{table_id_2} (x INT64)"
68+
assert_table_ref create_data.ddl_target_table, table_id_2
69+
create_data.statement_type.must_equal "CREATE_TABLE"
70+
create_data.ddl?.must_equal true
71+
create_data.dml?.must_equal false
72+
create_data.ddl_operation_performed.must_equal "CREATE"
73+
create_data.num_dml_affected_rows.must_be :nil?
74+
create_data.total.must_be :nil?
75+
create_data.next?.must_equal false
76+
create_data.next.must_be :nil?
77+
create_data.all.must_be_kind_of Enumerator
78+
create_data.count.must_equal 0
79+
create_data.to_a.must_equal []
80+
81+
insert_data = dataset.query "INSERT #{table_id_2} (x) VALUES(101),(102)"
82+
insert_data.ddl_target_table.must_be :nil?
83+
insert_data.statement_type.must_equal "INSERT"
84+
insert_data.ddl?.must_equal false
85+
insert_data.dml?.must_equal true
86+
insert_data.ddl_operation_performed.must_be :nil?
87+
insert_data.num_dml_affected_rows.must_equal 2
88+
insert_data.total.must_be :nil?
89+
insert_data.next?.must_equal false
90+
insert_data.next.must_be :nil?
91+
insert_data.all.must_be_kind_of Enumerator
92+
insert_data.count.must_equal 0
93+
insert_data.to_a.must_equal []
94+
95+
update_data = dataset.query "UPDATE #{table_id_2} SET x = x + 1 WHERE x IS NOT NULL"
96+
update_data.statement_type.must_equal "UPDATE"
97+
update_data.num_dml_affected_rows.must_equal 2
98+
99+
delete_data = dataset.query "DELETE #{table_id_2} WHERE x = 103"
100+
delete_data.statement_type.must_equal "DELETE"
101+
delete_data.num_dml_affected_rows.must_equal 1
102+
103+
drop_data = dataset.query "DROP TABLE #{table_id_2}"
104+
drop_data.statement_type.must_equal "DROP_TABLE"
105+
drop_data.ddl_operation_performed.must_equal "DROP"
106+
drop_data.num_dml_affected_rows.must_be :nil?
107+
assert_table_ref drop_data.ddl_target_table, table_id_2, exists: false
108+
end
109+
110+
def assert_table_ref table_ref, table_id, exists: true
111+
table_ref.must_be_kind_of Google::Cloud::Bigquery::Table
112+
table_ref.project_id.must_equal bigquery.project
113+
table_ref.dataset_id.must_equal dataset_id
114+
table_ref.table_id.must_equal table_id
115+
table_ref.reference?.must_equal true
116+
table_ref.exists?.must_equal exists
117+
end
118+
end

google-cloud-bigquery/acceptance/bigquery/dataset_ddl_test.rb

Lines changed: 0 additions & 56 deletions
This file was deleted.

google-cloud-bigquery/lib/google/cloud/bigquery/data.rb

Lines changed: 136 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ class Data < DelegateClass(::Array)
5959
# @private The Google API Client object in JSON Hash.
6060
attr_accessor :gapi_json
6161

62+
##
63+
# @private The query Job gapi object, or nil if from Table#data.
64+
attr_accessor :job_gapi
65+
6266
# @private
6367
def initialize arr = []
6468
@service = nil
@@ -195,6 +199,130 @@ def headers
195199
schema.headers
196200
end
197201

202+
##
203+
# The type of query statement, if valid. Possible values (new values
204+
# might be added in the future):
205+
#
206+
# * "CREATE_MODEL": DDL statement, see [Using Data Definition Language
207+
# Statements](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language)
208+
# * "CREATE_TABLE": DDL statement, see [Using Data Definition Language
209+
# Statements](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language)
210+
# * "CREATE_TABLE_AS_SELECT": DDL statement, see [Using Data Definition
211+
# Language Statements](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language)
212+
# * "CREATE_VIEW": DDL statement, see [Using Data Definition Language
213+
# Statements](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language)
214+
# * "DELETE": DML statement, see [Data Manipulation Language Syntax](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax)
215+
# * "DROP_MODEL": DDL statement, see [Using Data Definition Language
216+
# Statements](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language)
217+
# * "DROP_TABLE": DDL statement, see [Using Data Definition Language
218+
# Statements](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language)
219+
# * "DROP_VIEW": DDL statement, see [Using Data Definition Language
220+
# Statements](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language)
221+
# * "INSERT": DML statement, see [Data Manipulation Language Syntax](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax)
222+
# * "MERGE": DML statement, see [Data Manipulation Language Syntax](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax)
223+
# * "SELECT": SQL query, see [Standard SQL Query Syntax](https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax)
224+
# * "UPDATE": DML statement, see [Data Manipulation Language Syntax](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax)
225+
#
226+
# @return [String, nil] The type of query statement.
227+
#
228+
def statement_type
229+
return nil unless job_gapi && job_gapi.statistics.query
230+
job_gapi.statistics.query.statement_type
231+
end
232+
233+
##
234+
# Whether the query that created this data was a DDL statement.
235+
#
236+
# @see https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language
237+
# Using Data Definition Language Statements
238+
#
239+
# @return [Boolean]
240+
#
241+
# @example
242+
# require "google/cloud/bigquery"
243+
#
244+
# bigquery = Google::Cloud::Bigquery.new
245+
# data = bigquery.query "CREATE TABLE my_table (x INT64)"
246+
#
247+
# data.statement_type #=> "CREATE_TABLE"
248+
# data.ddl? #=> true
249+
#
250+
def ddl?
251+
%w[CREATE_MODEL CREATE_TABLE CREATE_TABLE_AS_SELECT CREATE_VIEW \
252+
DROP_MODEL DROP_TABLE DROP_VIEW].include? statement_type
253+
end
254+
255+
##
256+
# Whether the query that created this data was a DML statement.
257+
#
258+
# @see https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax
259+
# Data Manipulation Language Syntax
260+
#
261+
# @return [Boolean]
262+
#
263+
# @example
264+
# require "google/cloud/bigquery"
265+
#
266+
# bigquery = Google::Cloud::Bigquery.new
267+
# data = bigquery.query "UPDATE my_table " \
268+
# "SET x = x + 1 " \
269+
# "WHERE x IS NOT NULL"
270+
#
271+
# data.statement_type #=> "UPDATE"
272+
# data.dml? #=> true
273+
#
274+
def dml?
275+
%w[INSERT UPDATE MERGE DELETE].include? statement_type
276+
end
277+
278+
##
279+
# The DDL operation performed, possibly dependent on the pre-existence
280+
# of the DDL target. (See {#ddl_target_table}.) Possible values (new
281+
# values might be added in the future):
282+
#
283+
# * "CREATE": The query created the DDL target.
284+
# * "SKIP": No-op. Example cases: the query is
285+
# `CREATE TABLE IF NOT EXISTS` while the table already exists, or the
286+
# query is `DROP TABLE IF EXISTS` while the table does not exist.
287+
# * "REPLACE": The query replaced the DDL target. Example case: the
288+
# query is `CREATE OR REPLACE TABLE`, and the table already exists.
289+
# * "DROP": The query deleted the DDL target.
290+
#
291+
# @return [String, nil] The DDL operation performed.
292+
#
293+
def ddl_operation_performed
294+
return nil unless job_gapi && job_gapi.statistics.query
295+
job_gapi.statistics.query.ddl_operation_performed
296+
end
297+
298+
##
299+
# The DDL target table, in reference state. (See {Table#reference?}.)
300+
# Present only for `CREATE/DROP TABLE/VIEW` queries. (See
301+
# {#statement_type}.)
302+
#
303+
# @return [Google::Cloud::Bigquery::Table, nil] The DDL target table, in
304+
# reference state.
305+
#
306+
def ddl_target_table
307+
return nil unless job_gapi && job_gapi.statistics.query
308+
ensure_service!
309+
table = job_gapi.statistics.query.ddl_target_table
310+
return nil unless table
311+
Google::Cloud::Bigquery::Table.new_reference_from_gapi table, service
312+
end
313+
314+
##
315+
# The number of rows affected by a DML statement. Present only for DML
316+
# statements `INSERT`, `UPDATE` or `DELETE`. (See {#statement_type}.)
317+
#
318+
# @return [Integer, nil] The number of rows affected by a DML statement,
319+
# or `nil` if the query is not a DML statement.
320+
#
321+
def num_dml_affected_rows
322+
return nil unless job_gapi && job_gapi.statistics.query
323+
job_gapi.statistics.query.num_dml_affected_rows
324+
end
325+
198326
##
199327
# Whether there is a next page of data.
200328
#
@@ -252,7 +380,7 @@ def next
252380
@table_gapi.table_reference.dataset_id,
253381
@table_gapi.table_reference.table_id,
254382
token: token
255-
self.class.from_gapi_json data_json, @table_gapi, @service
383+
self.class.from_gapi_json data_json, @table_gapi, job_gapi, @service
256384
end
257385

258386
##
@@ -327,13 +455,16 @@ def all request_limit: nil
327455

328456
##
329457
# @private New Data from a response object.
330-
def self.from_gapi_json gapi_json, table_gapi, service
331-
formatted_rows = Convert.format_rows(gapi_json[:rows],
332-
table_gapi.schema.fields)
458+
def self.from_gapi_json gapi_json, table_gapi, job_gapi, service
459+
rows = gapi_json[:rows] || []
460+
unless rows.empty?
461+
rows = Convert.format_rows rows, table_gapi.schema.fields
462+
end
333463

334-
data = new formatted_rows
464+
data = new rows
335465
data.table_gapi = table_gapi
336466
data.gapi_json = gapi_json
467+
data.job_gapi = job_gapi
337468
data.service = service
338469
data
339470
end

0 commit comments

Comments
 (0)