Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions acceptance/data/fixtures.rb
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ def stuffs_random_row id = SecureRandom.int64
string: SecureRandom.hex(16),
byte: File.open("acceptance/data/face.jpg", "rb"),
date: Date.today + rand(-100..100),
timestamp: Time.now + rand(-60 * 60 * 24.0..60 * 60 * 24.0),
timestamp: Time.now + rand((-60 * 60 * 24.0)..(60 * 60 * 24.0)),
json: { venue: "Yellow Lake", rating: 10 },
ints: rand(2..10).times.map { rand(0..1000) },
floats: rand(2..10).times.map { rand(0.0..100.0) },
Expand All @@ -268,7 +268,7 @@ def stuffs_random_row id = SecureRandom.int64
File.open("acceptance/data/landmark.jpg", "rb"),
File.open("acceptance/data/logo.jpg", "rb")],
dates: rand(2..10).times.map { Date.today + rand(-100..100) },
timestamps: rand(2..10).times.map { Time.now + rand(-60 * 60 * 24.0..60 * 60 * 24.0) },
timestamps: rand(2..10).times.map { Time.now + rand((-60 * 60 * 24.0)..(60 * 60 * 24.0)) },
json_array: [{ venue: "Green Lake", rating: 8 }, { venue: "Blue Lake", rating: 9 }] }
end

Expand Down
12 changes: 12 additions & 0 deletions google-cloud-spanner/acceptance/spanner/database_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@
_(first_database).must_be_kind_of Google::Cloud::Spanner::Database
end

it "lists sessions" do
database = spanner.database instance_id, $spanner_database_id
_(database).wont_be :nil?

sessions = database.sessions
_(sessions).must_be_kind_of Google::Cloud::Spanner::Session::List
_(sessions).wont_be :empty?
sessions.each do |session|
_(session).must_be_kind_of Google::Cloud::Spanner::Session
end
end

it "creates database with pitr retention period" do
skip if emulator_enabled?

Expand Down
36 changes: 36 additions & 0 deletions google-cloud-spanner/lib/google/cloud/spanner/database.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
require "google/cloud/spanner/database/restore_info"
require "google/cloud/spanner/backup"
require "google/cloud/spanner/policy"
require "google/cloud/spanner/session/list"

module Google
module Cloud
Expand Down Expand Up @@ -618,6 +619,41 @@ def backups page_size: nil
Backup::List.from_grpc grpc, service
end

##
# Retrieves sessions belonging to the database.
#
# @param [Integer] page_size Optional. Number of sessions to be returned
# in the response. If 0 or less, defaults to the server's maximum
# allowed page size.
# @return [Array<Google::Cloud::Spanner::Session>] Enumerable list of
# sessions. (See {Google::Cloud::Spanner::Session::List})
#
# @example
# require "google/cloud/spanner"
#
# spanner = Google::Cloud::Spanner.new
# database = spanner.database "my-instance", "my-database"
#
# database.sessions.all.each do |session|
# puts session.session_id
# end
#
# @example List sessions by page size
# require "google/cloud/spanner"
#
# spanner = Google::Cloud::Spanner.new
# database = spanner.database "my-instance", "my-database"
#
# database.sessions(page_size: 5).all.each do |session|
# puts session.session_id
# end
#
def sessions page_size: nil
ensure_service!
grpc = service.list_sessions database: path, max: page_size
Session::List.from_grpc grpc, service
end

# Information about the source used to restore the database.
#
# @return [Google::Cloud::Spanner::Database::RestoreInfo, nil]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def parse interval_string
(?:T(?!$)
(?:(?<hours>-?\d+)H)?
(?:(?<minutes>-?\d+)M)?
(?:(?<seconds>-?(?!S)\d*(?:[\.,]\d{1,9})?)S)?)?
(?:(?<seconds>-?(?!S)\d*(?:[.,]\d{1,9})?)S)?)?
$
/x
interval_months = 0
Expand Down
10 changes: 10 additions & 0 deletions google-cloud-spanner/lib/google/cloud/spanner/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,16 @@ def delete_session session_name, call_options: nil
service.delete_session({ name: session_name }, opts)
end

def list_sessions database:, call_options: nil, token: nil, max: nil
opts = default_options call_options: call_options
request = {
database: database,
page_size: max,
page_token: token
}
service.list_sessions request, opts
end

def execute_streaming_sql session_name, sql, transaction: nil,
params: nil, types: nil, resume_token: nil,
partition_token: nil, seqno: nil,
Expand Down
175 changes: 175 additions & 0 deletions google-cloud-spanner/lib/google/cloud/spanner/session/list.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# Copyright 2025 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 Spanner
class Session
##
# Session::List is a special case Array with additional
# values.
class List < DelegateClass(::Array)
##
# @private
# The gRPC Service object.
attr_accessor :service

##
# @private
# The gRPC page enumerable object.
attr_accessor :grpc

##
# @private Create a new Session::List with an array of
# Session instances.
def initialize arr = []
super arr
end

##
# Whether there is a next page of sessions.
#
# @return [Boolean]
#
# @example
# require "google/cloud/spanner"
#
# spanner = Google::Cloud::Spanner.new
# database = spanner.database "my-instance", "my-database"
#
# sessions = database.sessions
# if sessions.next?
# next_sessions = sessions.next
# end
def next?
grpc.next_page?
end

##
# Retrieve the next page of sessions.
#
# @return [Session::List]
#
# @example
# require "google/cloud/spanner"
#
# spanner = Google::Cloud::Spanner.new
# database = spanner.database "my-instance", "my-database"
#
# sessions = database.sessions
# if sessions.next?
# next_sessions = sessions.next
# end
def next
return nil unless next?
ensure_service!
grpc.next_page
self.class.from_grpc grpc, @service
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 sessions. Default is no limit.
# @yield [session] The block for accessing each session.
# @yieldparam [Session] session The session object.
#
# @return [Enumerator]
#
# @example Iterating each session by passing a block:
# require "google/cloud/spanner"
#
# spanner = Google::Cloud::Spanner.new
# database = spanner.database "my-instance", "my-database"
#
# sessions = database.sessions
# sessions.all do |session|
# puts session.session_id
# end
#
# @example Using the enumerator by not passing a block:
# require "google/cloud/spanner"
#
# spanner = Google::Cloud::Spanner.new
# database = spanner.database "my-instance", "my-database"
#
# sessions = database.sessions
# all_session_ids = sessions.all.map do |session|
# session.session_id
# end
#
# @example Limit the number of API calls made:
# require "google/cloud/spanner"
#
# spanner = Google::Cloud::Spanner.new
# database = spanner.database "my-instance", "my-database"
#
# sessions = database.sessions
# sessions.all(request_limit: 10) do |session|
# puts session.session_id
# end
#
def all request_limit: nil, &block
request_limit = request_limit.to_i if request_limit
unless block_given?
return enum_for :all, request_limit: request_limit
end
results = self
loop do
results.each(&block)
if request_limit
request_limit -= 1
break if request_limit.negative?
end
break unless results.next?
results = results.next
end
end

##
# @private New Session::List from a
# `Google::Cloud::Spanner::V1::ListSessionsResponse`
# object.
def self.from_grpc grpc, service
sessions = List.new(Array(grpc.response.sessions).map do |session|
Session.from_grpc session, service
end)
sessions.grpc = grpc
sessions.service = service
sessions
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
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,7 @@
number = BigDecimal "-643383279502884.1971693993751058209749445923078164062"
duration = Google::Cloud::Spanner::Convert.number_to_duration number
_(duration).must_be_kind_of Google::Protobuf::Duration
# This should really be -643383279502884, but BigDecimal is doing something here...
_(duration.seconds).must_equal -643383279502885
_(duration.seconds).must_equal -643383279502884
_(duration.nanos).must_equal -197169399
end

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Copyright 2025 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::Database, :mock_spanner do
let(:instance_id) { "my-instance-id" }
let(:database_id) { "my-database-id" }
let(:database_grpc) do
Google::Cloud::Spanner::Admin::Database::V1::Database.new \
database_hash(instance_id: instance_id, database_id: database_id)
end
let(:database) { Google::Cloud::Spanner::Database.from_grpc database_grpc, spanner.service }

def sessions_hash count: 3, instance_id: "my-instance-id", database_id: "my-database-id"
sessions = count.times.map do |i|
{ name: session_path(instance_id, database_id, "session-#{i}") }
end
{ sessions: sessions }
end

let(:first_page) do
h = sessions_hash instance_id: instance_id, database_id: database_id
h[:next_page_token] = "next_page_token"
Google::Cloud::Spanner::V1::ListSessionsResponse.new h
end
let(:last_page) do
h = sessions_hash instance_id: instance_id, database_id: database_id
h[:sessions].pop
Google::Cloud::Spanner::V1::ListSessionsResponse.new h
end

it "lists sessions" do
get_sessions_resp = MockPagedEnumerable.new(
[first_page]
)
mock = Minitest::Mock.new
mock.expect :list_sessions, get_sessions_resp, [{ database: database_path(instance_id, database_id), page_size: nil, page_token: nil }, ::Gapic::CallOptions]
database.service.mocked_service = mock

sessions = database.sessions

mock.verify

_(sessions.count).must_equal 3
sessions.each do |session|
_(session).must_be_kind_of Google::Cloud::Spanner::Session
end
end

it "paginates sessions" do
get_sessions_resp = MockPagedEnumerable.new(
[first_page, last_page]
)
mock = Minitest::Mock.new
mock.expect :list_sessions, get_sessions_resp, [{ database: database_path(instance_id, database_id), page_size: nil, page_token: nil }, ::Gapic::CallOptions]
database.service.mocked_service = mock

sessions = database.sessions

mock.verify

_(sessions.count).must_equal 3
sessions.each do |session|
_(session).must_be_kind_of Google::Cloud::Spanner::Session
end
_(sessions.next?).must_equal true
end

it "paginates sessions with page size" do
get_sessions_resp = MockPagedEnumerable.new(
[first_page, last_page]
)
mock = Minitest::Mock.new
mock.expect :list_sessions, get_sessions_resp, [{ database: database_path(instance_id, database_id), page_size: 3, page_token: nil }, ::Gapic::CallOptions]
database.service.mocked_service = mock

sessions = database.sessions page_size: 3

mock.verify

_(sessions.count).must_equal 3
sessions.each do |session|
_(session).must_be_kind_of Google::Cloud::Spanner::Session
end
_(sessions.next?).must_equal true
end
end
Loading