From 856f6319611492dd1a9ef0863ca0e909c6672b76 Mon Sep 17 00:00:00 2001 From: Torrey Payne <11740989+torreypayne@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:52:51 +0000 Subject: [PATCH 1/3] feat(spanner): add send_message and ack_message to Commit --- .../lib/google/cloud/spanner/ack_options.rb | 60 +++++++ .../lib/google/cloud/spanner/commit.rb | 162 ++++++++++++++++++ .../lib/google/cloud/spanner/send_options.rb | 61 +++++++ .../support/doctest_helper.rb | 13 ++ .../cloud/spanner/commit_queues_test.rb | 137 +++++++++++++++ 5 files changed, 433 insertions(+) create mode 100644 google-cloud-spanner/lib/google/cloud/spanner/ack_options.rb create mode 100644 google-cloud-spanner/lib/google/cloud/spanner/send_options.rb create mode 100644 google-cloud-spanner/test/google/cloud/spanner/commit_queues_test.rb diff --git a/google-cloud-spanner/lib/google/cloud/spanner/ack_options.rb b/google-cloud-spanner/lib/google/cloud/spanner/ack_options.rb new file mode 100644 index 00000000..77ad6df1 --- /dev/null +++ b/google-cloud-spanner/lib/google/cloud/spanner/ack_options.rb @@ -0,0 +1,60 @@ +# Copyright 2026 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. + + +module Google + module Cloud + module Spanner + ## + # # AckOptions + # + # Optional settings for acknowledging a queue message with + # {Commit#ack_message}. A hash with the same keys can be passed + # instead. + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # db = spanner.client "my-instance", "my-database" + # + # ack_options = Google::Cloud::Spanner::AckOptions.new ignore_not_found: true + # + # db.commit do |c| + # c.ack_message "OrderQueue", 101, ack_options: ack_options + # end + # + class AckOptions + ## + # Whether the ack succeeds when the message doesn't exist. + # + # @return [Boolean] + attr_reader :ignore_not_found + + ## + # Creates a new AckOptions. + # + # @param [Boolean] ignore_not_found If `true`, the ack succeeds even + # if the message doesn't exist. Optional. The default is `false`, + # which fails the commit with `NOT_FOUND` when the message doesn't + # exist. + # + def initialize ignore_not_found: false + @ignore_not_found = ignore_not_found + end + end + end + end +end diff --git a/google-cloud-spanner/lib/google/cloud/spanner/commit.rb b/google-cloud-spanner/lib/google/cloud/spanner/commit.rb index 29295180..6b71cde8 100644 --- a/google-cloud-spanner/lib/google/cloud/spanner/commit.rb +++ b/google-cloud-spanner/lib/google/cloud/spanner/commit.rb @@ -15,6 +15,8 @@ require "google/cloud/spanner/data" require "google/cloud/spanner/convert" +require "google/cloud/spanner/send_options" +require "google/cloud/spanner/ack_options" module Google module Cloud @@ -280,6 +282,130 @@ def delete table, keys = [] keys end + ## + # Sends a message to a queue. + # + # All changes are accumulated in memory until the block passed to + # {Client#commit} completes. + # + # @param [String] queue The name of the queue. + # @param [Object, Array] key A single key or array of composite key parts for the message. + # @param [String, IO, StringIO, Hash, Google::Protobuf::MessageExts] payload + # The message payload. It is converted the same way as column values + # (see {#upsert}): + # * `String` - Sent as text. + # * `IO`, `StringIO`, `File` - Sent as bytes (Base64-encoded). Ruby has + # no bytes type, so wrap binary data in a `StringIO`. + # * `Hash` - Serialized to a JSON string. + # * `Google::Protobuf::MessageExts` - Serialized and Base64-encoded. + # @param [SendOptions, Hash] send_options A {SendOptions} or a hash of + # send options. Send options are optional. The following options can + # be provided: + # + # * `:deliver_time` (Time) The time at which Spanner begins trying to + # deliver the message. If not set, the message is delivered + # immediately. A time in the past is replaced with a time closer to + # the current time. + # + # @return [Object, Array] The key of the message. + # + # @raise [ArgumentError] If the payload type is unsupported, or if + # `send_options` is not a {SendOptions}, a hash of its options, or + # `nil`. + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # db = spanner.client "my-instance", "my-database" + # + # db.commit do |c| + # c.send_message "OrderQueue", 101, StringIO.new("Order payload") + # end + # + # @example Deliver the message in five minutes + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # db = spanner.client "my-instance", "my-database" + # + # send_options = { deliver_time: Time.now + 300 } + # + # db.commit do |c| + # c.send_message "OrderQueue", 101, StringIO.new("Order payload"), + # send_options: send_options + # end + # + def send_message queue, key, payload, send_options: nil + send_options = build_options send_options, SendOptions + send_mutation = V1::Mutation::Send.new( + queue: queue, + key: key_list_value(key), + payload: Convert.object_to_grpc_value(payload), + deliver_time: Convert.time_to_timestamp(send_options.deliver_time) + ) + @mutations += [V1::Mutation.new(send: send_mutation)] + key + end + + ## + # Acknowledges a message in a queue, which removes it from the queue. + # + # All changes are accumulated in memory until the block passed to + # {Client#commit} completes. + # + # @param [String] queue The name of the queue. + # @param [Object, Array] key A single key or array of composite key parts to match. + # @param [AckOptions, Hash] ack_options An {AckOptions} or a hash of ack + # options. Ack options are optional. The following options can be + # provided: + # + # * `:ignore_not_found` (Boolean) If `true`, the ack succeeds even if + # the message doesn't exist. Default value is `false`, which fails + # the commit with `NOT_FOUND` when the message doesn't exist. + # + # @return [Object, Array] The key of the message. + # + # @raise [ArgumentError] If `ack_options` is not an {AckOptions}, a + # hash of its options, or `nil`. + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # db = spanner.client "my-instance", "my-database" + # + # db.commit do |c| + # c.ack_message "OrderQueue", 101 + # end + # + # @example Ack a message whether or not it exists + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # db = spanner.client "my-instance", "my-database" + # + # ack_options = { ignore_not_found: true } + # + # db.commit do |c| + # c.ack_message "OrderQueue", 101, ack_options: ack_options + # end + # + def ack_message queue, key, ack_options: nil + ack_options = build_options ack_options, AckOptions + ack_mutation = V1::Mutation::Ack.new( + queue: queue, + key: key_list_value(key), + ignore_not_found: ack_options.ignore_not_found + ) + @mutations += [V1::Mutation.new(ack: ack_mutation)] + key + end + # Return the current mutations added to this `Spanner::Commit` object. # @private # @return [Array] @@ -325,6 +451,42 @@ def mutations_from_rows table, rows, type rows end + ## + # @private + # Generates a `ListValue` protobuf containing gRPC values for the given primary key. + # + # @param [Object, Array] key A single, or array of keys to convert. + # @return [Google::Protobuf::ListValue] + # + def key_list_value key + key = [key] unless key.is_a? Array + key_list = key.map do |k| + Convert.object_to_grpc_value k + end + Google::Protobuf::ListValue.new values: key_list + end + + ## + # @private + # Returns `options` as an instance of `options_class`, so callers can + # pass the options object, a hash of its keyword arguments, or `nil`. + # + # @param [Object, Hash, nil] options The options given by the caller. + # @param [Class] options_class The options class, e.g. {SendOptions}. + # @return [Object] An instance of `options_class`. + # @raise [ArgumentError] If `options` is any other type, or the hash + # has a key that `options_class` doesn't accept. + # + def build_options options, options_class + case options + when options_class then options + when nil then options_class.new + when Hash then options_class.new(**options) + else + raise ArgumentError, "expected #{options_class}, Hash, or nil, got #{options.class}" + end + end + def key_set keys return V1::KeySet.new all: true if keys.nil? keys = [keys] unless keys.is_a? Array diff --git a/google-cloud-spanner/lib/google/cloud/spanner/send_options.rb b/google-cloud-spanner/lib/google/cloud/spanner/send_options.rb new file mode 100644 index 00000000..811f491b --- /dev/null +++ b/google-cloud-spanner/lib/google/cloud/spanner/send_options.rb @@ -0,0 +1,61 @@ +# Copyright 2026 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. + + +module Google + module Cloud + module Spanner + ## + # # SendOptions + # + # Optional settings for sending a message to a queue with + # {Commit#send_message}. A hash with the same keys can be passed + # instead. + # + # @example + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # db = spanner.client "my-instance", "my-database" + # + # send_options = Google::Cloud::Spanner::SendOptions.new deliver_time: Time.now + 300 + # + # db.commit do |c| + # c.send_message "OrderQueue", 101, StringIO.new("Order payload"), + # send_options: send_options + # end + # + class SendOptions + ## + # The time at which Spanner begins trying to deliver the message. + # + # @return [Time, nil] + attr_reader :deliver_time + + ## + # Creates a new SendOptions. + # + # @param [Time, nil] deliver_time The time at which Spanner begins + # trying to deliver the message. Optional. If not set, the message + # is delivered immediately. A time in the past is replaced with a + # time closer to the current time. + # + def initialize deliver_time: nil + @deliver_time = deliver_time + end + end + end + end +end diff --git a/google-cloud-spanner/support/doctest_helper.rb b/google-cloud-spanner/support/doctest_helper.rb index 0e4a55f7..5afc2d2f 100644 --- a/google-cloud-spanner/support/doctest_helper.rb +++ b/google-cloud-spanner/support/doctest_helper.rb @@ -762,6 +762,19 @@ def verify_mock_params actual, expected end end + # SendOptions and AckOptions + + ["Google::Cloud::Spanner::SendOptions", "Google::Cloud::Spanner::AckOptions"].each do |options_class| + doctest.before options_class do + mock_spanner do |mock, mock_instances, mock_databases| + mock.expect :batch_create_sessions, OpenStruct.new(session: Array.new(10) { session_grpc }), [{database: "projects/my-project/instances/my-instance/databases/my-database", session_count: 10, session_template: nil }, Hash] + mock.expect :commit, commit_resp do |req| + verify_mock_params(req, session: "session-name", mutations: Array) + end + end + end + end + # Data doctest.before "Google::Cloud::Spanner::Data" do diff --git a/google-cloud-spanner/test/google/cloud/spanner/commit_queues_test.rb b/google-cloud-spanner/test/google/cloud/spanner/commit_queues_test.rb new file mode 100644 index 00000000..0c58d42b --- /dev/null +++ b/google-cloud-spanner/test/google/cloud/spanner/commit_queues_test.rb @@ -0,0 +1,137 @@ +# Copyright 2026 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::Spanner::Commit, :queues, :mock_spanner do + let(:commit) { Google::Cloud::Spanner::Commit.new } + let(:deliver_time) { Time.now + 300 } + + it "builds a send mutation with scalar key" do + commit.send_message "TestQueue", 2, "Hello, Queues!" + + assert_equal 1, commit.mutations.count + mutation = commit.mutations.first + + assert mutation["send"] + assert_equal "TestQueue", mutation["send"].queue + assert_equal Google::Cloud::Spanner::Convert.object_to_grpc_value([2]).list_value, mutation["send"].key + assert_equal Google::Cloud::Spanner::Convert.object_to_grpc_value("Hello, Queues!"), mutation["send"].payload + assert_nil mutation["send"].deliver_time + end + + it "builds a send mutation with composite key" do + commit.send_message "TestQueue", ["tenant1", 2], "Hello, Queues!" + + assert_equal 1, commit.mutations.count + mutation = commit.mutations.first + + assert mutation["send"] + assert_equal "TestQueue", mutation["send"].queue + assert_equal Google::Cloud::Spanner::Convert.object_to_grpc_value(["tenant1", 2]).list_value, mutation["send"].key + assert_equal Google::Cloud::Spanner::Convert.object_to_grpc_value("Hello, Queues!"), mutation["send"].payload + assert_nil mutation["send"].deliver_time + end + + it "encodes StringIO payloads as Base64" do + raw_bytes = "\x00\x01\x02\xFF".b + commit.send_message "TestQueue", 1, StringIO.new(raw_bytes) + + assert_equal 1, commit.mutations.count + assert_equal Base64.strict_encode64(raw_bytes), commit.mutations.first["send"].payload.string_value + end + + it "encodes Hash payload as JSON string" do + hash_payload = { "event" => "user_created", "status" => "active" } + + commit.send_message "TestQueue", 1, hash_payload + + assert_equal 1, commit.mutations.count + mutation = commit.mutations.first + + assert_equal hash_payload.to_json, mutation["send"].payload.string_value + end + + it "raises ArgumentError for unsupported payload types" do + assert_raises ArgumentError do + commit.send_message "TestQueue", 1, Object.new + end + end + + it "sets deliver_time from SendOptions or a Hash" do + send_options = Google::Cloud::Spanner::SendOptions.new deliver_time: deliver_time + + commit.send_message "TestQueue", 1, "payload", send_options: send_options + commit.send_message "TestQueue", 2, "payload", send_options: { deliver_time: deliver_time } + + expected = Google::Cloud::Spanner::Convert.time_to_timestamp deliver_time + assert_equal [expected, expected], commit.mutations.map { |mutation| mutation["send"].deliver_time } + end + + it "raises ArgumentError for invalid send_options" do + assert_raises(ArgumentError) { commit.send_message "TestQueue", 1, "payload", send_options: { deliver_at: deliver_time } } + assert_raises(ArgumentError) { commit.send_message "TestQueue", 1, "payload", send_options: deliver_time } + assert_raises ArgumentError do + commit.send_message "TestQueue", 1, "payload", send_options: Google::Cloud::Spanner::AckOptions.new + end + assert_empty commit.mutations + end + + it "returns the key from send_message" do + assert_equal 2, commit.send_message("TestQueue", 2, "payload") + assert_equal ["tenant1", 2], commit.send_message("TestQueue", ["tenant1", 2], "payload") + end + + it "builds an ack mutation with scalar and composite keys" do + commit.ack_message "TestQueue", 2 + commit.ack_message "TestQueue", ["tenant1", 2] + + assert_equal 2, commit.mutations.count + m1 = commit.mutations[0] + m2 = commit.mutations[1] + + assert m1.ack + assert_equal "TestQueue", m1.ack.queue + assert_equal Google::Cloud::Spanner::Convert.object_to_grpc_value([2]).list_value, m1.ack.key + assert_equal false, m1.ack.ignore_not_found + + assert m2.ack + assert_equal "TestQueue", m2.ack.queue + assert_equal Google::Cloud::Spanner::Convert.object_to_grpc_value(["tenant1", 2]).list_value, m2.ack.key + assert_equal false, m2.ack.ignore_not_found + end + + it "sets ignore_not_found from AckOptions or a Hash" do + ack_options = Google::Cloud::Spanner::AckOptions.new ignore_not_found: true + + commit.ack_message "TestQueue", 1, ack_options: ack_options + commit.ack_message "TestQueue", 2, ack_options: { ignore_not_found: true } + + assert_equal [true, true], commit.mutations.map { |mutation| mutation.ack.ignore_not_found } + end + + it "raises ArgumentError for invalid ack_options" do + assert_raises(ArgumentError) { commit.ack_message "TestQueue", 1, ack_options: { ignore_missing: true } } + assert_raises(ArgumentError) { commit.ack_message "TestQueue", 1, ack_options: true } + assert_raises ArgumentError do + commit.ack_message "TestQueue", 1, ack_options: Google::Cloud::Spanner::SendOptions.new + end + assert_empty commit.mutations + end + + it "returns the key from ack_message" do + assert_equal 2, commit.ack_message("TestQueue", 2) + assert_equal ["tenant1", 2], commit.ack_message("TestQueue", ["tenant1", 2]) + end +end From ee6bf01ebafc3fbdae91cfaa945c2565c72fce32 Mon Sep 17 00:00:00 2001 From: Torrey Payne <11740989+torreypayne@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:03:58 +0000 Subject: [PATCH 2/3] refactor(spanner): take deliver_time and ignore_not_found as keyword arguments Replace the SendOptions and AckOptions classes with keyword arguments, as agreed on #262. Adding an optional keyword later doesn't break callers, and an unknown keyword still raises ArgumentError. Also fix the key_list_value doc: it converts one key, not a list of keys. --- .../lib/google/cloud/spanner/ack_options.rb | 60 ---------------- .../lib/google/cloud/spanner/commit.rb | 72 ++++--------------- .../lib/google/cloud/spanner/send_options.rb | 61 ---------------- .../support/doctest_helper.rb | 13 ---- .../cloud/spanner/commit_queues_test.rb | 34 +++------ 5 files changed, 25 insertions(+), 215 deletions(-) delete mode 100644 google-cloud-spanner/lib/google/cloud/spanner/ack_options.rb delete mode 100644 google-cloud-spanner/lib/google/cloud/spanner/send_options.rb diff --git a/google-cloud-spanner/lib/google/cloud/spanner/ack_options.rb b/google-cloud-spanner/lib/google/cloud/spanner/ack_options.rb deleted file mode 100644 index 77ad6df1..00000000 --- a/google-cloud-spanner/lib/google/cloud/spanner/ack_options.rb +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2026 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. - - -module Google - module Cloud - module Spanner - ## - # # AckOptions - # - # Optional settings for acknowledging a queue message with - # {Commit#ack_message}. A hash with the same keys can be passed - # instead. - # - # @example - # require "google/cloud/spanner" - # - # spanner = Google::Cloud::Spanner.new - # - # db = spanner.client "my-instance", "my-database" - # - # ack_options = Google::Cloud::Spanner::AckOptions.new ignore_not_found: true - # - # db.commit do |c| - # c.ack_message "OrderQueue", 101, ack_options: ack_options - # end - # - class AckOptions - ## - # Whether the ack succeeds when the message doesn't exist. - # - # @return [Boolean] - attr_reader :ignore_not_found - - ## - # Creates a new AckOptions. - # - # @param [Boolean] ignore_not_found If `true`, the ack succeeds even - # if the message doesn't exist. Optional. The default is `false`, - # which fails the commit with `NOT_FOUND` when the message doesn't - # exist. - # - def initialize ignore_not_found: false - @ignore_not_found = ignore_not_found - end - end - end - end -end diff --git a/google-cloud-spanner/lib/google/cloud/spanner/commit.rb b/google-cloud-spanner/lib/google/cloud/spanner/commit.rb index 6b71cde8..1af6ca23 100644 --- a/google-cloud-spanner/lib/google/cloud/spanner/commit.rb +++ b/google-cloud-spanner/lib/google/cloud/spanner/commit.rb @@ -15,8 +15,6 @@ require "google/cloud/spanner/data" require "google/cloud/spanner/convert" -require "google/cloud/spanner/send_options" -require "google/cloud/spanner/ack_options" module Google module Cloud @@ -298,20 +296,14 @@ def delete table, keys = [] # no bytes type, so wrap binary data in a `StringIO`. # * `Hash` - Serialized to a JSON string. # * `Google::Protobuf::MessageExts` - Serialized and Base64-encoded. - # @param [SendOptions, Hash] send_options A {SendOptions} or a hash of - # send options. Send options are optional. The following options can - # be provided: - # - # * `:deliver_time` (Time) The time at which Spanner begins trying to - # deliver the message. If not set, the message is delivered - # immediately. A time in the past is replaced with a time closer to - # the current time. + # @param [Time, nil] deliver_time The time at which Spanner begins + # trying to deliver the message. Optional. If not set, the message is + # delivered immediately. A time in the past is replaced with a time + # closer to the current time. # # @return [Object, Array] The key of the message. # - # @raise [ArgumentError] If the payload type is unsupported, or if - # `send_options` is not a {SendOptions}, a hash of its options, or - # `nil`. + # @raise [ArgumentError] If the payload type is unsupported. # # @example # require "google/cloud/spanner" @@ -331,20 +323,17 @@ def delete table, keys = [] # # db = spanner.client "my-instance", "my-database" # - # send_options = { deliver_time: Time.now + 300 } - # # db.commit do |c| # c.send_message "OrderQueue", 101, StringIO.new("Order payload"), - # send_options: send_options + # deliver_time: Time.now + 300 # end # - def send_message queue, key, payload, send_options: nil - send_options = build_options send_options, SendOptions + def send_message queue, key, payload, deliver_time: nil send_mutation = V1::Mutation::Send.new( queue: queue, key: key_list_value(key), payload: Convert.object_to_grpc_value(payload), - deliver_time: Convert.time_to_timestamp(send_options.deliver_time) + deliver_time: Convert.time_to_timestamp(deliver_time) ) @mutations += [V1::Mutation.new(send: send_mutation)] key @@ -358,19 +347,12 @@ def send_message queue, key, payload, send_options: nil # # @param [String] queue The name of the queue. # @param [Object, Array] key A single key or array of composite key parts to match. - # @param [AckOptions, Hash] ack_options An {AckOptions} or a hash of ack - # options. Ack options are optional. The following options can be - # provided: - # - # * `:ignore_not_found` (Boolean) If `true`, the ack succeeds even if - # the message doesn't exist. Default value is `false`, which fails - # the commit with `NOT_FOUND` when the message doesn't exist. + # @param [Boolean] ignore_not_found If `true`, the ack succeeds even if + # the message doesn't exist. Optional. The default is `false`, which + # fails the commit with `NOT_FOUND` when the message doesn't exist. # # @return [Object, Array] The key of the message. # - # @raise [ArgumentError] If `ack_options` is not an {AckOptions}, a - # hash of its options, or `nil`. - # # @example # require "google/cloud/spanner" # @@ -389,18 +371,15 @@ def send_message queue, key, payload, send_options: nil # # db = spanner.client "my-instance", "my-database" # - # ack_options = { ignore_not_found: true } - # # db.commit do |c| - # c.ack_message "OrderQueue", 101, ack_options: ack_options + # c.ack_message "OrderQueue", 101, ignore_not_found: true # end # - def ack_message queue, key, ack_options: nil - ack_options = build_options ack_options, AckOptions + def ack_message queue, key, ignore_not_found: false ack_mutation = V1::Mutation::Ack.new( queue: queue, key: key_list_value(key), - ignore_not_found: ack_options.ignore_not_found + ignore_not_found: ignore_not_found ) @mutations += [V1::Mutation.new(ack: ack_mutation)] key @@ -455,7 +434,7 @@ def mutations_from_rows table, rows, type # @private # Generates a `ListValue` protobuf containing gRPC values for the given primary key. # - # @param [Object, Array] key A single, or array of keys to convert. + # @param [Object, Array] key A scalar key, or an array of a composite key's parts. # @return [Google::Protobuf::ListValue] # def key_list_value key @@ -466,27 +445,6 @@ def key_list_value key Google::Protobuf::ListValue.new values: key_list end - ## - # @private - # Returns `options` as an instance of `options_class`, so callers can - # pass the options object, a hash of its keyword arguments, or `nil`. - # - # @param [Object, Hash, nil] options The options given by the caller. - # @param [Class] options_class The options class, e.g. {SendOptions}. - # @return [Object] An instance of `options_class`. - # @raise [ArgumentError] If `options` is any other type, or the hash - # has a key that `options_class` doesn't accept. - # - def build_options options, options_class - case options - when options_class then options - when nil then options_class.new - when Hash then options_class.new(**options) - else - raise ArgumentError, "expected #{options_class}, Hash, or nil, got #{options.class}" - end - end - def key_set keys return V1::KeySet.new all: true if keys.nil? keys = [keys] unless keys.is_a? Array diff --git a/google-cloud-spanner/lib/google/cloud/spanner/send_options.rb b/google-cloud-spanner/lib/google/cloud/spanner/send_options.rb deleted file mode 100644 index 811f491b..00000000 --- a/google-cloud-spanner/lib/google/cloud/spanner/send_options.rb +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright 2026 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. - - -module Google - module Cloud - module Spanner - ## - # # SendOptions - # - # Optional settings for sending a message to a queue with - # {Commit#send_message}. A hash with the same keys can be passed - # instead. - # - # @example - # require "google/cloud/spanner" - # - # spanner = Google::Cloud::Spanner.new - # - # db = spanner.client "my-instance", "my-database" - # - # send_options = Google::Cloud::Spanner::SendOptions.new deliver_time: Time.now + 300 - # - # db.commit do |c| - # c.send_message "OrderQueue", 101, StringIO.new("Order payload"), - # send_options: send_options - # end - # - class SendOptions - ## - # The time at which Spanner begins trying to deliver the message. - # - # @return [Time, nil] - attr_reader :deliver_time - - ## - # Creates a new SendOptions. - # - # @param [Time, nil] deliver_time The time at which Spanner begins - # trying to deliver the message. Optional. If not set, the message - # is delivered immediately. A time in the past is replaced with a - # time closer to the current time. - # - def initialize deliver_time: nil - @deliver_time = deliver_time - end - end - end - end -end diff --git a/google-cloud-spanner/support/doctest_helper.rb b/google-cloud-spanner/support/doctest_helper.rb index 5afc2d2f..0e4a55f7 100644 --- a/google-cloud-spanner/support/doctest_helper.rb +++ b/google-cloud-spanner/support/doctest_helper.rb @@ -762,19 +762,6 @@ def verify_mock_params actual, expected end end - # SendOptions and AckOptions - - ["Google::Cloud::Spanner::SendOptions", "Google::Cloud::Spanner::AckOptions"].each do |options_class| - doctest.before options_class do - mock_spanner do |mock, mock_instances, mock_databases| - mock.expect :batch_create_sessions, OpenStruct.new(session: Array.new(10) { session_grpc }), [{database: "projects/my-project/instances/my-instance/databases/my-database", session_count: 10, session_template: nil }, Hash] - mock.expect :commit, commit_resp do |req| - verify_mock_params(req, session: "session-name", mutations: Array) - end - end - end - end - # Data doctest.before "Google::Cloud::Spanner::Data" do diff --git a/google-cloud-spanner/test/google/cloud/spanner/commit_queues_test.rb b/google-cloud-spanner/test/google/cloud/spanner/commit_queues_test.rb index 0c58d42b..fae81e2a 100644 --- a/google-cloud-spanner/test/google/cloud/spanner/commit_queues_test.rb +++ b/google-cloud-spanner/test/google/cloud/spanner/commit_queues_test.rb @@ -69,22 +69,15 @@ end end - it "sets deliver_time from SendOptions or a Hash" do - send_options = Google::Cloud::Spanner::SendOptions.new deliver_time: deliver_time - - commit.send_message "TestQueue", 1, "payload", send_options: send_options - commit.send_message "TestQueue", 2, "payload", send_options: { deliver_time: deliver_time } + it "sets deliver_time" do + commit.send_message "TestQueue", 1, "payload", deliver_time: deliver_time expected = Google::Cloud::Spanner::Convert.time_to_timestamp deliver_time - assert_equal [expected, expected], commit.mutations.map { |mutation| mutation["send"].deliver_time } + assert_equal expected, commit.mutations.first["send"].deliver_time end - it "raises ArgumentError for invalid send_options" do - assert_raises(ArgumentError) { commit.send_message "TestQueue", 1, "payload", send_options: { deliver_at: deliver_time } } - assert_raises(ArgumentError) { commit.send_message "TestQueue", 1, "payload", send_options: deliver_time } - assert_raises ArgumentError do - commit.send_message "TestQueue", 1, "payload", send_options: Google::Cloud::Spanner::AckOptions.new - end + it "raises ArgumentError for an unknown send_message keyword" do + assert_raises(ArgumentError) { commit.send_message "TestQueue", 1, "payload", deliver_at: deliver_time } assert_empty commit.mutations end @@ -112,21 +105,14 @@ assert_equal false, m2.ack.ignore_not_found end - it "sets ignore_not_found from AckOptions or a Hash" do - ack_options = Google::Cloud::Spanner::AckOptions.new ignore_not_found: true + it "sets ignore_not_found" do + commit.ack_message "TestQueue", 1, ignore_not_found: true - commit.ack_message "TestQueue", 1, ack_options: ack_options - commit.ack_message "TestQueue", 2, ack_options: { ignore_not_found: true } - - assert_equal [true, true], commit.mutations.map { |mutation| mutation.ack.ignore_not_found } + assert_equal true, commit.mutations.first.ack.ignore_not_found end - it "raises ArgumentError for invalid ack_options" do - assert_raises(ArgumentError) { commit.ack_message "TestQueue", 1, ack_options: { ignore_missing: true } } - assert_raises(ArgumentError) { commit.ack_message "TestQueue", 1, ack_options: true } - assert_raises ArgumentError do - commit.ack_message "TestQueue", 1, ack_options: Google::Cloud::Spanner::SendOptions.new - end + it "raises ArgumentError for an unknown ack_message keyword" do + assert_raises(ArgumentError) { commit.ack_message "TestQueue", 1, ignore_missing: true } assert_empty commit.mutations end From 5420d5eabc7dd37d199923b68555f342ae3ab28f Mon Sep 17 00:00:00 2001 From: Torrey Payne <11740989+torreypayne@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:03:11 +0000 Subject: [PATCH 3/3] docs(spanner): use String payloads in the send_message examples The examples wrapped text in a StringIO, which is only right for a queue with a BYTES payload column. On a STRING column the gem would send the Base64 text. Use a String and say the queue's payload column is STRING(MAX), and add a separate BYTES example that wraps binary data. --- .../lib/google/cloud/spanner/commit.rb | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/google-cloud-spanner/lib/google/cloud/spanner/commit.rb b/google-cloud-spanner/lib/google/cloud/spanner/commit.rb index 1af6ca23..319c391d 100644 --- a/google-cloud-spanner/lib/google/cloud/spanner/commit.rb +++ b/google-cloud-spanner/lib/google/cloud/spanner/commit.rb @@ -290,7 +290,8 @@ def delete table, keys = [] # @param [Object, Array] key A single key or array of composite key parts for the message. # @param [String, IO, StringIO, Hash, Google::Protobuf::MessageExts] payload # The message payload. It is converted the same way as column values - # (see {#upsert}): + # (see {#upsert}), so its type must match the queue's payload column, + # such as a `String` for `STRING` or a `StringIO` for `BYTES`: # * `String` - Sent as text. # * `IO`, `StringIO`, `File` - Sent as bytes (Base64-encoded). Ruby has # no bytes type, so wrap binary data in a `StringIO`. @@ -312,8 +313,20 @@ def delete table, keys = [] # # db = spanner.client "my-instance", "my-database" # + # # OrderQueue has a STRING(MAX) payload column. # db.commit do |c| - # c.send_message "OrderQueue", 101, StringIO.new("Order payload") + # c.send_message "OrderQueue", 101, "Order payload" + # end + # + # @example Send binary data to a queue with a BYTES payload column + # require "google/cloud/spanner" + # + # spanner = Google::Cloud::Spanner.new + # + # db = spanner.client "my-instance", "my-database" + # + # db.commit do |c| + # c.send_message "LabelQueue", 101, StringIO.new(File.binread("label.png")) # end # # @example Deliver the message in five minutes @@ -324,7 +337,7 @@ def delete table, keys = [] # db = spanner.client "my-instance", "my-database" # # db.commit do |c| - # c.send_message "OrderQueue", 101, StringIO.new("Order payload"), + # c.send_message "OrderQueue", 101, "Order payload", # deliver_time: Time.now + 300 # end #