diff --git a/google-cloud-bigquery/acceptance/bigquery/bigquery_test.rb b/google-cloud-bigquery/acceptance/bigquery/bigquery_test.rb index 124bff48d676..37b7385193b4 100644 --- a/google-cloud-bigquery/acceptance/bigquery/bigquery_test.rb +++ b/google-cloud-bigquery/acceptance/bigquery/bigquery_test.rb @@ -146,6 +146,7 @@ # job.statement_type.must_equal "SELECT" job.ddl_operation_performed.must_be :nil? job.ddl_target_table.must_be :nil? + job.ddl_target_routine.must_be :nil? end it "should run a query job with dryrun flag" do diff --git a/google-cloud-bigquery/acceptance/bigquery/model_test.rb b/google-cloud-bigquery/acceptance/bigquery/model_test.rb index 228d0943329b..af558282714b 100644 --- a/google-cloud-bigquery/acceptance/bigquery/model_test.rb +++ b/google-cloud-bigquery/acceptance/bigquery/model_test.rb @@ -25,7 +25,7 @@ end let(:model_id) { "model_#{SecureRandom.hex(4)}" } let :model_sql do - model_sql = <<-MODEL_SQL + model_sql = <<~MODEL_SQL CREATE MODEL #{dataset.dataset_id}.#{model_id} OPTIONS ( model_type='linear_reg', diff --git a/google-cloud-bigquery/acceptance/bigquery/routine_test.rb b/google-cloud-bigquery/acceptance/bigquery/routine_test.rb new file mode 100644 index 000000000000..7ac079baa39b --- /dev/null +++ b/google-cloud-bigquery/acceptance/bigquery/routine_test.rb @@ -0,0 +1,230 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require "bigquery_helper" + +describe Google::Cloud::Bigquery, :bigquery do + let(:dataset_id) { "#{prefix}_dataset" } + let(:dataset) do + d = bigquery.dataset dataset_id + if d.nil? + d = bigquery.create_dataset dataset_id + end + d + end + let(:routine_id) { "routine_#{SecureRandom.hex(4)}" } + let :routine_sql do + routine_sql = <<~SQL + CREATE FUNCTION `#{routine_id}`( + arr ARRAY> + ) AS ( + (SELECT SUM(IF(elem.name = "foo",elem.val,null)) FROM UNNEST(arr) AS elem) + ) + SQL + end + + it "can create from SQL, list, read, update, and delete a routine" do + # create from sql + job = dataset.query_job routine_sql + job.wait_until_done! + job.wont_be :failed? + job.ddl_operation_performed.must_equal "CREATE" + routine = job.ddl_target_routine + routine.must_be_kind_of Google::Cloud::Bigquery::Routine + routine.reference?.must_equal true + routine.project_id.must_equal bigquery.project + routine.dataset_id.must_equal dataset.dataset_id + routine.routine_id.must_equal routine_id + + # list + dataset.routines.all.map(&:routine_id).must_include routine_id + + # list with filter + dataset.routines(filter: "routineType:SCALAR_FUNCTION").all.map(&:routine_id).must_include routine_id + + # list with filter + dataset.routines(filter: "routineType:PROCEDURE").all.map(&:routine_id).wont_include routine_id + + # get + routine = dataset.routine routine_id + routine.must_be_kind_of Google::Cloud::Bigquery::Routine + routine.project_id.must_equal bigquery.project + routine.dataset_id.must_equal dataset.dataset_id + routine.routine_id.must_equal routine_id + + routine.description.must_be :nil? + routine.routine_type.must_equal "SCALAR_FUNCTION" + routine.language.must_equal "SQL" + routine.body.must_equal "(SELECT SUM(IF(elem.name = \"foo\",elem.val,null)) FROM UNNEST(arr) AS elem)" + + arguments = routine.arguments + arguments.must_be_kind_of Array + arguments.size.must_equal 1 + + argument = arguments.first + argument.must_be_kind_of Google::Cloud::Bigquery::Argument + argument.argument_kind.must_be :nil? + argument.fixed_type?.must_equal true + argument.any_type?.must_equal false + argument.mode.must_be :nil? + argument.in?.must_equal false + argument.out?.must_equal false + argument.inout?.must_equal false + argument.name.must_equal "arr" + + data_type = argument.data_type + data_type.must_be_kind_of Google::Cloud::Bigquery::StandardSql::DataType + data_type.type_kind.must_equal "ARRAY" + data_type.struct_type.must_be :nil? + data_type.array_element_type.must_be_kind_of Google::Cloud::Bigquery::StandardSql::DataType + data_type.array_element_type.type_kind.must_equal "STRUCT" + data_type.array_element_type.struct_type.must_be_kind_of Google::Cloud::Bigquery::StandardSql::StructType + + struct_fields = data_type.array_element_type.struct_type.fields + struct_fields.must_be_kind_of Array + struct_fields.size.must_equal 2 + struct_fields[0].must_be_kind_of Google::Cloud::Bigquery::StandardSql::Field + struct_fields[0].name.must_equal "name" + struct_fields[0].type.must_be_kind_of Google::Cloud::Bigquery::StandardSql::DataType + struct_fields[0].type.type_kind.must_equal "STRING" + struct_fields[1].must_be_kind_of Google::Cloud::Bigquery::StandardSql::Field + struct_fields[1].name.must_equal "val" + struct_fields[1].type.must_be_kind_of Google::Cloud::Bigquery::StandardSql::DataType + struct_fields[1].type.type_kind.must_equal "INT64" + + # update + new_description = "Routine was updated #{Time.now}" + routine.description = new_description + routine.refresh! + routine.description.must_equal new_description + + # delete + routine.delete.must_equal true + + dataset.routine(routine_id).must_be_nil + end + + it "can create, update and delete a routine" do + # create + routine = dataset.create_routine routine_id do |r| + r.routine_type = "SCALAR_FUNCTION" + r.language = :SQL + r.arguments = [ + Google::Cloud::Bigquery::Argument.new(name: "x", data_type: "INT64") + ] + r.body = "x * 3" + r.description = "my description" + end + + routine.must_be_kind_of Google::Cloud::Bigquery::Routine + routine.project_id.must_equal bigquery.project + routine.dataset_id.must_equal dataset.dataset_id + routine.routine_id.must_equal routine_id + + routine.description.must_equal "my description" + routine.routine_type.must_equal "SCALAR_FUNCTION" + routine.language.must_equal "SQL" + routine.body.must_equal "x * 3" + + arguments = routine.arguments + arguments.must_be_kind_of Array + arguments.size.must_equal 1 + + argument = arguments.first + argument.must_be_kind_of Google::Cloud::Bigquery::Argument + argument.argument_kind.must_be :nil? + argument.mode.must_be :nil? + argument.name.must_equal "x" + + data_type = argument.data_type + data_type.must_be_kind_of Google::Cloud::Bigquery::StandardSql::DataType + data_type.type_kind.must_equal "INT64" + data_type.array_element_type.must_be :nil? + data_type.struct_type.must_be :nil? + + # update + new_body = "(SELECT SUM(IF(elem.name = \"foo\",elem.val,null)) FROM UNNEST(arr) AS elem)" + new_arguments = [ + Google::Cloud::Bigquery::Argument.new( + name: "arr", + argument_kind: "FIXED_TYPE", + data_type: Google::Cloud::Bigquery::StandardSql::DataType.new( + type_kind: "ARRAY", + array_element_type: Google::Cloud::Bigquery::StandardSql::DataType.new( + type_kind: "STRUCT", + struct_type: Google::Cloud::Bigquery::StandardSql::StructType.new( + fields: [ + Google::Cloud::Bigquery::StandardSql::Field.new( + name: "name", + type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "STRING") + ), + Google::Cloud::Bigquery::StandardSql::Field.new( + name: "val", + type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "INT64") + ) + ] + ) + ) + ) + ) + ] + + routine.update do |r| + r.body = new_body + r.arguments = new_arguments + end + + routine.body.must_equal new_body + + arguments = routine.arguments + arguments.must_be_kind_of Array + arguments.size.must_equal 1 + + argument = arguments.first + argument.must_be_kind_of Google::Cloud::Bigquery::Argument + argument.argument_kind.must_equal "FIXED_TYPE" + argument.mode.must_be :nil? + argument.name.must_equal "arr" + + data_type = argument.data_type + data_type.must_be_kind_of Google::Cloud::Bigquery::StandardSql::DataType + data_type.type_kind.must_equal "ARRAY" + data_type.struct_type.must_be :nil? + data_type.array_element_type.must_be_kind_of Google::Cloud::Bigquery::StandardSql::DataType + data_type.array_element_type.type_kind.must_equal "STRUCT" + data_type.array_element_type.struct_type.must_be_kind_of Google::Cloud::Bigquery::StandardSql::StructType + + struct_fields = data_type.array_element_type.struct_type.fields + struct_fields.must_be_kind_of Array + struct_fields.size.must_equal 2 + struct_fields[0].must_be_kind_of Google::Cloud::Bigquery::StandardSql::Field + struct_fields[0].name.must_equal "name" + struct_fields[0].type.must_be_kind_of Google::Cloud::Bigquery::StandardSql::DataType + struct_fields[0].type.type_kind.must_equal "STRING" + struct_fields[1].must_be_kind_of Google::Cloud::Bigquery::StandardSql::Field + struct_fields[1].name.must_equal "val" + struct_fields[1].type.must_be_kind_of Google::Cloud::Bigquery::StandardSql::DataType + struct_fields[1].type.type_kind.must_equal "INT64" + + # get + routine.reload! + routine.body.must_equal new_body + routine.arguments.first.data_type.array_element_type.struct_type.fields.last.type.type_kind.must_equal "INT64" + + # delete + routine.delete.must_equal true + + dataset.routine(routine_id).must_be_nil + end +end diff --git a/google-cloud-bigquery/acceptance/bigquery/standard_query_test.rb b/google-cloud-bigquery/acceptance/bigquery/standard_query_test.rb index 1ecfc7fd824a..6e4c498124b5 100644 --- a/google-cloud-bigquery/acceptance/bigquery/standard_query_test.rb +++ b/google-cloud-bigquery/acceptance/bigquery/standard_query_test.rb @@ -112,4 +112,20 @@ rows.count.must_equal 1 rows.first[:value].must_equal ["foo", "bar", "baz"] end + + it "queries a struct with no names" do + rows = bigquery.query "SELECT STRUCT(1, 'abc', 3.14) AS value", standard_sql: true + + rows.class.must_equal Google::Cloud::Bigquery::Data + rows.count.must_equal 1 + rows.first[:value].must_equal({ _field_1: 1, _field_2: "abc", _field_3: 3.14 }) + end + + it "queries a struct with duplicate names" do + rows = bigquery.query "SELECT STRUCT(1 AS x, 'abc' AS x, 3.14 AS x) AS value", standard_sql: true + + rows.class.must_equal Google::Cloud::Bigquery::Data + rows.count.must_equal 1 + rows.first[:value].must_equal({ x: 1, _field_2: "abc", _field_3: 3.14 }) + end end diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/argument.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/argument.rb new file mode 100644 index 000000000000..5365934ad4a0 --- /dev/null +++ b/google-cloud-bigquery/lib/google/cloud/bigquery/argument.rb @@ -0,0 +1,197 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "google/cloud/bigquery/standard_sql" + +module Google + module Cloud + module Bigquery + ## + # # Argument + # + # Input/output argument of a function or a stored procedure. See {Routine}. + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.create_routine "my_routine" do |r| + # r.routine_type = "SCALAR_FUNCTION" + # r.language = :SQL + # r.body = "(SELECT SUM(IF(elem.name = \"foo\",elem.val,null)) FROM UNNEST(arr) AS elem)" + # r.arguments = [ + # Google::Cloud::Bigquery::Argument.new( + # name: "arr", + # argument_kind: "FIXED_TYPE", + # data_type: Google::Cloud::Bigquery::StandardSql::DataType.new( + # type_kind: "ARRAY", + # array_element_type: Google::Cloud::Bigquery::StandardSql::DataType.new( + # type_kind: "STRUCT", + # struct_type: Google::Cloud::Bigquery::StandardSql::StructType.new( + # fields: [ + # Google::Cloud::Bigquery::StandardSql::Field.new( + # name: "name", + # type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "STRING") + # ), + # Google::Cloud::Bigquery::StandardSql::Field.new( + # name: "val", + # type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "INT64") + # ) + # ] + # ) + # ) + # ) + # ) + # ] + # end + # + class Argument + ## + # Creates a new, immutable Argument object. + # + # @overload initialize(data_type, kind, mode, name) + # @param [StandardSql::DataType, String] data_type The data type of the argument. Required unless + # {#argument_kind} is `ANY_TYPE`. + # @param [String] argument_kind The kind of argument. Optional. Defaults to `FIXED_TYPE`. + # + # * `FIXED_TYPE` - The argument is a variable with fully specified type, which can be a struct or an array, + # but not a table. + # * `ANY_TYPE` - The argument is any type, including struct or array, but not a table. + # + # To be added: `FIXED_TABLE`, `ANY_TABLE`. + # @param [String] mode Specifies whether the argument is input or output. Optional. Can be set for procedures + # only. + # + # * IN - The argument is input-only. + # * OUT - The argument is output-only. + # * INOUT - The argument is both an input and an output. + # @param [String] name The name of the argument. Optional. Can be absent for a function return argument. + # + def initialize **kwargs + kwargs[:data_type] = StandardSql::DataType.gapi_from_string_or_data_type kwargs[:data_type] + @gapi = Google::Apis::BigqueryV2::Argument.new(**kwargs) + end + + ## + # The data type of the argument. Required unless {#argument_kind} is `ANY_TYPE`. + # + # @return [StandardSql::DataType] The data type. + # + def data_type + StandardSql::DataType.from_gapi @gapi.data_type + end + + ## + # The kind of argument. Optional. Defaults to `FIXED_TYPE`. + # + # * `FIXED_TYPE` - The argument is a variable with fully specified type, which can be a struct or an array, but + # not a table. + # * `ANY_TYPE` - The argument is any type, including struct or array, but not a table. + # + # To be added: `FIXED_TABLE`, `ANY_TABLE`. + # + # @return [String] The upper case kind of argument. + # + def argument_kind + @gapi.argument_kind + end + + ## + # Checks if the value of {#argument_kind} is `FIXED_TYPE`. The default is `true`. + # + # @return [Boolean] `true` when `FIXED_TYPE`, `false` otherwise. + # + def fixed_type? + return true if @gapi.argument_kind.nil? + @gapi.argument_kind == "FIXED_TYPE" + end + + ## + # Checks if the value of {#argument_kind} is `ANY_TYPE`. The default is `false`. + # + # @return [Boolean] `true` when `ANY_TYPE`, `false` otherwise. + # + def any_type? + @gapi.argument_kind == "ANY_TYPE" + end + + ## + # Specifies whether the argument is input or output. Optional. Can be set for procedures only. + # + # * IN - The argument is input-only. + # * OUT - The argument is output-only. + # * INOUT - The argument is both an input and an output. + # + # @return [String] The upper case input/output mode of the argument. + # + def mode + @gapi.mode + end + + ## + # Checks if the value of {#mode} is `IN`. Can be set for procedures only. The default is `false`. + # + # @return [Boolean] `true` when `IN`, `false` otherwise. + # + def in? + @gapi.mode == "IN" + end + + ## + # Checks if the value of {#mode} is `OUT`. Can be set for procedures only. The default is `false`. + # + # @return [Boolean] `true` when `OUT`, `false` otherwise. + # + def out? + @gapi.mode == "OUT" + end + + ## + # Checks if the value of {#mode} is `INOUT`. Can be set for procedures only. The default is `false`. + # + # @return [Boolean] `true` when `INOUT`, `false` otherwise. + # + def inout? + @gapi.mode == "INOUT" + end + + ## + # + # The name of the argument. Optional. Can be absent for a function return argument. + # + # @return [String] The name of the argument. + # + def name + @gapi.name + end + + ## + # @private + def to_gapi + @gapi + end + + ## + # @private New Argument from a Google API Client object. + def self.from_gapi gapi + new.tap do |a| + a.instance_variable_set :@gapi, gapi + end + end + end + end + end +end diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/copy_job.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/copy_job.rb index f7c5f6a9f73f..afcdb3818cc4 100644 --- a/google-cloud-bigquery/lib/google/cloud/bigquery/copy_job.rb +++ b/google-cloud-bigquery/lib/google/cloud/bigquery/copy_job.rb @@ -152,7 +152,7 @@ def initialize gapi # # @return [Google::Cloud::Bigquery::CopyJob::Updater] A job # configuration object for setting copy options. - def self.from_options service, source, target, options = {} + def self.from_options service, source, target, options job_ref = service.job_ref_from options[:job_id], options[:prefix] copy_cfg = Google::Apis::BigqueryV2::JobConfigurationTableCopy.new( source_table: source, @@ -284,6 +284,23 @@ def labels= value @gapi.configuration.update! labels: value end + def cancel + raise "not implemented in #{self.class}" + end + + def rerun! + raise "not implemented in #{self.class}" + end + + def reload! + raise "not implemented in #{self.class}" + end + alias refresh! reload! + + def wait_until_done! + raise "not implemented in #{self.class}" + end + ## # @private Returns the Google API client library version of this job. # diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/data.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/data.rb index 103e4f39aa27..9592b13d0286 100644 --- a/google-cloud-bigquery/lib/google/cloud/bigquery/data.rb +++ b/google-cloud-bigquery/lib/google/cloud/bigquery/data.rb @@ -316,6 +316,21 @@ def ddl_operation_performed job_gapi&.statistics&.query&.ddl_operation_performed end + ## + # The DDL target routine, in reference state. (See {Routine#reference?}.) + # Present only for `CREATE/DROP FUNCTION/PROCEDURE` queries. (See + # {#statement_type}.) + # + # @return [Google::Cloud::Bigquery::Routine, nil] The DDL target routine, in + # reference state. + # + def ddl_target_routine + ensure_service! + routine = job_gapi&.statistics&.query&.ddl_target_routine + return nil if routine.nil? + Google::Cloud::Bigquery::Routine.new_reference_from_gapi routine, service + end + ## # The DDL target table, in reference state. (See {Table#reference?}.) # Present only for `CREATE/DROP TABLE/VIEW` queries. (See diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/dataset.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/dataset.rb index aa7ce1424163..72ed8f3e667c 100644 --- a/google-cloud-bigquery/lib/google/cloud/bigquery/dataset.rb +++ b/google-cloud-bigquery/lib/google/cloud/bigquery/dataset.rb @@ -18,6 +18,7 @@ require "google/cloud/bigquery/service" require "google/cloud/bigquery/table" require "google/cloud/bigquery/model" +require "google/cloud/bigquery/routine" require "google/cloud/bigquery/external" require "google/cloud/bigquery/dataset/list" require "google/cloud/bigquery/dataset/access" @@ -731,8 +732,7 @@ def table table_id, skip_lookup: nil # def tables token: nil, max: nil ensure_service! - options = { token: token, max: max } - gapi = service.list_tables dataset_id, options + gapi = service.list_tables dataset_id, token: token, max: max Table::List.from_gapi gapi, service, dataset_id, max end @@ -817,6 +817,174 @@ def models token: nil, max: nil Model::List.from_gapi gapi, service, dataset_id, max end + ## + # Creates a new routine. The following attributes may be set in the yielded block: + # {Routine::Updater#routine_type=}, {Routine::Updater#language=}, {Routine::Updater#arguments=}, + # {Routine::Updater#return_type=}, {Routine::Updater#imported_libraries=}, {Routine::Updater#body=}, and + # {Routine::Updater#description=}. + # + # @param [String] routine_id The ID of the routine. The ID must contain only + # letters (a-z, A-Z), numbers (0-9), or underscores (_). The maximum length + # is 256 characters. + # @yield [routine] A block for setting properties on the routine. + # @yieldparam [Google::Cloud::Bigquery::Routine::Updater] routine An updater to set additional properties on the + # routine. + # + # @return [Google::Cloud::Bigquery::Routine] A new routine object. + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # + # routine = dataset.create_routine "my_routine" do |r| + # r.routine_type = "SCALAR_FUNCTION" + # r.language = "SQL" + # r.arguments = [ + # Google::Cloud::Bigquery::Argument.new(name: "x", data_type: "INT64") + # ] + # r.body = "x * 3" + # r.description = "My routine description" + # end + # + # puts routine.routine_id + # + # @example Extended example: + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.create_routine "my_routine" do |r| + # r.routine_type = "SCALAR_FUNCTION" + # r.language = :SQL + # r.body = "(SELECT SUM(IF(elem.name = \"foo\",elem.val,null)) FROM UNNEST(arr) AS elem)" + # r.arguments = [ + # Google::Cloud::Bigquery::Argument.new( + # name: "arr", + # argument_kind: "FIXED_TYPE", + # data_type: Google::Cloud::Bigquery::StandardSql::DataType.new( + # type_kind: "ARRAY", + # array_element_type: Google::Cloud::Bigquery::StandardSql::DataType.new( + # type_kind: "STRUCT", + # struct_type: Google::Cloud::Bigquery::StandardSql::StructType.new( + # fields: [ + # Google::Cloud::Bigquery::StandardSql::Field.new( + # name: "name", + # type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "STRING") + # ), + # Google::Cloud::Bigquery::StandardSql::Field.new( + # name: "val", + # type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "INT64") + # ) + # ] + # ) + # ) + # ) + # ) + # ] + # end + # + # @!group Routine + # + def create_routine routine_id + ensure_service! + new_tb = Google::Apis::BigqueryV2::Routine.new( + routine_reference: Google::Apis::BigqueryV2::RoutineReference.new( + project_id: project_id, dataset_id: dataset_id, routine_id: routine_id + ) + ) + updater = Routine::Updater.new new_tb + + yield updater if block_given? + + gapi = service.insert_routine dataset_id, updater.to_gapi + Routine.from_gapi gapi, service + end + + ## + # Retrieves an existing routine by ID. + # + # @param [String] routine_id The ID of a routine. + # @param [Boolean] skip_lookup Optionally create just a local reference + # object without verifying that the resource exists on the BigQuery + # service. Calls made on this object will raise errors if the resource + # does not exist. Default is `false`. Optional. + # + # @return [Google::Cloud::Bigquery::Routine, nil] Returns `nil` if the + # routine does not exist. + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # + # routine = dataset.routine "my_routine" + # puts routine.routine_id + # + # @example Avoid retrieving the routine resource with `skip_lookup`: + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # + # dataset = bigquery.dataset "my_dataset" + # + # routine = dataset.routine "my_routine", skip_lookup: true + # + # @!group Routine + # + def routine routine_id, skip_lookup: nil + ensure_service! + return Routine.new_reference project_id, dataset_id, routine_id, service if skip_lookup + gapi = service.get_routine dataset_id, routine_id + Routine.from_gapi gapi, service + rescue Google::Cloud::NotFoundError + nil + end + + ## + # Retrieves the list of routines belonging to the dataset. + # + # @param [String] token A previously-returned page token representing + # part of the larger set of results to view. + # @param [Integer] max Maximum number of routines to return. + # @param [String] filter If set, then only the routines matching this filter are returned. The current supported + # form is `routineType:`, with a {Routine#routine_type} enum value. Example: `routineType:SCALAR_FUNCTION`. + # + # @return [Array] An array of routines + # (See {Google::Cloud::Bigquery::Routine::List}) + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # + # routines = dataset.routines + # routines.each do |routine| + # puts routine.routine_id + # end + # + # @example Retrieve all routines: (See {Routine::List#all}) + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # + # routines = dataset.routines + # routines.all do |routine| + # puts routine.routine_id + # end + # + # @!group Routine + # + def routines token: nil, max: nil, filter: nil + ensure_service! + gapi = service.list_routines dataset_id, token: token, max: max, filter: filter + Routine::List.from_gapi gapi, service, dataset_id, max, filter: filter + end + ## # Queries data by creating a [query # job](https://cloud.google.com/bigquery/docs/query-overview#query_jobs). @@ -1073,7 +1241,7 @@ def models token: nil, max: nil # # job.wait_until_done! # if !job.failed? - # table_ref = job.ddl_target_table + # table_ref = job.ddl_target_table # Or ddl_target_routine for CREATE/DROP FUNCTION/PROCEDURE # end # # @example Execute a DML statement: @@ -1321,7 +1489,7 @@ def query_job query, params: nil, types: nil, external: nil, priority: "INTERACT # # data = bigquery.query "CREATE TABLE my_table (x INT64)" # - # table_ref = data.ddl_target_table + # table_ref = data.ddl_target_table # Or ddl_target_routine for CREATE/DROP FUNCTION/PROCEDURE # # @example Execute a DML statement: # require "google/cloud/bigquery" @@ -1942,7 +2110,7 @@ def reload! # dataset = bigquery.dataset "my_dataset", skip_lookup: true # dataset.exists? # true # - def exists? force: nil + def exists? force: false return gapi_exists? if force # If we have a memoized value, return it return @exists unless @exists.nil? @@ -2052,7 +2220,7 @@ def self.from_gapi gapi, conn end ## - # @private New lazy Dataset object without making an HTTP request. + # @private New lazy Dataset object without making an HTTP request, for use with the skip_lookup option. def self.new_reference project_id, dataset_id, service raise ArgumentError, "dataset_id is required" unless dataset_id new.tap do |b| @@ -2254,10 +2422,9 @@ def insert_data table_id, rows, skip_invalid: nil, ignore_unknown: nil, insert_i rows = [rows] if rows.is_a? Hash raise ArgumentError, "No rows provided" if rows.empty? ensure_service! - options = { skip_invalid: skip_invalid, - ignore_unknown: ignore_unknown, - insert_ids: insert_ids } - gapi = service.insert_tabledata dataset_id, table_id, rows, options + gapi = service.insert_tabledata dataset_id, table_id, rows, skip_invalid: skip_invalid, + ignore_unknown: ignore_unknown, + insert_ids: insert_ids InsertResponse.from_gapi rows, gapi end @@ -2454,14 +2621,14 @@ def udfs_gapi array_or_str end ## - # Yielded to a block to accumulate changes for a patch request. + # Yielded to a block to accumulate changes for a create request. See {Project#create_dataset}. class Updater < Dataset ## - # A list of attributes that were updated. + # @private A list of attributes that were updated. attr_reader :updates ## - # Create an Updater object. + # @private Create an Updater object. def initialize gapi @updates = [] @gapi = gapi @@ -2478,8 +2645,109 @@ def access @access end + # rubocop:disable Style/MethodDefParentheses + + ## + # @raise [RuntimeError] not implemented + def delete(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def create_table(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def create_view(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def table(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def tables(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def model(*) + raise "not implemented in #{self.class}" + end + ## - # Make sure any access changes are saved + # @raise [RuntimeError] not implemented + def models(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def create_routine(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def routine(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def routines(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def query_job(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def query(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def external(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def load_job(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def load(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def reload! + raise "not implemented in #{self.class}" + end + alias refresh! reload! + + # rubocop:enable Style/MethodDefParentheses + + ## + # @private Make sure any access changes are saved def check_for_mutated_access! return if @access.nil? return unless @access.changed? @@ -2487,6 +2755,8 @@ def check_for_mutated_access! patch_gapi! :access end + ## + # @private def to_gapi check_for_mutated_access! @gapi diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/dataset/list.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/dataset/list.rb index 72a278f1261c..ad9e1d410365 100644 --- a/google-cloud-bigquery/lib/google/cloud/bigquery/dataset/list.rb +++ b/google-cloud-bigquery/lib/google/cloud/bigquery/dataset/list.rb @@ -71,8 +71,7 @@ def next? def next return nil unless next? ensure_service! - options = { all: @hidden, filter: @filter, token: token, max: @max } - gapi = @service.list_datasets options + gapi = @service.list_datasets all: @hidden, filter: @filter, token: token, max: @max self.class.from_gapi gapi, @service, @hidden, @filter, @max end diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/extract_job.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/extract_job.rb index b3f56461cf1f..dbcd199e7749 100644 --- a/google-cloud-bigquery/lib/google/cloud/bigquery/extract_job.rb +++ b/google-cloud-bigquery/lib/google/cloud/bigquery/extract_job.rb @@ -182,7 +182,7 @@ def initialize gapi # # @return [Google::Cloud::Bigquery::ExtractJob::Updater] A job # configuration object for setting query options. - def self.from_options service, table, storage_files, options = {} + def self.from_options service, table, storage_files, options job_ref = service.job_ref_from options[:job_id], options[:prefix] storage_urls = Array(storage_files).map do |url| url.respond_to?(:to_gs_url) ? url.to_gs_url : url @@ -207,7 +207,7 @@ def self.from_options service, table, storage_files, options = {} # # @return [Google::Cloud::Bigquery::ExtractJob::Updater] A job # configuration object for setting query options. - def self.from_job_and_options request, options = {} + def self.from_job_and_options request, options updater = ExtractJob::Updater.new request updater.compression = options[:compression] updater.delimiter = options[:delimiter] @@ -336,6 +336,23 @@ def use_avro_logical_types= value @gapi.configuration.extract.use_avro_logical_types = value end + def cancel + raise "not implemented in #{self.class}" + end + + def rerun! + raise "not implemented in #{self.class}" + end + + def reload! + raise "not implemented in #{self.class}" + end + alias refresh! reload! + + def wait_until_done! + raise "not implemented in #{self.class}" + end + ## # @private Returns the Google API client library version of this job. # diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/job/list.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/job/list.rb index 8ca064f6f80b..48bc6c645f87 100644 --- a/google-cloud-bigquery/lib/google/cloud/bigquery/job/list.rb +++ b/google-cloud-bigquery/lib/google/cloud/bigquery/job/list.rb @@ -71,9 +71,9 @@ def next? def next return nil unless next? ensure_service! - next_options = @options.merge token: token - next_gapi = @service.list_jobs next_options - self.class.from_gapi next_gapi, @service, next_options + next_kwargs = @kwargs.merge token: token + next_gapi = @service.list_jobs next_kwargs + self.class.from_gapi next_gapi, @service, next_kwargs end ## @@ -139,12 +139,12 @@ def all request_limit: nil ## # @private New Job::List from a Google API Client # Google::Apis::BigqueryV2::JobList object. - def self.from_gapi gapi_list, service, options = {} + def self.from_gapi gapi_list, service, **kwargs jobs = List.new(Array(gapi_list.jobs).map { |gapi_object| Job.from_gapi gapi_object, service }) jobs.instance_variable_set :@token, gapi_list.next_page_token jobs.instance_variable_set :@etag, gapi_list.etag jobs.instance_variable_set :@service, service - jobs.instance_variable_set :@options, options + jobs.instance_variable_set :@kwargs, kwargs jobs end diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/load_job.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/load_job.rb index 50da7533592a..9cdf12030c03 100644 --- a/google-cloud-bigquery/lib/google/cloud/bigquery/load_job.rb +++ b/google-cloud-bigquery/lib/google/cloud/bigquery/load_job.rb @@ -1426,6 +1426,23 @@ def clustering_fields= fields @gapi.configuration.load.clustering.fields = fields end + def cancel + raise "not implemented in #{self.class}" + end + + def rerun! + raise "not implemented in #{self.class}" + end + + def reload! + raise "not implemented in #{self.class}" + end + alias refresh! reload! + + def wait_until_done! + raise "not implemented in #{self.class}" + end + ## # @private Returns the Google API client library version of this job. # diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/model.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/model.rb index 1c87671ad351..d95dd63270a6 100644 --- a/google-cloud-bigquery/lib/google/cloud/bigquery/model.rb +++ b/google-cloud-bigquery/lib/google/cloud/bigquery/model.rb @@ -449,7 +449,8 @@ def encryption= value def feature_columns ensure_full_data! Array(@gapi_json[:featureColumns]).map do |field_gapi_json| - StandardSql::Field.from_gapi_json field_gapi_json + field_gapi = Google::Apis::BigqueryV2::StandardSqlField.from_json field_gapi_json.to_json + StandardSql::Field.from_gapi field_gapi end end @@ -464,7 +465,8 @@ def feature_columns def label_columns ensure_full_data! Array(@gapi_json[:labelColumns]).map do |field_gapi_json| - StandardSql::Field.from_gapi_json field_gapi_json + field_gapi = Google::Apis::BigqueryV2::StandardSqlField.from_json field_gapi_json.to_json + StandardSql::Field.from_gapi field_gapi end end @@ -554,7 +556,7 @@ def reload! # model = dataset.model "my_model", skip_lookup: true # model.exists? #=> true # - def exists? force: nil + def exists? force: false return resource_exists? if force # If we have a value, return it return @exists unless @exists.nil? @@ -668,7 +670,7 @@ def self.from_gapi_json gapi_json, service end ## - # @private New lazy Model object without making an HTTP request. + # @private New lazy Model object without making an HTTP request, for use with the skip_lookup option. def self.new_reference project_id, dataset_id, model_id, service raise ArgumentError, "project_id is required" unless project_id raise ArgumentError, "dataset_id is required" unless dataset_id diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/project.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/project.rb index 934965e8d7cd..0a70309c4aa9 100644 --- a/google-cloud-bigquery/lib/google/cloud/bigquery/project.rb +++ b/google-cloud-bigquery/lib/google/cloud/bigquery/project.rb @@ -527,7 +527,7 @@ def copy source_table, destination_table, create: nil, write: nil, &block # # job.wait_until_done! # if !job.failed? - # table_ref = job.ddl_target_table + # table_ref = job.ddl_target_table # Or ddl_target_routine for CREATE/DROP FUNCTION/PROCEDURE # end # # @example Execute a DML statement: @@ -786,7 +786,7 @@ def query_job query, params: nil, types: nil, external: nil, priority: "INTERACT # # data = bigquery.query "CREATE TABLE `my_dataset.my_table` (x INT64)" # - # table_ref = data.ddl_target_table + # table_ref = data.ddl_target_table # Or ddl_target_routine for CREATE/DROP FUNCTION/PROCEDURE # # @example Execute a DML statement: # require "google/cloud/bigquery" @@ -1046,8 +1046,7 @@ def create_dataset dataset_id, name: nil, description: nil, # def datasets all: nil, filter: nil, token: nil, max: nil ensure_service! - options = { all: all, filter: filter, token: token, max: max } - gapi = service.list_datasets options + gapi = service.list_datasets all: all, filter: filter, token: token, max: max Dataset::List.from_gapi gapi, service, all, filter, max end @@ -1197,8 +1196,7 @@ def jobs all: nil, token: nil, max: nil, filter: nil, # def projects token: nil, max: nil ensure_service! - options = { token: token, max: max } - gapi = service.list_projects options + gapi = service.list_projects token: token, max: max Project::List.from_gapi gapi, service, max end diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/project/list.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/project/list.rb index 6152ac22d23a..68543cade80d 100644 --- a/google-cloud-bigquery/lib/google/cloud/bigquery/project/list.rb +++ b/google-cloud-bigquery/lib/google/cloud/bigquery/project/list.rb @@ -72,8 +72,7 @@ def next? def next return nil unless next? ensure_service! - options = { all: @hidden, token: token, max: @max } - gapi = @service.list_projects options + gapi = @service.list_projects token: token, max: @max self.class.from_gapi gapi, @service, @max end diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/query_job.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/query_job.rb index f9f2cf2c206c..6b25e6438d8b 100644 --- a/google-cloud-bigquery/lib/google/cloud/bigquery/query_job.rb +++ b/google-cloud-bigquery/lib/google/cloud/bigquery/query_job.rb @@ -305,6 +305,22 @@ def ddl_operation_performed @gapi.statistics.query.ddl_operation_performed end + ## + # The DDL target routine, in reference state. (See {Routine#reference?}.) + # Present only for `CREATE/DROP FUNCTION/PROCEDURE` queries. (See + # {#statement_type}.) + # + # @return [Google::Cloud::Bigquery::Routine, nil] The DDL target routine, in + # reference state. + # + def ddl_target_routine + return nil unless @gapi.statistics.query + ensure_service! + routine = @gapi.statistics.query.ddl_target_routine + return nil unless routine + Google::Cloud::Bigquery::Routine.new_reference_from_gapi routine, service + end + ## # The DDL target table, in reference state. (See {Table#reference?}.) # Present only for `CREATE/DROP TABLE/VIEW` queries. (See @@ -1198,6 +1214,23 @@ def clustering_fields= fields @gapi.configuration.query.clustering.fields = fields end + def cancel + raise "not implemented in #{self.class}" + end + + def rerun! + raise "not implemented in #{self.class}" + end + + def reload! + raise "not implemented in #{self.class}" + end + alias refresh! reload! + + def wait_until_done! + raise "not implemented in #{self.class}" + end + ## # @private Returns the Google API client library version of this job. # diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/routine.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/routine.rb new file mode 100644 index 000000000000..a81d1fc21c93 --- /dev/null +++ b/google-cloud-bigquery/lib/google/cloud/bigquery/routine.rb @@ -0,0 +1,1108 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# require "google/cloud/errors" +require "google/cloud/bigquery/convert" +require "google/cloud/bigquery/service" +require "google/cloud/bigquery/routine/list" +require "google/cloud/bigquery/argument" + +module Google + module Cloud + module Bigquery + ## + # # Routine + # + # A user-defined function or a stored procedure. + # + # @example Creating a new routine: + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # + # routine = dataset.create_routine "my_routine" do |r| + # r.routine_type = "SCALAR_FUNCTION" + # r.language = "SQL" + # r.arguments = [ + # Google::Cloud::Bigquery::Argument.new(name: "x", data_type: "INT64") + # ] + # r.body = "x * 3" + # r.description = "My routine description" + # end + # + # puts routine.routine_id + # + # @example Extended example: + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.create_routine "my_routine" do |r| + # r.routine_type = "SCALAR_FUNCTION" + # r.language = :SQL + # r.body = "(SELECT SUM(IF(elem.name = \"foo\",elem.val,null)) FROM UNNEST(arr) AS elem)" + # r.arguments = [ + # Google::Cloud::Bigquery::Argument.new( + # name: "arr", + # argument_kind: "FIXED_TYPE", + # data_type: Google::Cloud::Bigquery::StandardSql::DataType.new( + # type_kind: "ARRAY", + # array_element_type: Google::Cloud::Bigquery::StandardSql::DataType.new( + # type_kind: "STRUCT", + # struct_type: Google::Cloud::Bigquery::StandardSql::StructType.new( + # fields: [ + # Google::Cloud::Bigquery::StandardSql::Field.new( + # name: "name", + # type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "STRING") + # ), + # Google::Cloud::Bigquery::StandardSql::Field.new( + # name: "val", + # type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "INT64") + # ) + # ] + # ) + # ) + # ) + # ) + # ] + # end + # + # @example Retrieving and updating an existing routine: + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.routine "my_routine" + # + # routine.update do |r| + # r.body = "x * 4" + # r.description = "My new routine description" + # end + # + class Routine + ## + # @private The Service object. + attr_accessor :service + + ## + # @private The Google API Client object. + attr_accessor :gapi + + ## + # @private A Google API Client Dataset Reference object. + attr_reader :reference + + ## + # @private Creates an empty Routine object. + def initialize + @service = nil + @gapi = nil + @reference = nil + end + + ## + # A unique ID for this routine, without the project name. + # + # @return [String] The ID must contain only letters (a-z, A-Z), numbers (0-9), or underscores (_). The maximum + # length is 256 characters. + # + # @!group Attributes + # + def routine_id + return reference.routine_id if reference? + @gapi.routine_reference.routine_id + end + + ## + # The ID of the dataset containing this routine. + # + # @return [String] The dataset ID. + # + # @!group Attributes + # + def dataset_id + return reference.dataset_id if reference? + @gapi.routine_reference.dataset_id + end + + ## + # The ID of the project containing this routine. + # + # @return [String] The project ID. + # + # @!group Attributes + # + def project_id + return reference.project_id if reference? + @gapi.routine_reference.project_id + end + + ## + # @private The gapi fragment containing the Project ID, Dataset ID, and Routine ID. + # + # @return [Google::Apis::BigqueryV2::RoutineReference] + # + def routine_ref + reference? ? reference : @gapi.routine_reference + end + + ## + # The ETag hash of the routine. + # + # @return [String, nil] The ETag hash, or `nil` if the object is a reference (see {#reference?}). + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.routine "my_routine" + # + # routine.etag # "etag123456789" + # + # @!group Attributes + # + def etag + return nil if reference? + @gapi.etag + end + + ## + # The type of routine. Required. + # + # * `SCALAR_FUNCTION` - Non-builtin permanent scalar function. + # * `PROCEDURE` - Stored procedure. + # + # @return [String, nil] The type of routine in upper case, or `nil` if the object is a reference (see + # {#reference?}). + # + # @!group Attributes + # + def routine_type + return nil if reference? + @gapi.routine_type + end + + ## + # Updates the type of routine. Required. + # + # * `SCALAR_FUNCTION` - Non-builtin permanent scalar function. + # * `PROCEDURE` - Stored procedure. + # + # @param [String] new_routine_type The new type of the routine in upper case. + # + # @!group Attributes + # + def routine_type= new_routine_type + ensure_full_data! + @gapi.routine_type = new_routine_type + update_gapi! + end + + ## + # Checks if the value of {#routine_type} is `PROCEDURE`. The default is `false`. + # + # @return [Boolean] `true` when `PROCEDURE` and the object is not a reference (see {#reference?}), `false` + # otherwise. + # + # @!group Attributes + # + def procedure? + @gapi.routine_type == "PROCEDURE" + end + + ## + # Checks if the value of {#routine_type} is `SCALAR_FUNCTION`. The default is `true`. + # + # @return [Boolean] `true` when `SCALAR_FUNCTION` and the object is not a reference (see {#reference?}), `false` + # otherwise. + # + # @!group Attributes + # + def scalar_function? + @gapi.routine_type == "SCALAR_FUNCTION" + end + + ## + # The time when this routine was created. + # + # @return [Time, nil] The creation time, or `nil` if the object is a reference (see {#reference?}). + # + # @!group Attributes + # + def created_at + return nil if reference? + Convert.millis_to_time @gapi.creation_time + end + + ## + # The time when this routine was last modified. + # + # @return [Time, nil] The last modified time, or `nil` if the object is a reference (see {#reference?}). + # + # @!group Attributes + # + def modified_at + return nil if reference? + Convert.millis_to_time @gapi.last_modified_time + end + + ## + # The programming language of routine. Optional. Defaults to "SQL". + # + # * `SQL` - SQL language. + # * `JAVASCRIPT` - JavaScript language. + # + # @return [String, nil] The language in upper case, or `nil` if the object is a reference (see {#reference?}). + # + # @!group Attributes + # + def language + return nil if reference? + @gapi.language + end + + ## + # Updates the programming language of routine. Optional. Defaults to "SQL". + # + # * `SQL` - SQL language. + # * `JAVASCRIPT` - JavaScript language. + # + # @param [String] new_language The new language in upper case. + # + # @!group Attributes + # + def language= new_language + ensure_full_data! + @gapi.language = new_language + update_gapi! + end + + ## + # Checks if the value of {#language} is `JAVASCRIPT`. The default is `false`. + # + # @return [Boolean] `true` when `JAVASCRIPT` and the object is not a reference (see {#reference?}), `false` + # otherwise. + # + # @!group Attributes + # + def javascript? + @gapi.language == "JAVASCRIPT" + end + + ## + # Checks if the value of {#language} is `SQL`. The default is `true`. + # + # @return [Boolean] `true` when `SQL` and the object is not a reference (see {#reference?}), `false` + # otherwise. + # + # @!group Attributes + # + def sql? + return true if @gapi.language.nil? + @gapi.language == "SQL" + end + + ## + # The input/output arguments of the routine. Optional. + # + # @return [Array, nil] An array of argument objects, or `nil` if the object is a reference (see + # {#reference?}). + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.routine "my_routine" + # + # puts "#{routine.routine_id} arguments:" + # routine.arguments.each do |arguments| + # puts "* #{arguments.name}" + # end + # + # @!group Attributes + # + def arguments + return nil if reference? + ensure_full_data! + # always return frozen arguments + Array(@gapi.arguments).map { |a| Argument.from_gapi a }.freeze + end + + ## + # Updates the input/output arguments of the routine. Optional. + # + # @param [Array] new_arguments The new arguments. + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.routine "my_routine" + # + # routine.arguments = [ + # Google::Cloud::Bigquery::Argument.new(name: "x", data_type: "INT64") + # ] + # + # @!group Attributes + # + def arguments= new_arguments + ensure_full_data! + @gapi.update! arguments: new_arguments.map(&:to_gapi) + update_gapi! + end + + ## + # The return type of the routine. Optional if the routine is a SQL function ({#sql?}); required otherwise. + # + # If absent, the return type is inferred from {#body} at query time in each query that references this routine. + # If present, then the evaluated result will be cast to the specified returned type at query time. + # + # For example, for the functions created with the following statements: + # + # * `CREATE FUNCTION Add(x FLOAT64, y FLOAT64) RETURNS FLOAT64 AS (x + y);` + # * `CREATE FUNCTION Increment(x FLOAT64) AS (Add(x, 1));` + # * `CREATE FUNCTION Decrement(x FLOAT64) RETURNS FLOAT64 AS (Add(x, -1));` + # + # The returnType is `{typeKind: "FLOAT64"}` for Add and Decrement, and is absent for Increment (inferred as + # `FLOAT64` at query time). + # + # Suppose the function Add is replaced by `CREATE OR REPLACE FUNCTION Add(x INT64, y INT64) AS (x + y);` + # + # Then the inferred return type of Increment is automatically changed to `INT64` at query time, while the return + # type of Decrement remains `FLOAT64`. + # + # @return [Google::Cloud::Bigquery::StandardSql::DataType, nil] The return type in upper case, or `nil` if the + # object is a reference (see {#reference?}). + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.routine "my_routine" + # + # routine.return_type.type_kind #=> "INT64" + # + # @!group Attributes + # + def return_type + return nil if reference? + ensure_full_data! + return nil unless @gapi.return_type + StandardSql::DataType.from_gapi @gapi.return_type + end + + ## + # Updates the return type of the routine. Optional if the routine is a SQL function ({#sql?}); required + # otherwise. + # + # If absent, the return type is inferred from {#body} at query time in each query that references this routine. + # If present, then the evaluated result will be cast to the specified returned type at query time. + # + # For example, for the functions created with the following statements: + # + # * `CREATE FUNCTION Add(x FLOAT64, y FLOAT64) RETURNS FLOAT64 AS (x + y);` + # * `CREATE FUNCTION Increment(x FLOAT64) AS (Add(x, 1));` + # * `CREATE FUNCTION Decrement(x FLOAT64) RETURNS FLOAT64 AS (Add(x, -1));` + # + # The returnType is `{typeKind: "FLOAT64"}` for Add and Decrement, and is absent for Increment (inferred as + # `FLOAT64` at query time). + # + # Suppose the function Add is replaced by `CREATE OR REPLACE FUNCTION Add(x INT64, y INT64) AS (x + y);` + # + # Then the inferred return type of Increment is automatically changed to `INT64` at query time, while the return + # type of Decrement remains `FLOAT64`. + # + # @param [Google::Cloud::Bigquery::StandardSql::DataType, String] new_return_type The new return type for the + # routine. + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.routine "my_routine" + # + # routine.return_type.type_kind #=> "INT64" + # routine.return_type = "STRING" + # + # @!group Attributes + # + def return_type= new_return_type + ensure_full_data! + @gapi.return_type = StandardSql::DataType.gapi_from_string_or_data_type new_return_type + update_gapi! + end + + ## + # The list of the Google Cloud Storage URIs of imported JavaScript libraries. Optional. Only used if + # {#language} is `JAVASCRIPT` ({#javascript?}). + # + # @return [Array, nil] A frozen array of Google Cloud Storage URIs, e.g. + # `["gs://cloud-samples-data/bigquery/udfs/max-value.js"]`, or `nil` if the object is a reference (see + # {#reference?}). + # + # @!group Attributes + # + def imported_libraries + return nil if reference? + ensure_full_data! + @gapi.imported_libraries.freeze + end + + ## + # Updates the list of the Google Cloud Storage URIs of imported JavaScript libraries. Optional. Only used if + # {#language} is `JAVASCRIPT` ({#javascript?}). + # + # @param [Array, nil] new_imported_libraries An array of Google Cloud Storage URIs, e.g. + # `["gs://cloud-samples-data/bigquery/udfs/max-value.js"]`. + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.routine "my_routine" + # + # routine.imported_libraries = [ + # "gs://cloud-samples-data/bigquery/udfs/max-value.js" + # ] + # + # @!group Attributes + # + def imported_libraries= new_imported_libraries + ensure_full_data! + @gapi.imported_libraries = new_imported_libraries + update_gapi! + end + + ## + # The body of the routine. Required. + # + # For functions ({#scalar_function?}), this is the expression in the `AS` clause. + # + # When the routine is a SQL function ({#sql?}), it is the substring inside (but excluding) the parentheses. For + # example, for the function created with the following statement: + # ``` + # CREATE FUNCTION JoinLines(x string, y string) as (concat(x, "\n", y)) + # ``` + # The definition_body is `concat(x, "\n", y)` (`\n` is not replaced with linebreak). + # + # When the routine is a JavaScript function ({#javascript?}), it is the evaluated string in the `AS` clause. For + # example, for the function created with the following statement: + # ``` + # CREATE FUNCTION f() RETURNS STRING LANGUAGE js AS 'return "\n";\n' + # ``` + # The definition_body is + # ``` + # "return \"\n\";\n"` + # ``` + # Note that both `\n` are replaced with linebreaks. + # + # @return [String, nil] The body of the routine, or `nil` if the object is a reference (see {#reference?}). + # + # @!group Attributes + # + def body + return nil if reference? + ensure_full_data! + @gapi.definition_body + end + + ## + # Updates the body of the routine. Required. + # + # For functions ({#scalar_function?}), this is the expression in the `AS` clause. + # + # When the routine is a SQL function ({#sql?}), it is the substring inside (but excluding) the parentheses. For + # example, for the function created with the following statement: + # ``` + # CREATE FUNCTION JoinLines(x string, y string) as (concat(x, "\n", y)) + # ``` + # The definition_body is `concat(x, "\n", y)` (`\n` is not replaced with linebreak). + # + # When the routine is a JavaScript function ({#javascript?}), it is the evaluated string in the `AS` clause. For + # example, for the function created with the following statement: + # ``` + # CREATE FUNCTION f() RETURNS STRING LANGUAGE js AS 'return "\n";\n' + # ``` + # The definition_body is + # ``` + # "return \"\n\";\n"` + # ``` + # Note that both `\n` are replaced with linebreaks. + # + # @param [String] new_body The new body of the routine. + # + # @!group Attributes + # + def body= new_body + ensure_full_data! + @gapi.definition_body = new_body + update_gapi! + end + + ### + # The description of the routine if defined. Optional. [Experimental] + # + # @return [String, nil] The routine description, or `nil` if the object is a reference (see {#reference?}). + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.routine "my_routine" + # + # routine.description #=> "My routine description" + # + # @!group Attributes + # + def description + return nil if reference? + ensure_full_data! + @gapi.description + end + + ## + # Updates the description of the routine. Optional. [Experimental] + # + # @param [String] new_description The new routine description. + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.routine "my_routine" + # + # routine.description #=> "My routine description" + # routine.description = "My updated routine description" + # + # @!group Attributes + # + def description= new_description + ensure_full_data! + @gapi.description = new_description + update_gapi! + end + + ## + # Updates the routine with changes made in the given block in a single update request. The following attributes + # may be set: {Updater#routine_type=}, {Updater#language=}, {Updater#arguments=}, {Updater#return_type=}, + # {Updater#imported_libraries=}, {Updater#body=}, and {Updater#description=}. + # + # @yield [routine] A block for setting properties on the routine. + # @yieldparam [Google::Cloud::Bigquery::Routine::Updater] routine An updater to set additional properties on the + # routine. + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.routine "my_routine" + # + # routine.update do |r| + # r.routine_type = "SCALAR_FUNCTION" + # r.language = "SQL" + # r.arguments = [ + # Google::Cloud::Bigquery::Argument.new(name: "x", data_type: "INT64") + # ] + # r.body = "x * 3" + # r.description = "My new routine description" + # end + # + # @!group Lifecycle + # + def update + ensure_full_data! + updater = Updater.new @gapi + yield updater + update_gapi! updater.to_gapi if updater.updates? + end + + ## + # Permanently deletes the routine. + # + # @return [Boolean] Returns `true` if the routine was deleted. + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.routine "my_routine" + # + # routine.delete + # + # @!group Lifecycle + # + def delete + ensure_service! + service.delete_routine dataset_id, routine_id + # Set flag for #exists? + @exists = false + true + end + + ## + # Reloads the routine with current data from the BigQuery service. + # + # @return [Google::Cloud::Bigquery::Routine] Returns the reloaded + # routine. + # + # @example Skip retrieving the routine from the service, then load it: + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.routine "my_routine", skip_lookup: true + # + # routine.reload! + # + # @!group Lifecycle + # + def reload! + ensure_service! + @gapi = service.get_routine dataset_id, routine_id + @reference = nil + @exists = nil + self + end + alias refresh! reload! + + ## + # Determines whether the routine exists in the BigQuery service. The + # result is cached locally. To refresh state, set `force` to `true`. + # + # @param [Boolean] force Force the latest resource representation to be + # retrieved from the BigQuery service when `true`. Otherwise the + # return value of this method will be memoized to reduce the number of + # API calls made to the BigQuery service. The default is `false`. + # + # @return [Boolean] `true` when the routine exists in the BigQuery + # service, `false` otherwise. + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.routine "my_routine", skip_lookup: true + # routine.exists? #=> true + # + def exists? force: false + return resource_exists? if force + # If we have a value, return it + return @exists unless @exists.nil? + # Always true if we have a gapi object + return true if resource? + resource_exists? + end + + ## + # Whether the routine was created without retrieving the resource + # representation from the BigQuery service. + # + # @return [Boolean] `true` when the routine is just a local reference + # object, `false` otherwise. + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.routine "my_routine", skip_lookup: true + # + # routine.reference? #=> true + # routine.reload! + # routine.reference? #=> false + # + def reference? + @gapi.nil? + end + + ## + # Whether the routine was created with a resource representation from + # the BigQuery service. + # + # @return [Boolean] `true` when the routine was created with a resource + # representation, `false` otherwise. + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.routine "my_routine", skip_lookup: true + # + # routine.resource? #=> false + # routine.reload! + # routine.resource? #=> true + # + def resource? + !@gapi.nil? + end + + ## + # Whether the routine was created with a partial resource representation + # from the BigQuery service by retrieval through {Dataset#routines}. + # See [Models: list + # response](https://cloud.google.com/bigquery/docs/reference/rest/v2/routines/list#response) + # for the contents of the partial representation. Accessing any + # attribute outside of the partial representation will result in loading + # the full representation. + # + # @return [Boolean] `true` when the routine was created with a partial + # resource representation, `false` otherwise. + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.routines.first + # + # routine.resource_partial? #=> true + # routine.description # Loads the full resource. + # routine.resource_partial? #=> false + # + def resource_partial? + resource? && !resource_full? + end + + ## + # Whether the routine was created with a full resource representation + # from the BigQuery service. + # + # @return [Boolean] `true` when the routine was created with a full + # resource representation, `false` otherwise. + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.routine "my_routine" + # + # routine.resource_full? #=> true + # + def resource_full? + resource? && !@gapi.definition_body.nil? + end + + ## + # @private New Routine from a Google API Client object. + def self.from_gapi gapi, service + new.tap do |r| + r.instance_variable_set :@gapi, gapi + r.instance_variable_set :@service, service + end + end + + ## + # @private New lazy Routine object without making an HTTP request, for use with the skip_lookup option. + def self.new_reference project_id, dataset_id, routine_id, service + raise ArgumentError, "project_id is required" unless project_id + raise ArgumentError, "dataset_id is required" unless dataset_id + raise ArgumentError, "routine_id is required" unless routine_id + raise ArgumentError, "service is required" unless service + + gapi = Google::Apis::BigqueryV2::RoutineReference.new( + project_id: project_id, + dataset_id: dataset_id, + routine_id: routine_id + ) + new.tap do |r| + r.service = service + r.instance_variable_set :@reference, gapi + end + end + + ## + # @private New lazy Routine object from a Google API Client object. + def self.new_reference_from_gapi gapi, service + new.tap do |b| + b.service = service + b.instance_variable_set :@reference, gapi + end + end + + protected + + ## + # Raise an error unless an active service is available. + def ensure_service! + raise "Must have active connection" unless service + end + + ## + # Fetch gapi and memoize whether resource exists. + def resource_exists? + reload! + @exists = true + rescue Google::Cloud::NotFoundError + @exists = false + end + + ## + # Load the complete representation of the routine if it has been + # only partially loaded by a request to the API list method. + def ensure_full_data! + reload! unless resource_full? + end + + def update_gapi! update_gapi = nil + update_gapi ||= @gapi + ensure_service! + @gapi = service.update_routine dataset_id, routine_id, update_gapi + self + end + + ## + # Yielded to a block to accumulate changes. See {Dataset#create_routine} and {Routine#update}. + # + # @example Creating a new routine: + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # + # routine = dataset.create_routine "my_routine" do |r| + # r.routine_type = "SCALAR_FUNCTION" + # r.language = "SQL" + # r.arguments = [ + # Google::Cloud::Bigquery::Argument.new(name: "x", data_type: "INT64") + # ] + # r.body = "x * 3" + # r.description = "My routine description" + # end + # + # puts routine.routine_id + # + # @example Updating an existing routine: + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.routine "my_routine" + # + # routine.update do |r| + # r.body = "x * 4" + # r.description = "My new routine description" + # end + # + class Updater < Routine + ## + # @private Create an Updater object. + def initialize gapi + @original_gapi = gapi + @gapi = gapi.dup + end + + ## + # Updates the type of routine. Required. + # + # * `SCALAR_FUNCTION` - Non-builtin permanent scalar function. + # * `PROCEDURE` - Stored procedure. + # + # @param [String] new_routine_type The new type of the routine. + # + def routine_type= new_routine_type + @gapi.routine_type = new_routine_type + end + + ## + # Updates the programming language of routine. Optional. Defaults to "SQL". + # + # * `SQL` - SQL language. + # * `JAVASCRIPT` - JavaScript language. + # + # @param [String] new_language The new language in upper case. + # + def language= new_language + @gapi.language = new_language + end + + ## + # Updates the input/output arguments of the routine. Optional. + # + # @param [Array] new_arguments The new arguments. + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.routine "my_routine" + # + # routine.arguments = [ + # Google::Cloud::Bigquery::Argument.new(name: "x", data_type: "INT64") + # ] + # + def arguments= new_arguments + @gapi.arguments = new_arguments.map(&:to_gapi) + end + + ## + # Updates the return type of the routine. Optional if the routine is a SQL function ({#sql?}); required + # otherwise. + # + # If absent, the return type is inferred from {#body} at query time in each query that references this + # routine. If present, then the evaluated result will be cast to the specified returned type at query time. + # + # For example, for the functions created with the following statements: + # + # * `CREATE FUNCTION Add(x FLOAT64, y FLOAT64) RETURNS FLOAT64 AS (x + y);` + # * `CREATE FUNCTION Increment(x FLOAT64) AS (Add(x, 1));` + # * `CREATE FUNCTION Decrement(x FLOAT64) RETURNS FLOAT64 AS (Add(x, -1));` + # + # The returnType is `{typeKind: "FLOAT64"}` for Add and Decrement, and is absent for Increment (inferred as + # `FLOAT64` at query time). + # + # Suppose the function Add is replaced by `CREATE OR REPLACE FUNCTION Add(x INT64, y INT64) AS (x + y);` + # + # Then the inferred return type of Increment is automatically changed to `INT64` at query time, while the + # return type of Decrement remains `FLOAT64`. + # + # @param [Google::Cloud::Bigquery::StandardSql::DataType, String] new_return_type The new return type for the + # routine. + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.routine "my_routine" + # + # routine.return_type.type_kind #=> "INT64" + # routine.return_type = "STRING" + # + def return_type= new_return_type + @gapi.return_type = StandardSql::DataType.gapi_from_string_or_data_type new_return_type + end + + ## + # Updates the list of the Google Cloud Storage URIs of imported JavaScript libraries. Optional. Only used if + # {#language} is `JAVASCRIPT` ({#javascript?}). + # + # @param [Array, nil] new_imported_libraries An array of Google Cloud Storage URIs, e.g. + # `["gs://cloud-samples-data/bigquery/udfs/max-value.js"]`. + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.routine "my_routine" + # + # routine.imported_libraries = [ + # "gs://cloud-samples-data/bigquery/udfs/max-value.js" + # ] + # + def imported_libraries= new_imported_libraries + @gapi.imported_libraries = new_imported_libraries + end + + ## + # Updates the body of the routine. Required. + # + # For functions ({#scalar_function?}), this is the expression in the `AS` clause. + # + # When the routine is a SQL function ({#sql?}), it is the substring inside (but excluding) the parentheses. + # For example, for the function created with the following statement: + # ``` + # CREATE FUNCTION JoinLines(x string, y string) as (concat(x, "\n", y)) + # ``` + # The definition_body is `concat(x, "\n", y)` (`\n` is not replaced with linebreak). + # + # When the routine is a JavaScript function ({#javascript?}), it is the evaluated string in the `AS` clause. + # For example, for the function created with the following statement: + # ``` + # CREATE FUNCTION f() RETURNS STRING LANGUAGE js AS 'return "\n";\n' + # ``` + # The definition_body is + # ``` + # "return \"\n\";\n"` + # ``` + # Note that both `\n` are replaced with linebreaks. + # + # @param [String] new_body The new body of the routine. + # + def body= new_body + @gapi.definition_body = new_body + end + + ## + # Updates the description of the routine. Optional. [Experimental] + # + # @param [String] new_description The new routine description. + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.routine "my_routine" + # + # routine.description #=> "My routine description" + # routine.description = "My updated routine description" + # + def description= new_description + @gapi.description = new_description + end + + def update + raise "not implemented in #{self.class}" + end + + def delete + raise "not implemented in #{self.class}" + end + + def reload! + raise "not implemented in #{self.class}" + end + alias refresh! reload! + + # rubocop:disable Style/CaseEquality + + # @private + def updates? + !(@gapi === @original_gapi) + end + + # rubocop:enable Style/CaseEquality + + # @private + def to_gapi + @gapi + end + end + end + end + end +end diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/routine/list.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/routine/list.rb new file mode 100644 index 000000000000..1afe55ac01b2 --- /dev/null +++ b/google-cloud-bigquery/lib/google/cloud/bigquery/routine/list.rb @@ -0,0 +1,165 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require "delegate" + +module Google + module Cloud + module Bigquery + class Routine + ## + # Routine::List is a special case Array with additional values. + class List < DelegateClass(::Array) + ## + # If not empty, indicates that there are more records that match + # the request and this value should be passed to continue. + attr_accessor :token + + ## + # @private Create a new Routine::List with an array of routines. + def initialize arr = [] + super arr + end + + ## + # Whether there is a next page of routines. + # + # @return [Boolean] + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # + # routines = dataset.routines + # if routines.next? + # next_routines = routines.next + # end + # + def next? + !token.nil? + end + + ## + # Retrieve the next page of routines. + # + # @return [Routine::List] + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # + # routines = dataset.routines + # if routines.next? + # next_routines = routines.next + # end + # + def next + return nil unless next? + ensure_service! + gapi = @service.list_routines @dataset_id, token: token, max: @max, filter: @filter + self.class.from_gapi gapi, @service, @dataset_id, @max, filter: @filter + end + + ## + # Retrieves remaining results by repeatedly invoking {#next} until + # {#next?} returns `false`. Calls the given block once for each + # result, which is passed as the argument to the block. + # + # An Enumerator is returned if no block is given. + # + # This method will make repeated API calls until all remaining results + # are retrieved. (Unlike `#each`, for example, which merely iterates + # over the results returned by a single API call.) Use with caution. + # + # @param [Integer] request_limit The upper limit of API requests to + # make to load all routines. Default is no limit. + # @yield [routine] The block for accessing each routine. + # @yieldparam [Routine] routine The routine object. + # + # @return [Enumerator] + # + # @example Iterating each result by passing a block: + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # + # dataset.routines.all do |routine| + # puts routine.routine_id + # end + # + # @example Using the enumerator by not passing a block: + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # + # all_names = dataset.routines.all.map do |routine| + # routine.routine_id + # end + # + # @example Limit the number of API requests made: + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # + # dataset.routines.all(request_limit: 10) do |routine| + # puts routine.routine_id + # end + # + def all request_limit: nil + request_limit = request_limit.to_i if request_limit + return enum_for :all, request_limit: request_limit unless block_given? + results = self + loop do + results.each { |r| yield r } + if request_limit + request_limit -= 1 + break if request_limit.negative? + end + break unless results.next? + results = results.next + end + end + + ## + # @private New Routine::List from a response object. + def self.from_gapi gapi_list, service, dataset_id = nil, max = nil, filter: nil + routines = List.new(Array(gapi_list.routines).map { |gapi| Routine.from_gapi gapi, service }) + routines.instance_variable_set :@token, gapi_list.next_page_token + routines.instance_variable_set :@service, service + routines.instance_variable_set :@dataset_id, dataset_id + routines.instance_variable_set :@max, max + routines.instance_variable_set :@filter, filter + routines + end + + protected + + ## + # Raise an error unless an active service is available. + def ensure_service! + raise "Must have active connection" unless @service + end + end + end + end + end +end diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/service.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/service.rb index d0607013f78d..a52bed7c46dc 100644 --- a/google-cloud-bigquery/lib/google/cloud/bigquery/service.rb +++ b/google-cloud-bigquery/lib/google/cloud/bigquery/service.rb @@ -78,12 +78,10 @@ def project_service_account ## # Lists all datasets in the specified project to which you have # been granted the READER dataset role. - def list_datasets options = {} + def list_datasets all: nil, filter: nil, max: nil, token: nil # The list operation is considered idempotent execute backoff: true do - service.list_datasets \ - @project, all: options[:all], filter: options[:filter], - max_results: options[:max], page_token: options[:token] + service.list_datasets @project, all: all, filter: filter, max_results: max, page_token: token end end @@ -133,10 +131,10 @@ def delete_dataset dataset_id, force = nil ## # Lists all tables in the specified dataset. # Requires the READER dataset role. - def list_tables dataset_id, options = {} + def list_tables dataset_id, max: nil, token: nil # The list operation is considered idempotent execute backoff: true do - service.list_tables @project, dataset_id, max_results: options[:max], page_token: options[:token] + service.list_tables @project, dataset_id, max_results: max, page_token: token end end @@ -190,26 +188,29 @@ def delete_table dataset_id, table_id ## # Retrieves data from the table. - def list_tabledata dataset_id, table_id, options = {} + def list_tabledata dataset_id, table_id, max: nil, token: nil, start: nil # The list operation is considered idempotent execute backoff: true do json_txt = service.list_table_data \ @project, dataset_id, table_id, - max_results: options.delete(:max), - page_token: options.delete(:token), - start_index: options.delete(:start), + max_results: max, + page_token: token, + start_index: start, options: { skip_deserialization: true } JSON.parse json_txt, symbolize_names: true end end - def insert_tabledata dataset_id, table_id, rows, options = {} + def insert_tabledata dataset_id, table_id, rows, insert_ids: nil, ignore_unknown: nil, skip_invalid: nil json_rows = Array(rows).map { |row| Convert.to_json_row row } - insert_tabledata_json_rows dataset_id, table_id, json_rows, options + insert_tabledata_json_rows dataset_id, table_id, json_rows, insert_ids: insert_ids, + ignore_unknown: ignore_unknown, + skip_invalid: skip_invalid end - def insert_tabledata_json_rows dataset_id, table_id, json_rows, options = {} - rows_and_ids = Array(json_rows).zip Array(options[:insert_ids]) + def insert_tabledata_json_rows dataset_id, table_id, json_rows, insert_ids: nil, ignore_unknown: nil, + skip_invalid: nil + rows_and_ids = Array(json_rows).zip Array(insert_ids) insert_rows = rows_and_ids.map do |json_row, insert_id| if insert_id == :skip { json: json_row } @@ -224,8 +225,8 @@ def insert_tabledata_json_rows dataset_id, table_id, json_rows, options = {} insert_req = { rows: insert_rows, - ignoreUnknownValues: options[:ignore_unknown], - skipInvalidRows: options[:skip_invalid] + ignoreUnknownValues: ignore_unknown, + skipInvalidRows: skip_invalid }.to_json # The insertAll with insertId operation is considered idempotent @@ -285,16 +286,66 @@ def delete_model dataset_id, model_id execute { service.delete_model @project, dataset_id, model_id } end + ## + # Creates a new routine in the dataset. + def insert_routine dataset_id, new_routine_gapi + execute { service.insert_routine @project, dataset_id, new_routine_gapi } + end + + ## + # Lists all routines in the specified dataset. + # Requires the READER dataset role. + # Unless readMask is set in the request, only the following fields are populated: + # etag, projectId, datasetId, routineId, routineType, creationTime, lastModifiedTime, and language. + def list_routines dataset_id, max: nil, token: nil, filter: nil + # The list operation is considered idempotent + execute backoff: true do + service.list_routines @project, dataset_id, max_results: max, + page_token: token, + filter: filter + end + end + + ## + # Gets the specified routine resource by routine ID. + def get_routine dataset_id, routine_id + # The get operation is considered idempotent + execute backoff: true do + service.get_routine @project, dataset_id, routine_id + end + end + + ## + # Updates information in an existing routine, replacing the entire routine resource. + def update_routine dataset_id, routine_id, new_routine_gapi + update_with_backoff = false + options = {} + if new_routine_gapi.etag + options[:header] = { "If-Match" => new_routine_gapi.etag } + # The update with etag operation is considered idempotent + update_with_backoff = true + end + execute backoff: update_with_backoff do + service.update_routine @project, dataset_id, routine_id, new_routine_gapi, options: options + end + end + + ## + # Deletes the routine specified by routine_id from the dataset. + def delete_routine dataset_id, routine_id + execute { service.delete_routine @project, dataset_id, routine_id } + end + ## # Lists all jobs in the specified project to which you have # been granted the READER job role. - def list_jobs options = {} + def list_jobs all: nil, max: nil, token: nil, filter: nil, min_created_at: nil, max_created_at: nil # The list operation is considered idempotent - min_creation_time = Convert.time_to_millis options[:min_created_at] - max_creation_time = Convert.time_to_millis options[:max_created_at] + min_creation_time = Convert.time_to_millis min_created_at + max_creation_time = Convert.time_to_millis max_created_at execute backoff: true do - service.list_jobs @project, all_users: options[:all], max_results: options[:max], - page_token: options[:token], projection: "full", state_filter: options[:filter], + service.list_jobs @project, all_users: all, max_results: max, + page_token: token, projection: "full", state_filter: filter, min_creation_time: min_creation_time, max_creation_time: max_creation_time end end @@ -333,15 +384,15 @@ def query_job query_job_gapi ## # Returns the query data for the job - def job_query_results job_id, options = {} + def job_query_results job_id, location: nil, max: nil, token: nil, start: nil, timeout: nil # The get operation is considered idempotent execute backoff: true do service.get_job_query_results @project, job_id, - location: options.delete(:location), - max_results: options.delete(:max), - page_token: options.delete(:token), - start_index: options.delete(:start), - timeout_ms: options.delete(:timeout) + location: location, + max_results: max, + page_token: token, + start_index: start, + timeout_ms: timeout end end @@ -409,9 +460,9 @@ def self.validate_table_ref table_ref ## # Lists all projects to which you have been granted any project role. - def list_projects options = {} + def list_projects max: nil, token: nil execute backoff: true do - service.list_projects max_results: options[:max], page_token: options[:token] + service.list_projects max_results: max, page_token: token end end @@ -489,10 +540,10 @@ class << self sleep delay end - def initialize options = {} - @retries = (options[:retries] || Backoff.retries).to_i - @reasons = (options[:reasons] || Backoff.reasons).to_a - @backoff = options[:backoff] || Backoff.backoff + def initialize retries: nil, reasons: nil, backoff: nil + @retries = (retries || Backoff.retries).to_i + @reasons = (reasons || Backoff.reasons).to_a + @backoff = backoff || Backoff.backoff end def execute diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/standard_sql.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/standard_sql.rb index a9cf20e556c7..5ac5b1ac186d 100644 --- a/google-cloud-bigquery/lib/google/cloud/bigquery/standard_sql.rb +++ b/google-cloud-bigquery/lib/google/cloud/bigquery/standard_sql.rb @@ -18,89 +18,226 @@ module Cloud module Bigquery ## # BigQuery standard SQL is compliant with the SQL 2011 standard and has - # extensions that support querying nested and repeated data. + # extensions that support querying nested and repeated data. See {Routine} and {Argument}. + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.create_routine "my_routine" do |r| + # r.routine_type = "SCALAR_FUNCTION" + # r.language = :SQL + # r.body = "(SELECT SUM(IF(elem.name = \"foo\",elem.val,null)) FROM UNNEST(arr) AS elem)" + # r.arguments = [ + # Google::Cloud::Bigquery::Argument.new( + # name: "arr", + # argument_kind: "FIXED_TYPE", + # data_type: Google::Cloud::Bigquery::StandardSql::DataType.new( + # type_kind: "ARRAY", + # array_element_type: Google::Cloud::Bigquery::StandardSql::DataType.new( + # type_kind: "STRUCT", + # struct_type: Google::Cloud::Bigquery::StandardSql::StructType.new( + # fields: [ + # Google::Cloud::Bigquery::StandardSql::Field.new( + # name: "name", + # type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "STRING") + # ), + # Google::Cloud::Bigquery::StandardSql::Field.new( + # name: "val", + # type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "INT64") + # ) + # ] + # ) + # ) + # ) + # ) + # ] + # end + # module StandardSql ## - # A field or a column. + # A field or a column. See {Routine} and {Argument}. + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.create_routine "my_routine" do |r| + # r.routine_type = "SCALAR_FUNCTION" + # r.language = :SQL + # r.body = "(SELECT SUM(IF(elem.name = \"foo\",elem.val,null)) FROM UNNEST(arr) AS elem)" + # r.arguments = [ + # Google::Cloud::Bigquery::Argument.new( + # name: "arr", + # argument_kind: "FIXED_TYPE", + # data_type: Google::Cloud::Bigquery::StandardSql::DataType.new( + # type_kind: "ARRAY", + # array_element_type: Google::Cloud::Bigquery::StandardSql::DataType.new( + # type_kind: "STRUCT", + # struct_type: Google::Cloud::Bigquery::StandardSql::StructType.new( + # fields: [ + # Google::Cloud::Bigquery::StandardSql::Field.new( + # name: "name", + # type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "STRING") + # ), + # Google::Cloud::Bigquery::StandardSql::Field.new( + # name: "val", + # type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "INT64") + # ) + # ] + # ) + # ) + # ) + # ) + # ] + # end + # class Field ## - # @private Create an empty StandardSql::Field object. - def initialize - @gapi_json = nil + # Creates a new, immutable StandardSql::Field object. + # + # @overload initialize(name, type) + # @param [String] name The name of the field. Optional. Can be absent for struct fields. + # @param [StandardSql::DataType, String] type The type of the field. Optional. Absent if not explicitly + # specified (e.g., `CREATE FUNCTION` statement can omit the return type; in this case the output parameter + # does not have this "type" field). + # + def initialize **kwargs + # Convert client object kwargs to a gapi object + kwargs[:type] = DataType.gapi_from_string_or_data_type kwargs[:type] if kwargs[:type] + @gapi = Google::Apis::BigqueryV2::StandardSqlField.new(**kwargs) end ## - # The name of the field. (Can be absent for struct fields.) + # The name of the field. Optional. Can be absent for struct fields. # # @return [String, nil] # def name - return nil if @gapi_json[:name] == "".freeze - - @gapi_json[:name] + return if @gapi.name == "".freeze + @gapi.name end ## - # The type of the field. + # The type of the field. Optional. Absent if not explicitly specified (e.g., `CREATE FUNCTION` statement can + # omit the return type; in this case the output parameter does not have this "type" field). # - # @return [DataType] + # @return [DataType, nil] The type of the field. # def type - DataType.from_gapi_json @gapi_json[:type] + DataType.from_gapi @gapi.type if @gapi.type + end + + ## + # @private New Google::Apis::BigqueryV2::StandardSqlField object. + def to_gapi + @gapi end ## - # @private New StandardSql::Field from a JSON object. - def self.from_gapi_json gapi_json + # @private New StandardSql::Field from a Google::Apis::BigqueryV2::StandardSqlField object. + def self.from_gapi gapi new.tap do |f| - f.instance_variable_set :@gapi_json, gapi_json + f.instance_variable_set :@gapi, gapi end end end ## - # The type of a field or a column. + # The type of a variable, e.g., a function argument. See {Routine} and {Argument}. + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.create_routine "my_routine" do |r| + # r.routine_type = "SCALAR_FUNCTION" + # r.language = :SQL + # r.body = "(SELECT SUM(IF(elem.name = \"foo\",elem.val,null)) FROM UNNEST(arr) AS elem)" + # r.arguments = [ + # Google::Cloud::Bigquery::Argument.new( + # name: "arr", + # argument_kind: "FIXED_TYPE", + # data_type: Google::Cloud::Bigquery::StandardSql::DataType.new( + # type_kind: "ARRAY", + # array_element_type: Google::Cloud::Bigquery::StandardSql::DataType.new( + # type_kind: "STRUCT", + # struct_type: Google::Cloud::Bigquery::StandardSql::StructType.new( + # fields: [ + # Google::Cloud::Bigquery::StandardSql::Field.new( + # name: "name", + # type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "STRING") + # ), + # Google::Cloud::Bigquery::StandardSql::Field.new( + # name: "val", + # type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "INT64") + # ) + # ] + # ) + # ) + # ) + # ) + # ] + # end + # + # @see https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types Standard SQL Data Types + # class DataType ## - # @private Create an empty StandardSql::DataType object. - def initialize - @gapi_json = nil + # Creates a new, immutable StandardSql::DataType object. + # + # @overload initialize(type_kind, array_element_type, struct_type) + # @param [String] type_kind The top level type of this field. Required. Can be [any standard SQL data + # type](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types) (e.g., `INT64`, `DATE`, + # `ARRAY`). + # @param [DataType, String] array_element_type The type of the array's elements, if {#type_kind} is `ARRAY`. + # See {#array?}. Optional. + # @param [StructType] struct_type The fields of the struct, in order, if {#type_kind} is `STRUCT`. See + # {#struct?}. Optional. + # + def initialize **kwargs + # Convert client object kwargs to a gapi object + if kwargs[:array_element_type] + kwargs[:array_element_type] = self.class.gapi_from_string_or_data_type kwargs[:array_element_type] + end + kwargs[:struct_type] = kwargs[:struct_type].to_gapi if kwargs[:struct_type] + + @gapi = Google::Apis::BigqueryV2::StandardSqlDataType.new(**kwargs) end ## - # The top level type of this field. - # - # Can be any standard SQL data type (e.g., "INT64", "DATE", "ARRAY"). + # The top level type of this field. Required. Can be any standard SQL data type (e.g., `INT64`, `DATE`, + # `ARRAY`). # - # @see https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types - # Standard SQL Data Types + # @see https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types Standard SQL Data Types # - # @return [String] + # @return [String] The upper case type. # def type_kind - @gapi_json[:typeKind] + @gapi.type_kind end ## - # The type of a fields when DataType is an Array. (See #array?) + # The type of the array's elements, if {#type_kind} is `ARRAY`. See {#array?}. Optional. # # @return [DataType, nil] # def array_element_type - return if @gapi_json[:arrayElementType].nil? - - DataType.from_gapi_json @gapi_json[:arrayElementType] + return if @gapi.array_element_type.nil? + DataType.from_gapi @gapi.array_element_type end ## - # The fields of the struct. (See #struct?) + # The fields of the struct, in order, if {#type_kind} is `STRUCT`. See {#struct?}. Optional. # # @return [StructType, nil] # def struct_type - return if @gapi_json[:structType].nil? - - StructType.from_gapi_json @gapi_json[:structType] + return if @gapi.struct_type.nil? + StructType.from_gapi @gapi.struct_type end ## @@ -247,41 +384,108 @@ def struct? end ## - # @private New StandardSql::DataType from a JSON object. - def self.from_gapi_json gapi_json - new.tap do |dt| - dt.instance_variable_set :@gapi_json, gapi_json + # @private New Google::Apis::BigqueryV2::StandardSqlDataType object. + def to_gapi + @gapi + end + + ## + # @private New StandardSql::DataType from a Google::Apis::BigqueryV2::StandardSqlDataType object. + def self.from_gapi gapi + new.tap do |f| + f.instance_variable_set :@gapi, gapi + end + end + + ## + # @private New Google::Apis::BigqueryV2::StandardSqlDataType from a String or StandardSql::DataType object. + def self.gapi_from_string_or_data_type data_type + return if data_type.nil? + if data_type.is_a? StandardSql::DataType + data_type.to_gapi + elsif data_type.is_a? Hash + data_type + elsif data_type.is_a?(String) || data_type.is_a?(Symbol) + Google::Apis::BigqueryV2::StandardSqlDataType.new type_kind: data_type.to_s.upcase + else + raise ArgumentError, "Unable to convert #{data_type} to Google::Apis::BigqueryV2::StandardSqlDataType" end end end ## - # The type of a `STRUCT` field or a column. + # The fields of a `STRUCT` type. See {DataType#struct_type}. See {Routine} and {Argument}. + # + # @example + # require "google/cloud/bigquery" + # + # bigquery = Google::Cloud::Bigquery.new + # dataset = bigquery.dataset "my_dataset" + # routine = dataset.create_routine "my_routine" do |r| + # r.routine_type = "SCALAR_FUNCTION" + # r.language = :SQL + # r.body = "(SELECT SUM(IF(elem.name = \"foo\",elem.val,null)) FROM UNNEST(arr) AS elem)" + # r.arguments = [ + # Google::Cloud::Bigquery::Argument.new( + # name: "arr", + # argument_kind: "FIXED_TYPE", + # data_type: Google::Cloud::Bigquery::StandardSql::DataType.new( + # type_kind: "ARRAY", + # array_element_type: Google::Cloud::Bigquery::StandardSql::DataType.new( + # type_kind: "STRUCT", + # struct_type: Google::Cloud::Bigquery::StandardSql::StructType.new( + # fields: [ + # Google::Cloud::Bigquery::StandardSql::Field.new( + # name: "name", + # type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "STRING") + # ), + # Google::Cloud::Bigquery::StandardSql::Field.new( + # name: "val", + # type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "INT64") + # ) + # ] + # ) + # ) + # ) + # ) + # ] + # end + # class StructType ## - # @private Create an empty StandardSql::DataType object. - def initialize - @gapi_json = nil + # Creates a new, immutable StandardSql::StructType object. + # + # @overload initialize(fields) + # @param [Array] fields The fields of the struct. Required. + # + def initialize **kwargs + # Convert each field client object to gapi object, if fields given (self.from_gapi does not pass kwargs) + kwargs[:fields] = kwargs[:fields]&.map(&:to_gapi) if kwargs[:fields] + @gapi = Google::Apis::BigqueryV2::StandardSqlStructType.new(**kwargs) end ## - # The top level type of this field. + # The fields of the struct. # - # Can be any standard SQL data type (e.g., "INT64", "DATE", "ARRAY"). - # - # @return [Array] + # @return [Array] A frozen array of fields. # def fields - Array(@gapi_json[:fields]).map do |field_gapi_json| - Field.from_gapi_json field_gapi_json - end + Array(@gapi.fields).map do |field_gapi| + Field.from_gapi field_gapi + end.freeze + end + + ## + # @private New Google::Apis::BigqueryV2::StandardSqlStructType object. + def to_gapi + @gapi end ## - # @private New StandardSql::StructType from a JSON object. - def self.from_gapi_json gapi_json - new.tap do |st| - st.instance_variable_set :@gapi_json, gapi_json + # @private New StandardSql::StructType from a Google::Apis::BigqueryV2::StandardSqlStructType object. + def self.from_gapi gapi + new.tap do |f| + f.instance_variable_set :@gapi, gapi end end end diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/table.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/table.rb index 7b65ae13c298..2e7f1fd32c8c 100644 --- a/google-cloud-bigquery/lib/google/cloud/bigquery/table.rb +++ b/google-cloud-bigquery/lib/google/cloud/bigquery/table.rb @@ -1217,8 +1217,7 @@ def query_udfs def data token: nil, max: nil, start: nil ensure_service! reload! unless resource_full? - options = { token: token, max: max, start: start } - data_json = service.list_tabledata dataset_id, table_id, options + data_json = service.list_tabledata dataset_id, table_id, token: token, max: max, start: start Data.from_gapi_json data_json, gapi, nil, service end @@ -2165,7 +2164,7 @@ def reload! # table = dataset.table "my_table", skip_lookup: true # table.exists? # true # - def exists? force: nil + def exists? force: false return gapi_exists? if force # If we have a value, return it return @exists unless @exists.nil? @@ -2279,7 +2278,7 @@ def self.from_gapi gapi, service end ## - # @private New lazy Table object without making an HTTP request. + # @private New lazy Table object without making an HTTP request, for use with the skip_lookup option. def self.new_reference project_id, dataset_id, table_id, service raise ArgumentError, "dataset_id is required" unless dataset_id raise ArgumentError, "table_id is required" unless table_id @@ -2508,14 +2507,14 @@ def udfs_gapi array_or_str end ## - # Yielded to a block to accumulate changes for a patch request. + # Yielded to a block to accumulate changes for a create request. See {Dataset#create_table}. class Updater < Table ## - # A list of attributes that were updated. + # @private A list of attributes that were updated. attr_reader :updates ## - # Create an Updater object. + # @private Create an Updater object. def initialize gapi @updates = [] @gapi = gapi @@ -2958,8 +2957,97 @@ def record name, description: nil, mode: nil, &block schema.record name, description: description, mode: mode, &block end + # rubocop:disable Style/MethodDefParentheses + + ## + # @raise [RuntimeError] not implemented + def data(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def copy_job(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def copy(*) + raise "not implemented in #{self.class}" + end + ## - # Make sure any access changes are saved + # @raise [RuntimeError] not implemented + def extract_job(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def extract(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def load_job(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def load(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def insert(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def insert_async(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def delete + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def query_job(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def query(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def external(*) + raise "not implemented in #{self.class}" + end + + ## + # @raise [RuntimeError] not implemented + def reload! + raise "not implemented in #{self.class}" + end + alias refresh! reload! + + # rubocop:enable Style/MethodDefParentheses + + ## + # @private Make sure any access changes are saved def check_for_mutated_schema! return if @schema.nil? return unless @schema.changed? @@ -2967,6 +3055,8 @@ def check_for_mutated_schema! patch_gapi! :schema end + ## + # @private def to_gapi check_for_mutated_schema! @gapi diff --git a/google-cloud-bigquery/lib/google/cloud/bigquery/table/list.rb b/google-cloud-bigquery/lib/google/cloud/bigquery/table/list.rb index 6e418542da16..30e9796b415e 100644 --- a/google-cloud-bigquery/lib/google/cloud/bigquery/table/list.rb +++ b/google-cloud-bigquery/lib/google/cloud/bigquery/table/list.rb @@ -78,8 +78,7 @@ def next? def next return nil unless next? ensure_service! - options = { token: token, max: @max } - gapi = @service.list_tables @dataset_id, options + gapi = @service.list_tables @dataset_id, token: token, max: @max self.class.from_gapi gapi, @service, @dataset_id, @max end diff --git a/google-cloud-bigquery/support/doctest_helper.rb b/google-cloud-bigquery/support/doctest_helper.rb index 60fe303684e2..3e4852738e60 100644 --- a/google-cloud-bigquery/support/doctest_helper.rb +++ b/google-cloud-bigquery/support/doctest_helper.rb @@ -107,6 +107,13 @@ def mock_storage end end + doctest.before "Google::Cloud::Bigquery::Argument" do + mock_bigquery do |mock| + mock.expect :get_dataset, dataset_full_gapi, ["my-project", "my_dataset"] + mock.expect :insert_routine, random_routine_gapi("my_dataset", "my_routine"), ["my-project", "my_dataset", Google::Apis::BigqueryV2::Routine] + end + end + doctest.skip "Google::Cloud::Bigquery::Credentials" # occasionally getting "This code example is not yet mocked" # Google::Cloud::Bigquery::Data#all@Iterating each rows by passing a block: @@ -191,6 +198,13 @@ def mock_storage end end + doctest.before "Google::Cloud::Bigquery::Dataset#create_routine" do + mock_bigquery do |mock| + mock.expect :get_dataset, dataset_full_gapi, ["my-project", "my_dataset"] + mock.expect :insert_routine, random_routine_gapi("my_dataset", "my_routine"), ["my-project", "my_dataset", Google::Apis::BigqueryV2::Routine] + end + end + # Google::Cloud::Bigquery::Dataset#create_table@Or the table's schema can be configured with the block. # Google::Cloud::Bigquery::Dataset#create_table@The table's schema fields can be passed as an argument. # Google::Cloud::Bigquery::Dataset#create_table@You can also pass name and description options. @@ -293,6 +307,28 @@ def mock_storage end end + doctest.before "Google::Cloud::Bigquery::Dataset#routine" do + mock_bigquery do |mock| + mock.expect :get_dataset, dataset_full_gapi, ["my-project", "my_dataset"] + mock.expect :get_routine, random_routine_gapi("my_dataset", "my_routine"), ["my-project", "my_dataset", "my_routine"] + end + end + + # Google::Cloud::Bigquery::Dataset#routines@Retrieve all routines: (See {Routine::List#all}) + doctest.before "Google::Cloud::Bigquery::Dataset#routines" do + mock_bigquery do |mock| + mock.expect :get_dataset, dataset_full_gapi, ["my-project", "my_dataset"] + mock.expect :list_routines, list_routines_gapi("my_dataset"), ["my-project", "my_dataset", Hash] + mock.expect :get_routine, random_routine_gapi("my_dataset", "my_routine"), [String, String, String] + mock.expect :get_routine, random_routine_gapi("my_dataset", "my_routine"), [String, String, String] + mock.expect :get_routine, random_routine_gapi("my_dataset", "my_routine"), [String, String, String] + mock.expect :get_routine, random_routine_gapi("my_dataset", "my_routine"), [String, String, String] + mock.expect :get_routine, random_routine_gapi("my_dataset", "my_routine"), [String, String, String] + mock.expect :get_routine, random_routine_gapi("my_dataset", "my_routine"), [String, String, String] + mock.expect :get_routine, random_routine_gapi("my_dataset", "my_routine"), [String, String, String] + end + end + doctest.before "Google::Cloud::Bigquery::Dataset#load" do mock_bigquery do |mock| mock.expect :get_dataset, dataset_full_gapi, ["my-project", "my_dataset"] @@ -679,6 +715,48 @@ def mock_storage end end + doctest.before "Google::Cloud::Bigquery::Routine" do + mock_bigquery do |mock| + mock.expect :get_dataset, dataset_full_gapi, [String, String] + mock.expect :insert_routine, random_routine_gapi("my_dataset", "my_routine"), ["my-project", "my_dataset", Google::Apis::BigqueryV2::Routine] + mock.expect :get_routine, random_routine_gapi("my_dataset", "my_routine"), [String, String, String] + mock.expect :get_routine, random_routine_gapi("my_dataset", "my_routine"), [String, String, String] + mock.expect :get_routine, random_routine_gapi("my_dataset", "my_routine"), [String, String, String] + mock.expect :get_routine, random_routine_gapi("my_dataset", "my_routine"), [String, String, String] + mock.expect :get_routine, random_routine_gapi("my_dataset", "my_routine"), [String, String, String] + mock.expect :list_routines, list_routines_gapi("my_dataset"), [String, String, Hash] + mock.expect :update_routine, random_routine_gapi("my_dataset", "my_routine"), [String, String, String, Object, Hash] + mock.expect :delete_routine, random_routine_gapi("my_dataset", "my_routine"), [String, String, String] + end + end + + doctest.before "Google::Cloud::Bigquery::Routine#resource_full?" do + mock_bigquery do |mock| + mock.expect :get_dataset, dataset_full_gapi, ["my-project", "my_dataset"] + mock.expect :get_routine, random_routine_gapi("my_dataset", "my_routine"), ["my-project", "my_dataset", "my_routine"] + end + end + + doctest.before "Google::Cloud::Bigquery::Routine#resource_partial?" do + mock_bigquery do |mock| + mock.expect :get_dataset, dataset_full_gapi, ["my-project", "my_dataset"] + mock.expect :list_routines, list_routines_gapi("my_dataset"), ["my-project", "my_dataset", Hash] + mock.expect :get_routine, random_routine_gapi("my_dataset", "my_routine"), ["my-project", "my_dataset", "my_routine"] + end + end + + doctest.before "Google::Cloud::Bigquery::Routine::List" do + mock_bigquery do |mock| + mock.expect :get_dataset, dataset_full_gapi, [String, String] + mock.expect :get_routine, random_routine_gapi("my_dataset", "my_routine"), [String, String, String] + mock.expect :get_routine, random_routine_gapi("my_dataset", "my_routine"), [String, String, String] + mock.expect :get_routine, random_routine_gapi("my_dataset", "my_routine"), [String, String, String] + mock.expect :get_routine, random_routine_gapi("my_dataset", "my_routine"), [String, String, String] + mock.expect :get_routine, random_routine_gapi("my_dataset", "my_routine"), [String, String, String] + mock.expect :list_routines, list_routines_gapi("my_dataset"), [String, String, Hash] + end + end + # Google::Cloud::Bigquery::Schema#record doctest.before "Google::Cloud::Bigquery::Schema" do mock_bigquery do |mock| @@ -721,6 +799,13 @@ def mock_storage end end + doctest.before "Google::Cloud::Bigquery::StandardSql" do + mock_bigquery do |mock| + mock.expect :get_dataset, dataset_full_gapi, ["my-project", "my_dataset"] + mock.expect :insert_routine, random_routine_gapi("my_dataset", "my_routine"), ["my-project", "my_dataset", Google::Apis::BigqueryV2::Routine] + end + end + doctest.before "Google::Cloud::Bigquery::Table" do mock_bigquery do |mock| mock.expect :get_dataset, dataset_full_gapi, ["my-project", "my_dataset"] @@ -1045,7 +1130,7 @@ def random_dataset_hash project = "my-project", id = nil, name = nil, descriptio } end -def random_dataset_small_hash project = "my-project", id = nil, name = nil +def random_dataset_partial_hash project = "my-project", id = nil, name = nil id ||= "my_dataset" name ||= "My Dataset" @@ -1061,7 +1146,7 @@ def random_dataset_small_hash project = "my-project", id = nil, name = nil end def list_datasets_gapi count = 2, token = nil - datasets = count.times.map { random_dataset_small_hash } + datasets = count.times.map { random_dataset_partial_hash } hash = {"kind"=>"bigquery#datasetList", "datasets"=>datasets} hash["nextPageToken"] = token unless token.nil? Google::Apis::BigqueryV2::DatasetList.from_json hash.to_json @@ -1120,7 +1205,7 @@ def table_full_hash project = "my-project", dataset = "my_dataset", id = nil, na } end -def random_table_small_hash project = "my-project", dataset = "my_dataset", id = nil, name = nil +def random_table_partial_hash project = "my-project", dataset = "my_dataset", id = nil, name = nil id ||= "my_table" name ||= "Table Name" @@ -1156,7 +1241,7 @@ def table_data_gapi token: "token1234567890" end def list_tables_gapi project = "my-project", dataset = "my_dataset", count = 2, token = nil, total = nil - tables = count.times.map { random_table_small_hash(dataset) } + tables = count.times.map { random_table_partial_hash(dataset) } hash = {"kind" => "bigquery#tableList", "tables" => tables, "totalItems" => (total || count)} hash["nextPageToken"] = token unless token.nil? @@ -1258,6 +1343,61 @@ def table_data_hash token: "token1234567890" } end +def random_routine_hash dataset, id = nil, project: "my-project", etag: "etag123456789", + creation_time: time_millis, last_modified_time: time_millis + id ||= "my_routine" + + h = { + kind: "bigquery#routine", + id: "#{project}:#{dataset}.#{id}", + selfLink: "http://googleapi/bigquery/v2/projects/#{project}/datasets/#{dataset}/routines/#{id}", + routineReference: { + projectId: project, + datasetId: dataset, + routineId: id + }, + routineType: "SCALAR_FUNCTION", + language: "SQL", + arguments: [{ dataType: { typeKind: "INT64" }, name: "x", argumentKind: nil, mode: nil }], + returnType: { typeKind: "INT64" }, + importedLibraries: ["gs://cloud-samples-data/bigquery/udfs/max-value.js"], + definitionBody: "x * 3", + description: "My routine description" + } + h[:etag] = etag if etag + h[:creationTime] = creation_time if creation_time + h[:lastModifiedTime] = last_modified_time if last_modified_time + h +end + +def random_routine_partial_hash dataset, id + # List representation: etag, routineReference, routineType, creationTime, lastModifiedTime and language. + { + etag: "etag123456789", + routineReference: { + projectId: "my-project", + datasetId: dataset, + routineId: id + }, + routineType: "SCALAR_FUNCTION", + creationTime: time_millis, + lastModifiedTime: time_millis, + language: "SQL" + } +end + +def list_routines_gapi dataset, count = 2, token = nil + routines = count.times.map { |i| random_routine_partial_hash dataset, "my_routine" } + hash = { "kind"=>"bigquery#routineList", "routines" => routines } + hash["nextPageToken"] = token unless token.nil? + Google::Apis::BigqueryV2::ListRoutinesResponse.from_json hash.to_json +end + +def random_routine_gapi dataset, id = nil, project: nil + json = random_routine_hash(dataset, id, project: project).to_json + Google::Apis::BigqueryV2::Routine.from_json json +end + def query_data_gapi token: "token1234567890" Google::Apis::BigqueryV2::QueryResponse.from_json query_data_hash(token: token).to_json end diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/data_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/data_test.rb index 089bdfdd7992..2009a7730a1b 100644 --- a/google-cloud-bigquery/test/google/cloud/bigquery/data_test.rb +++ b/google-cloud-bigquery/test/google/cloud/bigquery/data_test.rb @@ -93,6 +93,7 @@ data.ddl?.must_equal false data.dml?.must_equal false data.ddl_operation_performed.must_be :nil? + data.ddl_target_routine.must_be :nil? data.ddl_target_table.must_be :nil? data.num_dml_affected_rows.must_be :nil? end diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/dataset/routine_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/dataset/routine_test.rb new file mode 100644 index 000000000000..7b44968635c1 --- /dev/null +++ b/google-cloud-bigquery/test/google/cloud/bigquery/dataset/routine_test.rb @@ -0,0 +1,114 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require "helper" + +describe Google::Cloud::Bigquery::Dataset, :routine, :mock_bigquery do + let(:dataset_id) { "my_dataset" } + let(:dataset_hash) { random_dataset_hash dataset_id } + let(:dataset_gapi) { Google::Apis::BigqueryV2::Dataset.from_json dataset_hash.to_json } + let(:dataset) { Google::Cloud::Bigquery::Dataset.from_gapi dataset_gapi, bigquery.service } + let(:routine_id) { "my-routine-id" } + let(:routine_hash) { random_routine_hash dataset_id, routine_id } + let(:routine_gapi) { Google::Apis::BigqueryV2::Routine.from_json routine_hash.to_json } + let(:routine_insert_hash) { random_routine_hash dataset_id, routine_id, etag: nil, creation_time: nil, last_modified_time: nil } + let(:routine_insert_gapi) { Google::Apis::BigqueryV2::Routine.from_json routine_insert_hash.to_json } + + it "creates a routine" do + mock = Minitest::Mock.new + insert_routine = Google::Apis::BigqueryV2::Routine.new( + routine_reference: Google::Apis::BigqueryV2::RoutineReference.new( + project_id: project, dataset_id: dataset_id, routine_id: routine_id)) + return_routine = insert_routine.dup + mock.expect :insert_routine, return_routine, [project, dataset_id, insert_routine] + dataset.service.mocked_service = mock + + routine = dataset.create_routine routine_id + + mock.verify + + routine.must_be_kind_of Google::Cloud::Bigquery::Routine + routine.routine_id.must_equal routine_id + end + + it "creates a routine with attributes in a block" do + mock = Minitest::Mock.new + mock.expect :insert_routine, routine_gapi, [project, dataset_id, routine_insert_gapi] + dataset.service.mocked_service = mock + + routine = dataset.create_routine routine_id do |r| + r.routine_type = "SCALAR_FUNCTION" + r.language = "SQL" + r.arguments = [ + Google::Cloud::Bigquery::Argument.new( + name: "arr", + argument_kind: "FIXED_TYPE", + mode: "IN", + data_type: Google::Cloud::Bigquery::StandardSql::DataType.new( + type_kind: "ARRAY", + array_element_type: Google::Cloud::Bigquery::StandardSql::DataType.new( + type_kind: "STRUCT", + struct_type: Google::Cloud::Bigquery::StandardSql::StructType.new( + fields: [ + Google::Cloud::Bigquery::StandardSql::Field.new( + name: "my-struct-name", + type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "STRING") + ), + Google::Cloud::Bigquery::StandardSql::Field.new( + name: "my-struct-val", + type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "INT64") + ) + ] + ) + ) + ) + ), + Google::Cloud::Bigquery::Argument.new( + name: "out", + argument_kind: "ANY_TYPE", + mode: "OUT", + data_type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "STRING") + ) + ] + r.return_type = "INT64" + r.imported_libraries = ["gs://cloud-samples-data/bigquery/udfs/max-value.js"] + r.body = "x * 3" + r.description = "This is my routine" + expect { r.update }.must_raise RuntimeError + expect { r.delete }.must_raise RuntimeError + expect { r.reload! }.must_raise RuntimeError + expect { r.refresh! }.must_raise RuntimeError + end + + mock.verify + + routine.must_be_kind_of Google::Cloud::Bigquery::Routine + routine.routine_id.must_equal routine_id + end + + it "finds a routine" do + found_routine_id = "found_routine" + + mock = Minitest::Mock.new + mock.expect :get_routine, random_routine_gapi(dataset.dataset_id, found_routine_id), [project, dataset.dataset_id, found_routine_id] + dataset.service.mocked_service = mock + + routine = dataset.routine found_routine_id + + mock.verify + + routine.must_be_kind_of Google::Cloud::Bigquery::Routine + routine.routine_id.must_equal found_routine_id + end +end diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/dataset/routines_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/dataset/routines_test.rb new file mode 100644 index 000000000000..9bbd1b782585 --- /dev/null +++ b/google-cloud-bigquery/test/google/cloud/bigquery/dataset/routines_test.rb @@ -0,0 +1,238 @@ + # Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require "helper" + +describe Google::Cloud::Bigquery::Dataset, :routines, :mock_bigquery do + let(:dataset_hash) { random_dataset_hash } + let(:dataset_gapi) { Google::Apis::BigqueryV2::Dataset.from_json dataset_hash.to_json } + let(:dataset) { Google::Cloud::Bigquery::Dataset.from_gapi dataset_gapi, bigquery.service } + let(:filter) { "routineType:SCALAR_FUNCTION" } + + it "lists routines" do + mock = Minitest::Mock.new + mock.expect :list_routines, list_routines_gapi(dataset.dataset_id, 3), + [project, dataset.dataset_id, max_results: nil, page_token: nil, filter: nil] + dataset.service.mocked_service = mock + + routines = dataset.routines + + mock.verify + + routines.size.must_equal 3 + routines.each { |ds| ds.must_be_kind_of Google::Cloud::Bigquery::Routine } + end + + it "lists routines with max set" do + mock = Minitest::Mock.new + mock.expect :list_routines, list_routines_gapi(dataset.dataset_id, 3, "next_page_token"), + [project, dataset.dataset_id, max_results: 3, page_token: nil, filter: nil] + dataset.service.mocked_service = mock + + routines = dataset.routines max: 3 + + mock.verify + + routines.count.must_equal 3 + routines.each { |ds| ds.must_be_kind_of Google::Cloud::Bigquery::Routine } + routines.token.wont_be :nil? + routines.token.must_equal "next_page_token" + end + + it "lists routines with filter set" do + mock = Minitest::Mock.new + mock.expect :list_routines, list_routines_gapi(dataset.dataset_id, 3, "next_page_token"), + [project, dataset.dataset_id, max_results: nil, page_token: nil, filter: filter] + dataset.service.mocked_service = mock + + routines = dataset.routines filter: filter + + mock.verify + + routines.count.must_equal 3 + routines.each { |ds| ds.must_be_kind_of Google::Cloud::Bigquery::Routine } + routines.token.wont_be :nil? + routines.token.must_equal "next_page_token" + end + + it "paginates routines" do + mock = Minitest::Mock.new + mock.expect :list_routines, list_routines_gapi(dataset.dataset_id, 3, "next_page_token"), + [project, dataset.dataset_id, max_results: nil, page_token: nil, filter: nil] + mock.expect :list_routines, list_routines_gapi(dataset.dataset_id, 2, nil), + [project, dataset.dataset_id, max_results: nil, page_token: "next_page_token", filter: nil] + dataset.service.mocked_service = mock + + first_routines = dataset.routines + second_routines = dataset.routines token: first_routines.token + + mock.verify + + first_routines.count.must_equal 3 + first_routines.each { |ds| ds.must_be_kind_of Google::Cloud::Bigquery::Routine } + first_routines.token.wont_be :nil? + first_routines.token.must_equal "next_page_token" + + second_routines.count.must_equal 2 + second_routines.each { |ds| ds.must_be_kind_of Google::Cloud::Bigquery::Routine } + second_routines.token.must_be :nil? + end + + it "paginates routines with next? and next" do + mock = Minitest::Mock.new + mock.expect :list_routines, list_routines_gapi(dataset.dataset_id, 3, "next_page_token"), + [project, dataset.dataset_id, max_results: nil, page_token: nil, filter: nil] + mock.expect :list_routines, list_routines_gapi(dataset.dataset_id, 2, nil), + [project, dataset.dataset_id, max_results: nil, page_token: "next_page_token", filter: nil] + dataset.service.mocked_service = mock + + first_routines = dataset.routines + second_routines = first_routines.next + + mock.verify + + first_routines.count.must_equal 3 + first_routines.each { |ds| ds.must_be_kind_of Google::Cloud::Bigquery::Routine } + first_routines.token.wont_be :nil? + first_routines.token.must_equal "next_page_token" + + second_routines.count.must_equal 2 + second_routines.each { |ds| ds.must_be_kind_of Google::Cloud::Bigquery::Routine } + second_routines.token.must_be :nil? + end + + it "paginates routines with next? and next and max" do + mock = Minitest::Mock.new + mock.expect :list_routines, list_routines_gapi(dataset.dataset_id, 3, "next_page_token"), + [project, dataset.dataset_id, max_results: 3, page_token: nil, filter: nil] + mock.expect :list_routines, list_routines_gapi(dataset.dataset_id, 2, nil), + [project, dataset.dataset_id, max_results: 3, page_token: "next_page_token", filter: nil] + dataset.service.mocked_service = mock + + first_routines = dataset.routines max: 3 + second_routines = first_routines.next + + mock.verify + + first_routines.count.must_equal 3 + first_routines.each { |ds| ds.must_be_kind_of Google::Cloud::Bigquery::Routine } + first_routines.next?.must_equal true + + second_routines.count.must_equal 2 + second_routines.each { |ds| ds.must_be_kind_of Google::Cloud::Bigquery::Routine } + second_routines.next?.must_equal false + end + + it "paginates routines with next? and next and filter" do + mock = Minitest::Mock.new + mock.expect :list_routines, list_routines_gapi(dataset.dataset_id, 3, "next_page_token"), + [project, dataset.dataset_id, max_results: nil, page_token: nil, filter: filter] + mock.expect :list_routines, list_routines_gapi(dataset.dataset_id, 2, nil), + [project, dataset.dataset_id, max_results: nil, page_token: "next_page_token", filter: filter] + dataset.service.mocked_service = mock + + first_routines = dataset.routines filter: filter + second_routines = first_routines.next + + mock.verify + + first_routines.count.must_equal 3 + first_routines.each { |ds| ds.must_be_kind_of Google::Cloud::Bigquery::Routine } + first_routines.next?.must_equal true + + second_routines.count.must_equal 2 + second_routines.each { |ds| ds.must_be_kind_of Google::Cloud::Bigquery::Routine } + second_routines.next?.must_equal false + end + + it "paginates routines with all" do + mock = Minitest::Mock.new + mock.expect :list_routines, list_routines_gapi(dataset.dataset_id, 3, "next_page_token"), + [project, dataset.dataset_id, max_results: nil, page_token: nil, filter: nil] + mock.expect :list_routines, list_routines_gapi(dataset.dataset_id, 2, nil), + [project, dataset.dataset_id, max_results: nil, page_token: "next_page_token", filter: nil] + dataset.service.mocked_service = mock + + routines = dataset.routines.all.to_a + + mock.verify + + routines.count.must_equal 5 + routines.each { |ds| ds.must_be_kind_of Google::Cloud::Bigquery::Routine } + end + + it "paginates routines with all and max" do + mock = Minitest::Mock.new + mock.expect :list_routines, list_routines_gapi(dataset.dataset_id, 3, "next_page_token"), + [project, dataset.dataset_id, max_results: 3, page_token: nil, filter: nil] + mock.expect :list_routines, list_routines_gapi(dataset.dataset_id, 2, nil), + [project, dataset.dataset_id, max_results: 3, page_token: "next_page_token", filter: nil] + dataset.service.mocked_service = mock + + routines = dataset.routines(max: 3).all.to_a + + mock.verify + + routines.count.must_equal 5 + routines.each { |ds| ds.must_be_kind_of Google::Cloud::Bigquery::Routine } + end + + it "paginates routines with all and filter" do + mock = Minitest::Mock.new + mock.expect :list_routines, list_routines_gapi(dataset.dataset_id, 3, "next_page_token"), + [project, dataset.dataset_id, max_results: nil, page_token: nil, filter: filter] + mock.expect :list_routines, list_routines_gapi(dataset.dataset_id, 2, nil), + [project, dataset.dataset_id, max_results: nil, page_token: "next_page_token", filter: filter] + dataset.service.mocked_service = mock + + routines = dataset.routines(filter: filter).all.to_a + + mock.verify + + routines.count.must_equal 5 + routines.each { |ds| ds.must_be_kind_of Google::Cloud::Bigquery::Routine } + end + + it "iterates routines with all using Enumerator" do + mock = Minitest::Mock.new + mock.expect :list_routines, list_routines_gapi(dataset.dataset_id, 3, "next_page_token"), + [project, dataset.dataset_id, max_results: nil, page_token: nil, filter: nil] + mock.expect :list_routines, list_routines_gapi(dataset.dataset_id, 3, "second_page_token"), + [project, dataset.dataset_id, max_results: nil, page_token: "next_page_token", filter: nil] + dataset.service.mocked_service = mock + + routines = dataset.routines.all.take(5) + + mock.verify + + routines.count.must_equal 5 + routines.each { |ds| ds.must_be_kind_of Google::Cloud::Bigquery::Routine } + end + + it "iterates routines with all with request_limit set" do + mock = Minitest::Mock.new + mock.expect :list_routines, list_routines_gapi(dataset.dataset_id, 3, "next_page_token"), + [project, dataset.dataset_id, max_results: nil, page_token: nil, filter: nil] + mock.expect :list_routines, list_routines_gapi(dataset.dataset_id, 3, "second_page_token"), + [project, dataset.dataset_id, max_results: nil, page_token: "next_page_token", filter: nil] + dataset.service.mocked_service = mock + + routines = dataset.routines.all(request_limit: 1).to_a + + mock.verify + + routines.count.must_equal 6 + routines.each { |ds| ds.must_be_kind_of Google::Cloud::Bigquery::Routine } + end +end diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/dataset_attributes_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/dataset_attributes_test.rb index 26574da4c8e1..11822a64e7c3 100644 --- a/google-cloud-bigquery/test/google/cloud/bigquery/dataset_attributes_test.rb +++ b/google-cloud-bigquery/test/google/cloud/bigquery/dataset_attributes_test.rb @@ -22,7 +22,7 @@ let(:dataset_name) { "My Dataset" } let(:description) { "This is my dataset" } let(:default_expiration) { "999" } # String per google/google-api-ruby-client#439 - let(:dataset_gapi) { Google::Apis::BigqueryV2::DatasetList::Dataset.from_json random_dataset_small_hash(dataset_id, dataset_name).to_json } + let(:dataset_gapi) { Google::Apis::BigqueryV2::DatasetList::Dataset.from_json random_dataset_partial_hash(dataset_id, dataset_name).to_json } let(:dataset_full_json) { random_dataset_hash(dataset_id, dataset_name, description, default_expiration).to_json } let(:dataset_full_gapi) { Google::Apis::BigqueryV2::Dataset.from_json dataset_full_json } let(:dataset) { Google::Cloud::Bigquery::Dataset.from_gapi dataset_gapi, diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/dataset_load_job_schema_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/dataset_load_job_schema_test.rb index b65d4ce6fffd..2f72cb82f2fc 100644 --- a/google-cloud-bigquery/test/google/cloud/bigquery/dataset_load_job_schema_test.rb +++ b/google-cloud-bigquery/test/google/cloud/bigquery/dataset_load_job_schema_test.rb @@ -101,6 +101,11 @@ def storage_file path = nil job.time_partitioning_expiration = 86_400 job.time_partitioning_require_filter = true job.clustering_fields = clustering_fields + expect { job.cancel }.must_raise RuntimeError + expect { job.rerun! }.must_raise RuntimeError + expect { job.reload! }.must_raise RuntimeError + expect { job.refresh! }.must_raise RuntimeError + expect { job.wait_until_done! }.must_raise RuntimeError end job.must_be_kind_of Google::Cloud::Bigquery::LoadJob diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/dataset_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/dataset_test.rb index f7b4775cc202..608f239b4cf0 100644 --- a/google-cloud-bigquery/test/google/cloud/bigquery/dataset_test.rb +++ b/google-cloud-bigquery/test/google/cloud/bigquery/dataset_test.rb @@ -186,6 +186,21 @@ table = dataset.create_table table_id do |t| t.name = table_name t.description = table_description + expect { t.data }.must_raise RuntimeError + expect { t.copy_job }.must_raise RuntimeError + expect { t.copy }.must_raise RuntimeError + expect { t.extract_job }.must_raise RuntimeError + expect { t.extract }.must_raise RuntimeError + expect { t.load_job }.must_raise RuntimeError + expect { t.load }.must_raise RuntimeError + expect { t.insert }.must_raise RuntimeError + expect { t.insert_async }.must_raise RuntimeError + expect { t.delete }.must_raise RuntimeError + expect { t.query_job }.must_raise RuntimeError + expect { t.query }.must_raise RuntimeError + expect { t.external }.must_raise RuntimeError + expect { t.reload! }.must_raise RuntimeError + expect { t.refresh! }.must_raise RuntimeError end mock.verify diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/project_copy_job_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/project_copy_job_test.rb index 5ee5af957787..e9cbc92e3173 100644 --- a/google-cloud-bigquery/test/google/cloud/bigquery/project_copy_job_test.rb +++ b/google-cloud-bigquery/test/google/cloud/bigquery/project_copy_job_test.rb @@ -219,6 +219,11 @@ job = bigquery.copy_job source_table, target_table do |j| j.location = region + expect { j.cancel }.must_raise RuntimeError + expect { j.rerun! }.must_raise RuntimeError + expect { j.reload! }.must_raise RuntimeError + expect { j.refresh! }.must_raise RuntimeError + expect { j.wait_until_done! }.must_raise RuntimeError end mock.verify diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/project_extract_job_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/project_extract_job_test.rb index fb755150372b..a9ef1518d961 100644 --- a/google-cloud-bigquery/test/google/cloud/bigquery/project_extract_job_test.rb +++ b/google-cloud-bigquery/test/google/cloud/bigquery/project_extract_job_test.rb @@ -194,6 +194,11 @@ job = bigquery.extract_job table, "#{extract_url}.avro" do |j| j.use_avro_logical_types = true + expect { j.cancel }.must_raise RuntimeError + expect { j.rerun! }.must_raise RuntimeError + expect { j.reload! }.must_raise RuntimeError + expect { j.refresh! }.must_raise RuntimeError + expect { j.wait_until_done! }.must_raise RuntimeError end mock.verify diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/project_query_job_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/project_query_job_test.rb index 1927e5c069d7..e68849912db6 100644 --- a/google-cloud-bigquery/test/google/cloud/bigquery/project_query_job_test.rb +++ b/google-cloud-bigquery/test/google/cloud/bigquery/project_query_job_test.rb @@ -48,6 +48,7 @@ job.ddl?.must_equal false job.dml?.must_equal false job.ddl_operation_performed.must_be :nil? + job.ddl_target_routine.must_be :nil? job.ddl_target_table.must_be :nil? job.num_dml_affected_rows.must_be :nil? end diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/project_query_job_updater_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/project_query_job_updater_test.rb index 7cff59f7a6be..1132f9c76417 100644 --- a/google-cloud-bigquery/test/google/cloud/bigquery/project_query_job_updater_test.rb +++ b/google-cloud-bigquery/test/google/cloud/bigquery/project_query_job_updater_test.rb @@ -76,6 +76,11 @@ job = bigquery.query_job query do |j| j.priority = :batch j.cache = false + expect { j.cancel }.must_raise RuntimeError + expect { j.rerun! }.must_raise RuntimeError + expect { j.reload! }.must_raise RuntimeError + expect { j.refresh! }.must_raise RuntimeError + expect { j.wait_until_done! }.must_raise RuntimeError end mock.verify diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/project_query_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/project_query_test.rb index dcf91449e0f0..2789fcd8d74a 100644 --- a/google-cloud-bigquery/test/google/cloud/bigquery/project_query_test.rb +++ b/google-cloud-bigquery/test/google/cloud/bigquery/project_query_test.rb @@ -69,7 +69,7 @@ bigquery.service.mocked_service = mock job_gapi = query_job_gapi ddl_query, location: nil - resp_gapi = query_job_resp_gapi ddl_query, job_id: job_id, target_table: true, statement_type: "CREATE_TABLE", ddl_operation_performed: "CREATE" + resp_gapi = query_job_resp_gapi ddl_query, job_id: job_id, target_routine: true, target_table: true, statement_type: "CREATE_TABLE", ddl_operation_performed: "CREATE" mock.expect :insert_job, resp_gapi, [project, job_gapi] data = bigquery.query ddl_query @@ -85,6 +85,8 @@ data.ddl_operation_performed.must_equal "CREATE" data.ddl_target_table.wont_be :nil? data.num_dml_affected_rows.must_be :nil? + # in real life this example does not create a routine, but test the attribute here anyway + data.ddl_target_routine.wont_be :nil? end it "executes a DML statement" do diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/project_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/project_test.rb index ada9b62435b3..f6869cbededa 100644 --- a/google-cloud-bigquery/test/google/cloud/bigquery/project_test.rb +++ b/google-cloud-bigquery/test/google/cloud/bigquery/project_test.rb @@ -142,6 +142,23 @@ acl.add_writer_user "writers@example.com" assert acl.writer_user? "writers@example.com" end + expect { ds.delete }.must_raise RuntimeError + expect { ds.create_table }.must_raise RuntimeError + expect { ds.create_view }.must_raise RuntimeError + expect { ds.table }.must_raise RuntimeError + expect { ds.tables }.must_raise RuntimeError + expect { ds.model }.must_raise RuntimeError + expect { ds.models }.must_raise RuntimeError + expect { ds.create_routine }.must_raise RuntimeError + expect { ds.routine }.must_raise RuntimeError + expect { ds.routines }.must_raise RuntimeError + expect { ds.query_job }.must_raise RuntimeError + expect { ds.query }.must_raise RuntimeError + expect { ds.external }.must_raise RuntimeError + expect { ds.load_job }.must_raise RuntimeError + expect { ds.load }.must_raise RuntimeError + expect { ds.reload! }.must_raise RuntimeError + expect { ds.refresh! }.must_raise RuntimeError end mock.verify diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/query_job_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/query_job_test.rb index a0f9e4c153ec..7553ff81e8f6 100644 --- a/google-cloud-bigquery/test/google/cloud/bigquery/query_job_test.rb +++ b/google-cloud-bigquery/test/google/cloud/bigquery/query_job_test.rb @@ -17,7 +17,7 @@ require "uri" describe Google::Cloud::Bigquery::QueryJob, :mock_bigquery do - let(:job_gapi) { query_job_gapi target_table: true, statement_type: "CREATE_TABLE", num_dml_affected_rows: 50, ddl_operation_performed: "CREATE" } + let(:job_gapi) { query_job_gapi target_routine: true, target_table: true, statement_type: "CREATE_TABLE", num_dml_affected_rows: 50, ddl_operation_performed: "CREATE" } let(:job) { Google::Cloud::Bigquery::Job.from_gapi job_gapi, bigquery.service } let(:job_id) { job.job_id } @@ -64,6 +64,11 @@ job.statement_type.must_equal "CREATE_TABLE" job.ddl?.must_equal true job.dml?.must_equal false + # in real life this example does not create a routine, but test the attribute here anyway + job.ddl_target_routine.must_be_kind_of Google::Cloud::Bigquery::Routine + job.ddl_target_routine.project_id.must_equal "target_project_id" + job.ddl_target_routine.dataset_id.must_equal "target_dataset_id" + job.ddl_target_routine.routine_id.must_equal "target_routine_id" end it "knows its query config" do @@ -112,9 +117,10 @@ job.udfs.last.must_equal "gs://my-bucket/my-lib.js" end - def query_job_gapi target_table: false, statement_type: nil, num_dml_affected_rows: nil, ddl_operation_performed: nil + def query_job_gapi target_routine: false, target_table: false, statement_type: nil, num_dml_affected_rows: nil, ddl_operation_performed: nil gapi = Google::Apis::BigqueryV2::Job.from_json query_job_hash.to_json - gapi.statistics.query = statistics_query_gapi target_table: target_table, + gapi.statistics.query = statistics_query_gapi target_routine: target_routine, + target_table: target_table, statement_type: statement_type, num_dml_affected_rows: num_dml_affected_rows, ddl_operation_performed: ddl_operation_performed diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/routine/partial/routine_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/routine/partial/routine_test.rb new file mode 100644 index 000000000000..a118defc21fc --- /dev/null +++ b/google-cloud-bigquery/test/google/cloud/bigquery/routine/partial/routine_test.rb @@ -0,0 +1,355 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require "helper" +require "json" +require "uri" + +describe Google::Cloud::Bigquery::Routine, :mock_bigquery do + let(:dataset) { "my_dataset" } + let(:routine_id) { "my_routine" } + let(:etag) { "etag123456789" } + let(:routine_type) { "SCALAR_FUNCTION" } + let(:now) { ::Time.now } + let(:language) { "SQL" } + let(:arguments) do + [ + Google::Cloud::Bigquery::Argument.new( + data_type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "INT64"), + name: "x" + ) + ] + end + let(:return_type) { Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: "INT64" } + let(:imported_libraries) { ["gs://cloud-samples-data/bigquery/udfs/max-value.js"] } + let(:body) { "x * 3" } + let(:description) { "This is my routine" } + let(:new_routine_type) { "PROCEDURE" } + let(:new_language) { "JAVASCRIPT" } + let(:new_arguments_gapi) do + [ + Google::Apis::BigqueryV2::Argument.new( + data_type: Google::Apis::BigqueryV2::StandardSqlDataType.new(type_kind: "INT64"), + name: "x" + ), + Google::Apis::BigqueryV2::Argument.new( + data_type: Google::Apis::BigqueryV2::StandardSqlDataType.new(type_kind: "STRING"), + name: "y" + ), + Google::Apis::BigqueryV2::Argument.new( + data_type: Google::Apis::BigqueryV2::StandardSqlDataType.new(type_kind: "BOOL"), + name: "z" + ) + ] + end + let(:new_arguments) do + [ + Google::Cloud::Bigquery::Argument.new( + data_type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "INT64"), + name: "x" + ), + Google::Cloud::Bigquery::Argument.new( + data_type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "STRING"), + name: "y" + ), + Google::Cloud::Bigquery::Argument.new( + data_type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "BOOL"), + name: "z" + ) + ] + end + let(:new_return_type) { Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: "STRING" } + let(:new_imported_libraries) { ["gs://cloud-samples-data/bigquery/udfs/max-value-2.js"] } + let(:new_body) { "x * 4" } + let(:new_description) { "This is my updated routine" } + let(:routine_partial_hash) { random_routine_partial_hash dataset, routine_id } + let(:routine_partial_gapi) { Google::Apis::BigqueryV2::Routine.from_json routine_partial_hash.to_json } + let(:routine_hash) { random_routine_hash dataset, routine_id } + let(:routine_gapi) { Google::Apis::BigqueryV2::Routine.from_json routine_hash.to_json } + let(:routine) { Google::Cloud::Bigquery::Routine.from_gapi routine_partial_gapi, bigquery.service } + + it "knows its attributes" do + routine.routine_id.must_equal routine_id + routine.dataset_id.must_equal dataset + routine.project_id.must_equal project + # routine_ref is private + routine.routine_ref.must_be_kind_of Google::Apis::BigqueryV2::RoutineReference + routine.routine_ref.routine_id.must_equal routine_id + routine.routine_ref.dataset_id.must_equal dataset + routine.routine_ref.project_id.must_equal project + + # Only the following fields are populated: + # etag, routineReference, routineType, creationTime, lastModifiedTime and language + routine.etag.must_equal etag + routine.routine_type.must_equal "SCALAR_FUNCTION" + routine.procedure?.must_equal false + routine.scalar_function?.must_equal true + routine.created_at.must_be_close_to now, 1 + routine.modified_at.must_be_close_to now, 1 + routine.language.must_equal "SQL" + routine.javascript?.must_equal false + routine.sql?.must_equal true + end + + it "can test its existence" do + routine.exists?.must_equal true + end + + it "can test its existence with force to load resource" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + routine.service.mocked_service = mock + + routine.exists?(force: true).must_equal true + + mock.verify + end + + it "can delete itself" do + mock = Minitest::Mock.new + mock.expect :delete_routine, nil, [project, dataset, routine_id] + routine.service.mocked_service = mock + + routine.delete.must_equal true + + routine.exists?.must_equal false + + mock.verify + end + + it "can reload itself" do + mock = Minitest::Mock.new + routine_hash = random_routine_hash dataset, routine_id, description: new_description + mock.expect :get_routine, Google::Apis::BigqueryV2::Routine.from_json(routine_hash.to_json), + [project, dataset, routine_id] + routine.service.mocked_service = mock + + routine.reload! + + mock.verify + + routine.description.must_equal new_description + end + + it "updates its routine_type" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.routine_type = new_routine_type + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.routine_type = new_routine_type + + mock.verify + + routine.routine_type.must_equal new_routine_type + end + + it "updates its language" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.language = new_language + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.language = new_language + + mock.verify + + routine.language.must_equal new_language + end + + it "updates its arguments" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.arguments = new_arguments_gapi + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.arguments = new_arguments + + mock.verify + + routine.arguments.size.must_equal new_arguments.size + end + + it "updates its return_type" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.return_type = Google::Apis::BigqueryV2::StandardSqlDataType.new type_kind: "STRING" + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + new_return_type = Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: "STRING" + + routine.return_type = new_return_type + + mock.verify + + routine.return_type.type_kind.must_equal new_return_type.type_kind + end + + it "updates its return_type with a string" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.return_type = Google::Apis::BigqueryV2::StandardSqlDataType.new type_kind: "STRING" + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.return_type = "STRING" + + mock.verify + + routine.return_type.type_kind.must_equal "STRING" + end + + it "updates its return_type to nil" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.return_type = nil + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + new_return_type = nil + + routine.return_type = new_return_type + + mock.verify + + routine.return_type.must_be :nil? + end + + it "updates its imported_libraries" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.imported_libraries = new_imported_libraries + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.imported_libraries = new_imported_libraries + + mock.verify + + routine.imported_libraries.must_equal new_imported_libraries + end + + it "updates its body" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.definition_body = new_body + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.body = new_body + + mock.verify + + routine.body.must_equal new_body + end + + it "updates its description" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.description = new_description + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.description = new_description + + mock.verify + + routine.description.must_equal new_description + end + + it "updates its attributes in a block" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.routine_type = new_routine_type + updated_routine_gapi.language = new_language + updated_routine_gapi.arguments = new_arguments_gapi + updated_routine_gapi.return_type = Google::Apis::BigqueryV2::StandardSqlDataType.new type_kind: "STRING" + updated_routine_gapi.imported_libraries = new_imported_libraries + updated_routine_gapi.definition_body = new_body + updated_routine_gapi.description = new_description + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.update do |r| + r.routine_type = new_routine_type + r.language = new_language + r.arguments = new_arguments + r.return_type = new_return_type + r.imported_libraries = new_imported_libraries + r.description = new_description + r.body = new_body + r.description = new_description + end + + mock.verify + + routine.routine_type.must_equal new_routine_type + routine.language.must_equal new_language + routine.arguments.size.must_equal new_arguments.size + routine.return_type.type_kind.must_equal new_return_type.type_kind + routine.imported_libraries.must_equal new_imported_libraries + routine.body.must_equal new_body + routine.description.must_equal new_description + end + + it "skips update when no updates are made in a block" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + routine.service.mocked_service = mock + + routine.update do |r| + end + + mock.verify + end + + it "raises from unsupported methods called on the updater" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + routine.service.mocked_service = mock + + routine.update do |r| + expect { r.update }.must_raise RuntimeError + expect { r.delete }.must_raise RuntimeError + expect { r.reload! }.must_raise RuntimeError + expect { r.refresh! }.must_raise RuntimeError + end + + mock.verify + end +end diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/routine/reference/routine_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/routine/reference/routine_test.rb new file mode 100644 index 000000000000..d853994c9fda --- /dev/null +++ b/google-cloud-bigquery/test/google/cloud/bigquery/routine/reference/routine_test.rb @@ -0,0 +1,355 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require "helper" +require "json" +require "uri" + +describe Google::Cloud::Bigquery::Routine, :reference, :mock_bigquery do + let(:dataset) { "my_dataset" } + let(:routine_id) { "my_routine" } + let(:etag) { "etag123456789" } + let(:routine_type) { "SCALAR_FUNCTION" } + let(:language) { "SQL" } + let(:arguments) do + [ + Google::Cloud::Bigquery::Argument.new( + Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: "INT64", + name: "x" + ) + ] + end + let(:return_type) { Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: "INT64" } + let(:imported_libraries) { ["gs://cloud-samples-data/bigquery/udfs/max-value.js"] } + let(:body) { "x * 3" } + let(:description) { "This is my routine" } + let(:new_routine_type) { "PROCEDURE" } + let(:new_language) { "JAVASCRIPT" } + let(:new_arguments_gapi) do + [ + Google::Apis::BigqueryV2::Argument.new( + data_type: Google::Apis::BigqueryV2::StandardSqlDataType.new(type_kind: "INT64"), + name: "x" + ), + Google::Apis::BigqueryV2::Argument.new( + data_type: Google::Apis::BigqueryV2::StandardSqlDataType.new(type_kind: "STRING"), + name: "y" + ), + Google::Apis::BigqueryV2::Argument.new( + data_type: Google::Apis::BigqueryV2::StandardSqlDataType.new(type_kind: "BOOL"), + name: "z" + ) + ] + end + let(:new_arguments) do + [ + Google::Cloud::Bigquery::Argument.new( + data_type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "INT64"), + name: "x" + ), + Google::Cloud::Bigquery::Argument.new( + data_type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "STRING"), + name: "y" + ), + Google::Cloud::Bigquery::Argument.new( + data_type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "BOOL"), + name: "z" + ) + ] + end + let(:new_return_type) { Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: "STRING" } + let(:new_imported_libraries) { ["gs://cloud-samples-data/bigquery/udfs/max-value-2.js"] } + let(:new_body) { "x * 4" } + let(:new_description) { "This is my updated routine" } + let(:routine_hash) { random_routine_hash dataset, routine_id } + let(:routine_gapi) { Google::Apis::BigqueryV2::Routine.from_json routine_hash.to_json } + let(:routine) { Google::Cloud::Bigquery::Routine.new_reference project, dataset, routine_id, bigquery.service } + + it "knows its attributes" do + routine.routine_id.must_equal routine_id + routine.dataset_id.must_equal dataset + routine.project_id.must_equal project + # routine_ref is private + routine.routine_ref.must_be_kind_of Google::Apis::BigqueryV2::RoutineReference + routine.routine_ref.routine_id.must_equal routine_id + routine.routine_ref.dataset_id.must_equal dataset + routine.routine_ref.project_id.must_equal project + + routine.etag.must_be_nil + routine.routine_type.must_be_nil + routine.created_at.must_be_nil + routine.modified_at.must_be_nil + routine.language.must_be_nil + routine.arguments.must_be_nil + routine.return_type.must_be_nil + routine.body.must_be_nil + routine.description.must_be_nil + end + + it "can test its existence" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + routine.service.mocked_service = mock + + routine.exists?.must_equal true + + mock.verify + end + + it "can test its existence with force to load resource" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + routine.service.mocked_service = mock + + routine.exists?(force: true).must_equal true + + mock.verify + end + + it "can delete itself" do + mock = Minitest::Mock.new + mock.expect :delete_routine, nil, [project, dataset, routine_id] + routine.service.mocked_service = mock + + routine.delete.must_equal true + + routine.exists?.must_equal false + + mock.verify + end + + it "can reload itself" do + mock = Minitest::Mock.new + routine_hash = random_routine_hash dataset, routine_id, description: new_description + mock.expect :get_routine, Google::Apis::BigqueryV2::Routine.from_json(routine_hash.to_json), + [project, dataset, routine_id] + routine.service.mocked_service = mock + + routine.description.must_be_nil + routine.reload! + + mock.verify + + routine.description.must_equal new_description + end + + it "updates its routine_type" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.routine_type = new_routine_type + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.routine_type = new_routine_type + + mock.verify + + routine.routine_type.must_equal new_routine_type + end + + it "updates its language" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.language = new_language + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.language = new_language + + mock.verify + + routine.language.must_equal new_language + end + + it "updates its arguments" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.arguments = new_arguments_gapi + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.arguments = new_arguments + + mock.verify + + routine.arguments.size.must_equal new_arguments.size + end + + it "updates its return_type" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.return_type = Google::Apis::BigqueryV2::StandardSqlDataType.new type_kind: "STRING" + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.return_type = new_return_type + + mock.verify + + routine.return_type.type_kind.must_equal new_return_type.type_kind + end + + it "updates its return_type with a string" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.return_type = Google::Apis::BigqueryV2::StandardSqlDataType.new type_kind: "STRING" + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.return_type = "STRING" + + mock.verify + + routine.return_type.type_kind.must_equal new_return_type.type_kind + end + + it "updates its return_type to nil" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.return_type = nil + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + new_return_type = nil + + routine.return_type = new_return_type + + mock.verify + + routine.return_type.must_be :nil? + end + + it "updates its imported_libraries" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.imported_libraries = new_imported_libraries + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.imported_libraries = new_imported_libraries + + mock.verify + + routine.imported_libraries.must_equal new_imported_libraries + end + + it "updates its body" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.definition_body = new_body + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.body = new_body + + mock.verify + + routine.body.must_equal new_body + end + + it "updates its description" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.description = new_description + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.description = new_description + + mock.verify + + routine.description.must_equal new_description + end + + it "updates its attributes in a block" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.routine_type = new_routine_type + updated_routine_gapi.language = new_language + updated_routine_gapi.arguments = new_arguments_gapi + updated_routine_gapi.return_type = Google::Apis::BigqueryV2::StandardSqlDataType.new type_kind: "STRING" + updated_routine_gapi.imported_libraries = new_imported_libraries + updated_routine_gapi.definition_body = new_body + updated_routine_gapi.description = new_description + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.update do |r| + r.routine_type = new_routine_type + r.language = new_language + r.arguments = new_arguments + r.return_type = new_return_type + r.imported_libraries = new_imported_libraries + r.description = new_description + r.body = new_body + r.description = new_description + end + + mock.verify + + routine.routine_type.must_equal new_routine_type + routine.language.must_equal new_language + routine.arguments.size.must_equal new_arguments.size + routine.return_type.type_kind.must_equal new_return_type.type_kind + routine.imported_libraries.must_equal new_imported_libraries + routine.body.must_equal new_body + routine.description.must_equal new_description + end + + it "skips update when no updates are made in a block" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + routine.service.mocked_service = mock + + routine.update do |r| + end + + mock.verify + end + + it "raises from unsupported methods called on the updater" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + routine.service.mocked_service = mock + + routine.update do |r| + expect { r.update }.must_raise RuntimeError + expect { r.delete }.must_raise RuntimeError + expect { r.reload! }.must_raise RuntimeError + expect { r.refresh! }.must_raise RuntimeError + end + + mock.verify + end +end diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/routine/resource/routine_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/routine/resource/routine_test.rb new file mode 100644 index 000000000000..82c24a4aace8 --- /dev/null +++ b/google-cloud-bigquery/test/google/cloud/bigquery/routine/resource/routine_test.rb @@ -0,0 +1,383 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require "helper" +require "json" +require "uri" + +describe Google::Cloud::Bigquery::Routine, :resource, :mock_bigquery do + let(:dataset) { "my_dataset" } + let(:routine_id) { "my_routine" } + let(:etag) { "etag123456789" } + let(:routine_type) { "SCALAR_FUNCTION" } + let(:now) { ::Time.now } + let(:language) { "SQL" } + let(:return_type) { Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: "INT64" } + let(:imported_libraries) { ["gs://cloud-samples-data/bigquery/udfs/max-value.js"] } + let(:body) { "x * 3" } + let(:description) { "This is my routine" } + let(:new_routine_type) { "PROCEDURE" } + let(:new_language) { "JAVASCRIPT" } + let(:new_arguments_gapi) do + [ + Google::Apis::BigqueryV2::Argument.new( + data_type: Google::Apis::BigqueryV2::StandardSqlDataType.new(type_kind: "INT64"), + name: "x" + ), + Google::Apis::BigqueryV2::Argument.new( + data_type: Google::Apis::BigqueryV2::StandardSqlDataType.new(type_kind: "STRING"), + name: "y" + ), + Google::Apis::BigqueryV2::Argument.new( + data_type: Google::Apis::BigqueryV2::StandardSqlDataType.new(type_kind: "BOOL"), + name: "z" + ) + ] + end + let(:new_arguments) do + [ + Google::Cloud::Bigquery::Argument.new( + data_type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "INT64"), + name: "x" + ), + Google::Cloud::Bigquery::Argument.new( + data_type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "STRING"), + name: "y" + ), + Google::Cloud::Bigquery::Argument.new( + data_type: Google::Cloud::Bigquery::StandardSql::DataType.new(type_kind: "BOOL"), + name: "z" + ) + ] + end + let(:new_return_type) { Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: "STRING" } + let(:new_imported_libraries) { ["gs://cloud-samples-data/bigquery/udfs/max-value-2.js"] } + let(:new_body) { "x * 4" } + let(:new_description) { "This is my updated routine" } + let(:routine_hash) { random_routine_hash dataset, routine_id } + let(:routine_gapi) { Google::Apis::BigqueryV2::Routine.from_json routine_hash.to_json } + let(:routine) { Google::Cloud::Bigquery::Routine.from_gapi routine_gapi, bigquery.service } + + it "knows its attributes" do + routine.routine_id.must_equal routine_id + routine.dataset_id.must_equal dataset + routine.project_id.must_equal project + # routine_ref is private + routine.routine_ref.must_be_kind_of Google::Apis::BigqueryV2::RoutineReference + routine.routine_ref.routine_id.must_equal routine_id + routine.routine_ref.dataset_id.must_equal dataset + routine.routine_ref.project_id.must_equal project + + routine.etag.must_equal etag + routine.routine_type.must_equal "SCALAR_FUNCTION" + routine.procedure?.must_equal false + routine.scalar_function?.must_equal true + routine.created_at.must_be_close_to now, 1 + routine.modified_at.must_be_close_to now, 1 + routine.language.must_equal "SQL" + routine.javascript?.must_equal false + routine.sql?.must_equal true + + routine.arguments.must_be_kind_of Array + routine.arguments.must_be :frozen? + routine.arguments.size.must_equal 2 + routine.arguments[0].must_be_kind_of Google::Cloud::Bigquery::Argument + routine.arguments[0].name.must_equal "arr" + routine.arguments[0].argument_kind.must_equal "FIXED_TYPE" + routine.arguments[0].fixed_type?.must_equal true + routine.arguments[0].any_type?.must_equal false + routine.arguments[0].mode.must_equal "IN" + routine.arguments[0].in?.must_equal true + routine.arguments[0].out?.must_equal false + routine.arguments[0].inout?.must_equal false + routine.arguments[0].data_type.must_be_kind_of Google::Cloud::Bigquery::StandardSql::DataType + routine.arguments[0].data_type.type_kind.must_equal "ARRAY" + routine.arguments[0].data_type.array_element_type.must_be_kind_of Google::Cloud::Bigquery::StandardSql::DataType + routine.arguments[0].data_type.array_element_type.type_kind.must_equal "STRUCT" + routine.arguments[0].data_type.array_element_type.struct_type.must_be_kind_of Google::Cloud::Bigquery::StandardSql::StructType + routine.arguments[0].data_type.array_element_type.struct_type.fields.must_be_kind_of Array + routine.arguments[0].data_type.array_element_type.struct_type.fields.must_be :frozen? + routine.arguments[0].data_type.array_element_type.struct_type.fields.size.must_equal 2 + routine.arguments[0].data_type.array_element_type.struct_type.fields[0].must_be_kind_of Google::Cloud::Bigquery::StandardSql::Field + routine.arguments[0].data_type.array_element_type.struct_type.fields[0].name.must_equal "my-struct-name" + routine.arguments[0].data_type.array_element_type.struct_type.fields[0].type.must_be_kind_of Google::Cloud::Bigquery::StandardSql::DataType + routine.arguments[0].data_type.array_element_type.struct_type.fields[0].type.type_kind.must_equal "STRING" + routine.arguments[0].data_type.array_element_type.struct_type.fields[1].name.must_equal "my-struct-val" + routine.arguments[0].data_type.array_element_type.struct_type.fields[1].type.type_kind.must_equal "INT64" + routine.arguments[1].name.must_equal "out" + routine.arguments[1].argument_kind.must_equal "ANY_TYPE" + routine.arguments[1].fixed_type?.must_equal false + routine.arguments[1].any_type?.must_equal true + routine.arguments[1].mode.must_equal "OUT" + routine.arguments[1].in?.must_equal false + routine.arguments[1].out?.must_equal true + routine.arguments[1].inout?.must_equal false + routine.arguments[1].data_type.type_kind.must_equal "STRING" + + routine.return_type.must_be_kind_of Google::Cloud::Bigquery::StandardSql::DataType + routine.return_type.type_kind.must_equal "INT64" + + routine.imported_libraries.must_equal ["gs://cloud-samples-data/bigquery/udfs/max-value.js"] + routine.imported_libraries.must_be :frozen? + routine.body.must_equal "x * 3" + routine.description.must_equal description + end + + it "can test its existence" do + routine.exists?.must_equal true + end + + it "can test its existence with force to load resource" do + mock = Minitest::Mock.new + mock.expect :get_routine, routine_gapi, [routine.project_id, routine.dataset_id, routine.routine_id] + routine.service.mocked_service = mock + + routine.exists?(force: true).must_equal true + + mock.verify + end + + it "can delete itself" do + mock = Minitest::Mock.new + mock.expect :delete_routine, nil, [project, dataset, routine_id] + routine.service.mocked_service = mock + + routine.delete.must_equal true + + routine.exists?.must_equal false + + mock.verify + end + + it "can reload itself" do + new_description = "New description of the routine." + + mock = Minitest::Mock.new + routine_hash = random_routine_hash dataset, routine_id, description: new_description + mock.expect :get_routine, Google::Apis::BigqueryV2::Routine.from_json(routine_hash.to_json), + [project, dataset, routine_id] + routine.service.mocked_service = mock + + routine.description.must_equal description + routine.reload! + + mock.verify + + routine.description.must_equal new_description + end + + it "updates its routine_type" do + routine.routine_type.must_equal routine_type + + mock = Minitest::Mock.new + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.routine_type = new_routine_type + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.routine_type = new_routine_type + + mock.verify + + routine.routine_type.must_equal new_routine_type + end + + it "updates its language" do + routine.language.must_equal language + + mock = Minitest::Mock.new + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.language = new_language + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.language = new_language + + mock.verify + + routine.language.must_equal new_language + end + + it "updates its arguments" do + routine.arguments.size.must_equal routine_gapi.arguments.size + + mock = Minitest::Mock.new + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.arguments = new_arguments_gapi + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.arguments = new_arguments + + mock.verify + + routine.arguments.size.must_equal new_arguments.size + end + + it "updates its return_type" do + routine.return_type.type_kind.must_equal return_type.type_kind + + mock = Minitest::Mock.new + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.return_type = Google::Apis::BigqueryV2::StandardSqlDataType.new type_kind: "STRING" + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.return_type = new_return_type + + mock.verify + + routine.return_type.type_kind.must_equal new_return_type.type_kind + end + + it "updates its return_type with a string" do + routine.return_type.type_kind.must_equal return_type.type_kind + + mock = Minitest::Mock.new + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.return_type = Google::Apis::BigqueryV2::StandardSqlDataType.new type_kind: "STRING" + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.return_type = "STRING" + + mock.verify + + routine.return_type.type_kind.must_equal new_return_type.type_kind + end + + it "updates its return_type to nil" do + routine.return_type.type_kind.must_equal return_type.type_kind + + mock = Minitest::Mock.new + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.return_type = nil + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.return_type = nil + + mock.verify + + routine.return_type.must_be :nil? + end + + it "updates its imported_libraries" do + routine.imported_libraries.must_equal imported_libraries + + mock = Minitest::Mock.new + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.imported_libraries = new_imported_libraries + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.imported_libraries = new_imported_libraries + + mock.verify + + routine.imported_libraries.must_equal new_imported_libraries + end + + it "updates its body" do + routine.body.must_equal body + + mock = Minitest::Mock.new + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.definition_body = new_body + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.body = new_body + + mock.verify + + routine.body.must_equal new_body + end + + it "updates its description" do + routine.description.must_equal description + + mock = Minitest::Mock.new + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.description = new_description + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.description = new_description + + mock.verify + + routine.description.must_equal new_description + end + + it "updates its attributes in a block" do + routine.description.must_equal description + + mock = Minitest::Mock.new + updated_routine_gapi = routine_gapi.dup + updated_routine_gapi.routine_type = new_routine_type + updated_routine_gapi.language = new_language + updated_routine_gapi.arguments = new_arguments_gapi + updated_routine_gapi.return_type = Google::Apis::BigqueryV2::StandardSqlDataType.new type_kind: "STRING" + updated_routine_gapi.imported_libraries = new_imported_libraries + updated_routine_gapi.definition_body = new_body + updated_routine_gapi.description = new_description + mock.expect :update_routine, updated_routine_gapi, + [project, dataset, routine_id, updated_routine_gapi, options: { header: { "If-Match" => etag } }] + routine.service.mocked_service = mock + + routine.update do |r| + r.routine_type = new_routine_type + r.language = new_language + r.arguments = new_arguments + r.return_type = new_return_type + r.imported_libraries = new_imported_libraries + r.body = new_body + r.description = new_description + end + + mock.verify + + routine.routine_type.must_equal new_routine_type + routine.language.must_equal new_language + routine.arguments.size.must_equal new_arguments.size + routine.return_type.type_kind.must_equal new_return_type.type_kind + routine.imported_libraries.must_equal new_imported_libraries + routine.body.must_equal new_body + routine.description.must_equal new_description + end + + it "skips update when no updates are made in a block" do + routine.update do |r| + end + end + + it "raises from unsupported methods called on the updater" do + routine.update do |r| + expect { r.update }.must_raise RuntimeError + expect { r.delete }.must_raise RuntimeError + expect { r.reload! }.must_raise RuntimeError + expect { r.refresh! }.must_raise RuntimeError + end + end +end diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/schema_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/schema_test.rb index 855560080629..2bf0cefca4ac 100644 --- a/google-cloud-bigquery/test/google/cloud/bigquery/schema_test.rb +++ b/google-cloud-bigquery/test/google/cloud/bigquery/schema_test.rb @@ -106,7 +106,7 @@ let(:empty_schema) { Google::Cloud::Bigquery::Schema.from_gapi } let(:kittens_schema_json) do - <<-JSON + <<~JSON [ {"name":"id","type":"INTEGER","mode":"REQUIRED","description":"id description"}, {"name":"breed","type":"STRING","mode":"REQUIRED","description":"breed description"}, diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/standard_sql/array_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/standard_sql/array_test.rb index aa0849e6bb49..d7a15e7bca64 100644 --- a/google-cloud-bigquery/test/google/cloud/bigquery/standard_sql/array_test.rb +++ b/google-cloud-bigquery/test/google/cloud/bigquery/standard_sql/array_test.rb @@ -16,7 +16,7 @@ describe Google::Cloud::Bigquery::StandardSql, :array do it "represents a INT64 Array field" do - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "int_array_col", type: { typeKind: "ARRAY", arrayElementType: { typeKind: "INT64" } } }) + field = array_field "int_array_col", "INT64" field.name.must_equal "int_array_col" @@ -44,7 +44,7 @@ end it "represents a FLOAT64 Array field" do - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "float_array_col", type: { typeKind: "ARRAY", arrayElementType: { typeKind: "FLOAT64" } } }) + field = array_field "float_array_col", "FLOAT64" field.name.must_equal "float_array_col" @@ -72,7 +72,7 @@ end it "represents a NUMERIC Array field" do - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "num_array_col", type: { typeKind: "ARRAY", arrayElementType: { typeKind: "NUMERIC" } } }) + field = array_field "num_array_col", "NUMERIC" field.name.must_equal "num_array_col" @@ -100,7 +100,7 @@ end it "represents a BOOL Array field" do - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "bool_array_col", type: { typeKind: "ARRAY", arrayElementType: { typeKind: "BOOL" } } }) + field = array_field "bool_array_col", "BOOL" field.name.must_equal "bool_array_col" @@ -128,7 +128,7 @@ end it "represents a STRING Array field" do - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "str_array_col", type: { typeKind: "ARRAY", arrayElementType: { typeKind: "STRING" } } }) + field = array_field "str_array_col", "STRING" field.name.must_equal "str_array_col" @@ -156,7 +156,7 @@ end it "represents a BYTES Array field" do - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "bytes_array_col", type: { typeKind: "ARRAY", arrayElementType: { typeKind: "BYTES" } } }) + field = array_field "bytes_array_col", "BYTES" field.name.must_equal "bytes_array_col" @@ -184,7 +184,7 @@ end it "represents a DATE Array field" do - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "date_array_col", type: { typeKind: "ARRAY", arrayElementType: { typeKind: "DATE" } } }) + field = array_field "date_array_col", "DATE" field.name.must_equal "date_array_col" @@ -212,7 +212,7 @@ end it "represents a DATETIME Array field" do - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "datetime_array_col", type: { typeKind: "ARRAY", arrayElementType: { typeKind: "DATETIME" } } }) + field = array_field "datetime_array_col", "DATETIME" field.name.must_equal "datetime_array_col" @@ -240,7 +240,7 @@ end it "represents a GEOGRAPHY Array field" do - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "geo_array_col", type: { typeKind: "ARRAY", arrayElementType: { typeKind: "GEOGRAPHY" } } }) + field = array_field "geo_array_col", "GEOGRAPHY" field.name.must_equal "geo_array_col" @@ -268,7 +268,7 @@ end it "represents a TIME Array field" do - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "time_array_col", type: { typeKind: "ARRAY", arrayElementType: { typeKind: "TIME" } } }) + field = array_field "time_array_col", "TIME" field.name.must_equal "time_array_col" @@ -296,7 +296,7 @@ end it "represents a TIMESTAMP Array field" do - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "ts_array_col", type: { typeKind: "ARRAY", arrayElementType: { typeKind: "TIMESTAMP" } } }) + field = array_field "ts_array_col", "TIMESTAMP" field.name.must_equal "ts_array_col" @@ -322,4 +322,10 @@ field.type.must_be :array? field.type.wont_be :struct? end + + def array_field name, type_kind + array_element_type = Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: type_kind + array_data_type =Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: "ARRAY", array_element_type: array_element_type + Google::Cloud::Bigquery::StandardSql::Field.new name: name, type: array_data_type + end end diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/standard_sql/struct_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/standard_sql/struct_test.rb index a7331d8bcda3..1ea498a19b4c 100644 --- a/google-cloud-bigquery/test/google/cloud/bigquery/standard_sql/struct_test.rb +++ b/google-cloud-bigquery/test/google/cloud/bigquery/standard_sql/struct_test.rb @@ -16,8 +16,11 @@ describe Google::Cloud::Bigquery::StandardSql, :struct do it "represents a simple STRUCT field" do - struct_hash = { fields: [{ name: "int_col", type: { typeKind: "INT64" } }] } - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "struct_col", type: { typeKind: "STRUCT", structType: struct_hash } }) + struct_type = Google::Cloud::Bigquery::StandardSql::StructType.new fields: [ + Google::Cloud::Bigquery::StandardSql::Field.new(name: "int_col", type: "INT64") + ] + struct_data_type = Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: "STRUCT", struct_type: struct_type + field = Google::Cloud::Bigquery::StandardSql::Field.new name: "struct_col", type: struct_data_type field.name.must_equal "struct_col" @@ -65,8 +68,11 @@ end it "represents an anonymous STRUCT field (missing)" do - struct_hash = { fields: [{ type: { typeKind: "INT64" } }] } - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "struct_col", type: { typeKind: "STRUCT", structType: struct_hash } }) + struct_type = Google::Cloud::Bigquery::StandardSql::StructType.new fields: [ + Google::Cloud::Bigquery::StandardSql::Field.new(type: "INT64") + ] + struct_data_type = Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: "STRUCT", struct_type: struct_type + field = Google::Cloud::Bigquery::StandardSql::Field.new name: "struct_col", type: struct_data_type field.name.must_equal "struct_col" @@ -114,8 +120,11 @@ end it "represents an anonymous STRUCT field (empty)" do - struct_hash = { fields: [{ name: "", type: { typeKind: "INT64" } }] } - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "struct_col", type: { typeKind: "STRUCT", structType: struct_hash } }) + struct_type = Google::Cloud::Bigquery::StandardSql::StructType.new fields: [ + Google::Cloud::Bigquery::StandardSql::Field.new(name: "", type: "INT64") + ] + struct_data_type = Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: "STRUCT", struct_type: struct_type + field = Google::Cloud::Bigquery::StandardSql::Field.new name: "struct_col", type: struct_data_type field.name.must_equal "struct_col" @@ -163,8 +172,9 @@ end it "represents an emtpy STRUCT field" do - struct_hash = { fields: [] } - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "struct_col", type: { typeKind: "STRUCT", structType: struct_hash } }) + struct_type = Google::Cloud::Bigquery::StandardSql::StructType.new fields: [] + struct_data_type = Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: "STRUCT", struct_type: struct_type + field = Google::Cloud::Bigquery::StandardSql::Field.new name: "struct_col", type: struct_data_type field.name.must_equal "struct_col" @@ -192,11 +202,15 @@ end it "represents nested STRUCT fields" do - nested_hash = { fields: [{ name: "int_col", type: { typeKind: "INT64" } }] } - struct_hash = { fields: [ - { name: "nested_col", type: { typeKind: "STRUCT", structType: nested_hash } } - ] } - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "struct_col", type: { typeKind: "STRUCT", structType: struct_hash } }) + nested_struct_type = Google::Cloud::Bigquery::StandardSql::StructType.new fields: [ + Google::Cloud::Bigquery::StandardSql::Field.new(name: "int_col", type: "INT64") + ] + nested_struct_data_type = Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: "STRUCT", struct_type: nested_struct_type + struct_type = Google::Cloud::Bigquery::StandardSql::StructType.new fields: [ + Google::Cloud::Bigquery::StandardSql::Field.new(name: "nested_col", type: nested_struct_data_type) + ] + struct_data_type = Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: "STRUCT", struct_type: struct_type + field = Google::Cloud::Bigquery::StandardSql::Field.new name: "struct_col", type: struct_data_type field.name.must_equal "struct_col" @@ -264,20 +278,23 @@ end it "represents all types of value fields" do - struct_hash = { fields: [ - { name: "int_col", type: { typeKind: "INT64" } }, - { name: "float_col", type: { typeKind: "FLOAT64" } }, - { name: "num_col", type: { typeKind: "NUMERIC" } }, - { name: "bool_col", type: { typeKind: "BOOL" } }, - { name: "str_col", type: { typeKind: "STRING" } }, - { name: "bytes_col", type: { typeKind: "BYTES" } }, - { name: "date_col", type: { typeKind: "DATE" } }, - { name: "datetime_col", type: { typeKind: "DATETIME" } }, - { name: "geo_col", type: { typeKind: "GEOGRAPHY" } }, - { name: "time_col", type: { typeKind: "TIME" } }, - { name: "ts_col", type: { typeKind: "TIMESTAMP" } } - ] } - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "struct_col", type: { typeKind: "STRUCT", structType: struct_hash } }) + fields = [ + value_field("int_col", "INT64"), + value_field("float_col", "FLOAT64"), + value_field("num_col", "NUMERIC"), + value_field("bool_col", "BOOL"), + value_field("str_col", "STRING"), + value_field("bytes_col", "BYTES"), + value_field("date_col", "DATE"), + value_field("datetime_col", "DATETIME"), + value_field("geo_col", "GEOGRAPHY"), + value_field("time_col", "TIME"), + value_field("ts_col", "TIMESTAMP") + ] + + struct_type = Google::Cloud::Bigquery::StandardSql::StructType.new fields: fields + struct_data_type = Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: "STRUCT", struct_type: struct_type + field = Google::Cloud::Bigquery::StandardSql::Field.new name: "struct_col", type: struct_data_type field.name.must_equal "struct_col" @@ -525,20 +542,23 @@ end it "represents all types of array fields" do - struct_hash = { fields: [ - { name: "int_array_col", type: { typeKind: "ARRAY", arrayElementType: { typeKind: "INT64" } } }, - { name: "float_array_col", type: { typeKind: "ARRAY", arrayElementType: { typeKind: "FLOAT64" } } }, - { name: "num_array_col", type: { typeKind: "ARRAY", arrayElementType: { typeKind: "NUMERIC" } } }, - { name: "bool_array_col", type: { typeKind: "ARRAY", arrayElementType: { typeKind: "BOOL" } } }, - { name: "str_array_col", type: { typeKind: "ARRAY", arrayElementType: { typeKind: "STRING" } } }, - { name: "bytes_array_col", type: { typeKind: "ARRAY", arrayElementType: { typeKind: "BYTES" } } }, - { name: "date_array_col", type: { typeKind: "ARRAY", arrayElementType: { typeKind: "DATE" } } }, - { name: "datetime_array_col", type: { typeKind: "ARRAY", arrayElementType: { typeKind: "DATETIME" } } }, - { name: "geo_array_col", type: { typeKind: "ARRAY", arrayElementType: { typeKind: "GEOGRAPHY" } } }, - { name: "time_array_col", type: { typeKind: "ARRAY", arrayElementType: { typeKind: "TIME" } } }, - { name: "ts_array_col", type: { typeKind: "ARRAY", arrayElementType: { typeKind: "TIMESTAMP" } } } - ] } - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "struct_col", type: { typeKind: "STRUCT", structType: struct_hash } }) + fields = [ + array_field("int_array_col", "INT64"), + array_field("float_array_col", "FLOAT64"), + array_field("num_array_col", "NUMERIC"), + array_field("bool_array_col", "BOOL"), + array_field("str_array_col", "STRING"), + array_field("bytes_array_col", "BYTES"), + array_field("date_array_col", "DATE"), + array_field("datetime_array_col", "DATETIME"), + array_field("geo_array_col", "GEOGRAPHY"), + array_field("time_array_col", "TIME"), + array_field("ts_array_col", "TIMESTAMP") + ] + + struct_type = Google::Cloud::Bigquery::StandardSql::StructType.new fields: fields + struct_data_type = Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: "STRUCT", struct_type: struct_type + field = Google::Cloud::Bigquery::StandardSql::Field.new name: "struct_col", type: struct_data_type field.name.must_equal "struct_col" @@ -817,4 +837,15 @@ field.type.struct_type.fields[10].type.must_be :array? field.type.struct_type.fields[10].type.wont_be :struct? end + + def value_field name, type_kind + value_type = Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: type_kind + Google::Cloud::Bigquery::StandardSql::Field.new name: name, type: value_type + end + + def array_field name, type_kind + array_element_type = Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: type_kind + array_data_type =Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: "ARRAY", array_element_type: array_element_type + Google::Cloud::Bigquery::StandardSql::Field.new name: name, type: array_data_type + end end diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/standard_sql/value_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/standard_sql/value_test.rb index 88884844a9b8..579dd1c6bcb6 100644 --- a/google-cloud-bigquery/test/google/cloud/bigquery/standard_sql/value_test.rb +++ b/google-cloud-bigquery/test/google/cloud/bigquery/standard_sql/value_test.rb @@ -15,8 +15,30 @@ require "helper" describe Google::Cloud::Bigquery::StandardSql, :value do + describe "immutable constructors" do + # TODO: move these tests someplace more logical... + it "takes DataType as an argument" do + parent = Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: "STRING" + data_type = Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: "ARRAY", array_element_type: parent + + data_type.must_be_kind_of Google::Cloud::Bigquery::StandardSql::DataType + data_type.type_kind.must_equal "ARRAY" + data_type.array_element_type.must_be_kind_of Google::Cloud::Bigquery::StandardSql::DataType + data_type.array_element_type.type_kind.must_equal "STRING" + end + + it "takes Hash as an argument" do + data_type = Google::Cloud::Bigquery::StandardSql::DataType.new type_kind: "ARRAY", array_element_type: "STRING" + + data_type.must_be_kind_of Google::Cloud::Bigquery::StandardSql::DataType + data_type.type_kind.must_equal "ARRAY" + data_type.array_element_type.must_be_kind_of Google::Cloud::Bigquery::StandardSql::DataType + data_type.array_element_type.type_kind.must_equal "STRING" + end + end + it "represents a INT64 field" do - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "int_col", type: { typeKind: "INT64" } }) + field = Google::Cloud::Bigquery::StandardSql::Field.new name: "int_col", type: "INT64" field.name.must_equal "int_col" @@ -41,7 +63,7 @@ end it "represents a FLOAT64 field" do - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "float_col", type: { typeKind: "FLOAT64" } }) + field = Google::Cloud::Bigquery::StandardSql::Field.new name: "float_col", type: "FLOAT64" field.name.must_equal "float_col" @@ -66,7 +88,7 @@ end it "represents a NUMERIC field" do - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "num_col", type: { typeKind: "NUMERIC" } }) + field = Google::Cloud::Bigquery::StandardSql::Field.new name: "num_col", type: "NUMERIC" field.name.must_equal "num_col" @@ -91,7 +113,7 @@ end it "represents a BOOL field" do - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "bool_col", type: { typeKind: "BOOL" } }) + field = Google::Cloud::Bigquery::StandardSql::Field.new name: "bool_col", type: "BOOL" field.name.must_equal "bool_col" @@ -116,7 +138,7 @@ end it "represents a STRING field" do - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "str_col", type: { typeKind: "STRING" } }) + field = Google::Cloud::Bigquery::StandardSql::Field.new name: "str_col", type: "STRING" field.name.must_equal "str_col" @@ -141,7 +163,7 @@ end it "represents a BYTES field" do - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "bytes_col", type: { typeKind: "BYTES" } }) + field = Google::Cloud::Bigquery::StandardSql::Field.new name: "bytes_col", type: "BYTES" field.name.must_equal "bytes_col" @@ -166,7 +188,7 @@ end it "represents a DATE field" do - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "date_col", type: { typeKind: "DATE" } }) + field = Google::Cloud::Bigquery::StandardSql::Field.new name: "date_col", type: "DATE" field.name.must_equal "date_col" @@ -191,7 +213,7 @@ end it "represents a DATETIME field" do - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "datetime_col", type: { typeKind: "DATETIME" } }) + field = Google::Cloud::Bigquery::StandardSql::Field.new name: "datetime_col", type: "DATETIME" field.name.must_equal "datetime_col" @@ -216,7 +238,7 @@ end it "represents a GEOGRAPHY field" do - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "geo_col", type: { typeKind: "GEOGRAPHY" } }) + field = Google::Cloud::Bigquery::StandardSql::Field.new name: "geo_col", type: "GEOGRAPHY" field.name.must_equal "geo_col" @@ -241,7 +263,7 @@ end it "represents a TIME field" do - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "time_col", type: { typeKind: "TIME" } }) + field = Google::Cloud::Bigquery::StandardSql::Field.new name: "time_col", type: "TIME" field.name.must_equal "time_col" @@ -266,7 +288,7 @@ end it "represents a TIMESTAMP field" do - field = Google::Cloud::Bigquery::StandardSql::Field.from_gapi_json({ name: "ts_col", type: { typeKind: "TIMESTAMP" } }) + field = Google::Cloud::Bigquery::StandardSql::Field.new name: "ts_col", type: "TIMESTAMP" field.name.must_equal "ts_col" diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/table_attributes_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/table_attributes_test.rb index 2155f7ea0453..0ec75fbd9060 100644 --- a/google-cloud-bigquery/test/google/cloud/bigquery/table_attributes_test.rb +++ b/google-cloud-bigquery/test/google/cloud/bigquery/table_attributes_test.rb @@ -21,7 +21,7 @@ let(:table_id) { "my_table" } let(:table_name) { "My Table" } let(:description) { "This is my table" } - let(:table_hash) { random_table_small_hash "my_table", table_id, table_name } + let(:table_hash) { random_table_partial_hash "my_table", table_id, table_name } let(:table_full_hash) { random_table_hash "my_table", table_id, table_name, description } let(:table_gapi) { Google::Apis::BigqueryV2::TableList::Table.from_json table_hash.to_json } let(:table_full_gapi) { Google::Apis::BigqueryV2::Table.from_json table_full_hash.to_json } diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/table_schema_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/table_schema_test.rb index a969be5c7b8f..b1fb19a4c711 100644 --- a/google-cloud-bigquery/test/google/cloud/bigquery/table_schema_test.rb +++ b/google-cloud-bigquery/test/google/cloud/bigquery/table_schema_test.rb @@ -60,7 +60,7 @@ let(:etag) { "etag123456789" } let(:rank_schema_json) do - <<-JSON + <<~JSON [ {"name":"first_name","type":"STRING","mode":"REQUIRED"}, {"name":"rank","type":"INTEGER","mode":"NULLABLE", "description":"An integer value from 1 to 100"}, diff --git a/google-cloud-bigquery/test/google/cloud/bigquery/view_attributes_test.rb b/google-cloud-bigquery/test/google/cloud/bigquery/view_attributes_test.rb index 586235590613..465b67e7432d 100644 --- a/google-cloud-bigquery/test/google/cloud/bigquery/view_attributes_test.rb +++ b/google-cloud-bigquery/test/google/cloud/bigquery/view_attributes_test.rb @@ -21,7 +21,7 @@ let(:table_id) { "my_view" } let(:table_name) { "My View" } let(:description) { "This is my view" } - let(:view_hash) { random_view_small_hash "my_view", table_id, table_name } + let(:view_hash) { random_view_partial_hash "my_view", table_id, table_name } let(:view_full_hash) { random_view_hash "my_view", table_id, table_name, description } let(:view_gapi) { Google::Apis::BigqueryV2::TableList::Table.from_json view_hash.to_json } let(:view_full_gapi) { Google::Apis::BigqueryV2::Table.from_json view_full_hash.to_json } diff --git a/google-cloud-bigquery/test/helper.rb b/google-cloud-bigquery/test/helper.rb index 62f7a0c3a2bb..eb5be9c11289 100644 --- a/google-cloud-bigquery/test/helper.rb +++ b/google-cloud-bigquery/test/helper.rb @@ -90,7 +90,7 @@ def random_dataset_hash id = nil, name = nil, description = nil, default_expirat } end - def random_dataset_small_hash id = nil, name = nil + def random_dataset_partial_hash id = nil, name = nil id ||= "my_dataset" name ||= "My Dataset" @@ -106,7 +106,7 @@ def random_dataset_small_hash id = nil, name = nil end def list_datasets_gapi count = 2, token = nil - datasets = count.times.map { random_dataset_small_hash } + datasets = count.times.map { random_dataset_partial_hash } hash = {"kind"=>"bigquery#datasetList", "datasets"=>datasets} hash["nextPageToken"] = token unless token.nil? Google::Apis::BigqueryV2::DatasetList.from_json hash.to_json @@ -255,7 +255,7 @@ def random_table_hash dataset, id = nil, name = nil, description = nil, project_ } end - def random_table_small_hash dataset, id = nil, name = nil + def random_table_partial_hash dataset, id = nil, name = nil id ||= "my_table" name ||= "Table Name" @@ -273,7 +273,7 @@ def random_table_small_hash dataset, id = nil, name = nil end def list_tables_gapi count = 2, token = nil, total = nil - tables = count.times.map { random_table_small_hash(dataset_id) } + tables = count.times.map { random_table_partial_hash(dataset_id) } hash = {"kind" => "bigquery#tableList", "tables" => tables, "totalItems" => (total || count)} hash["nextPageToken"] = token unless token.nil? @@ -373,7 +373,7 @@ def random_view_hash dataset, id = nil, name = nil, description = nil } end - def random_view_small_hash dataset, id = nil, name = nil + def random_view_partial_hash dataset, id = nil, name = nil id ||= "my_view" name ||= "View Name" @@ -467,6 +467,95 @@ def list_models_gapi_json dataset_id, count = 2, token = nil hash.to_json end + def random_routine_hash dataset, id = nil, project_id: nil, etag: "etag123456789", description: "This is my routine", + creation_time: time_millis, last_modified_time: time_millis + id ||= "my_routine" + + h = { + kind: "bigquery#routine", + id: "#{project}:#{dataset}.#{id}", + selfLink: "http://googleapi/bigquery/v2/projects/#{project}/datasets/#{dataset}/routines/#{id}", + routineReference: { + projectId: (project_id || project), + datasetId: dataset, + routineId: id + }, + routineType: "SCALAR_FUNCTION", + language: "SQL", + arguments: [ + { + name: "arr", + argumentKind: "FIXED_TYPE", + mode: "IN", + dataType: { + typeKind: "ARRAY", + arrayElementType: { + typeKind: "STRUCT", + structType: { + fields: [ + { + name: "my-struct-name", + type: { + typeKind: "STRING" + } + }, + { + name: "my-struct-val", + type: { + typeKind: "INT64" + } + } + ] + } + } + } + }, + { + name: "out", + argumentKind: "ANY_TYPE", + mode: "OUT", + dataType: { typeKind: "STRING" } + } + ], + returnType: { typeKind: "INT64" }, + importedLibraries: ["gs://cloud-samples-data/bigquery/udfs/max-value.js"], + definitionBody: "x * 3", + description: description + } + h[:etag] = etag if etag + h[:creationTime] = creation_time if creation_time + h[:lastModifiedTime] = last_modified_time if last_modified_time + h + end + + def random_routine_partial_hash dataset, id + # List representation: etag, routineReference, routineType, creationTime, lastModifiedTime and language. + { + etag: "etag123456789", + routineReference: { + projectId: project, + datasetId: dataset, + routineId: id + }, + routineType: "SCALAR_FUNCTION", + creationTime: time_millis, + lastModifiedTime: time_millis, + language: "SQL" + } + end + + def list_routines_gapi dataset, count = 2, token = nil + routines = count.times.map { |i| random_routine_partial_hash dataset, "my_routine_#{i}" } + hash = { "kind"=>"bigquery#routineList", "routines" => routines } + hash["nextPageToken"] = token unless token.nil? + Google::Apis::BigqueryV2::ListRoutinesResponse.from_json hash.to_json + end + + def random_routine_gapi dataset, id = nil, project_id: nil, description: nil + json = random_routine_hash(dataset, id, project_id: project_id, description: description).to_json + Google::Apis::BigqueryV2::Routine.from_json json + end + def random_job_hash id = "job_9876543210", state = "running", location: "US" hash = { "kind" => "bigquery#job", @@ -533,9 +622,9 @@ def job_reference_gapi project, job_id, location: "US" job_ref end - def query_job_resp_gapi query, job_id: nil, target_table: false, statement_type: "SELECT", num_dml_affected_rows: nil, ddl_operation_performed: nil + def query_job_resp_gapi query, job_id: nil, target_routine: false, target_table: false, statement_type: "SELECT", num_dml_affected_rows: nil, ddl_operation_performed: nil gapi = Google::Apis::BigqueryV2::Job.from_json query_job_resp_json query, job_id: job_id - gapi.statistics.query = statistics_query_gapi target_table: target_table, statement_type: statement_type, num_dml_affected_rows: num_dml_affected_rows, ddl_operation_performed: ddl_operation_performed + gapi.statistics.query = statistics_query_gapi target_routine: target_routine, target_table: target_table, statement_type: statement_type, num_dml_affected_rows: num_dml_affected_rows, ddl_operation_performed: ddl_operation_performed gapi end @@ -566,7 +655,14 @@ def query_job_resp_json query, job_id: "job_9876543210", location: "US" hash.to_json end - def statistics_query_gapi target_table: false, statement_type: nil, num_dml_affected_rows: nil, ddl_operation_performed: nil + def statistics_query_gapi target_routine: false, target_table: false, statement_type: nil, num_dml_affected_rows: nil, ddl_operation_performed: nil + ddl_target_routine = if target_routine + Google::Apis::BigqueryV2::RoutineReference.new( + project_id: "target_project_id", + dataset_id: "target_dataset_id", + routine_id: "target_routine_id" + ) + end ddl_target_table = if target_table Google::Apis::BigqueryV2::TableReference.new( project_id: "target_project_id", @@ -578,6 +674,7 @@ def statistics_query_gapi target_table: false, statement_type: nil, num_dml_affe billing_tier: 1, cache_hit: true, ddl_operation_performed: ddl_operation_performed, + ddl_target_routine: ddl_target_routine, ddl_target_table: ddl_target_table, num_dml_affected_rows: num_dml_affected_rows, query_plan: [