Skip to content

Commit 65d6bd8

Browse files
authored
chore(ruby): support multi-statement SQL strings in Ruby wrapper (googleapis#655)
* chore(ruby): support multi-statement SQL strings in Ruby wrapper
1 parent aac54e0 commit 65d6bd8

File tree

4 files changed

+107
-24
lines changed

4 files changed

+107
-24
lines changed

spannerlib/wrappers/spannerlib-ruby/lib/spannerlib/ffi.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ class Message < FFI::Struct
116116
attach_function :ResultSetStats, %i[int64 int64 int64], Message.by_value
117117
attach_function :CloseRows, %i[int64 int64 int64], Message.by_value
118118
attach_function :Release, [:int64], :void
119+
attach_function :NextResultSet, %i[int64 int64 int64], Message.by_value
119120

120121
# --- Ruby-friendly Wrappers ---
121122

@@ -272,6 +273,12 @@ def self.close_rows(pool_id, conn_id, rows_id)
272273
message = CloseRows(pool_id, conn_id, rows_id)
273274
handle_status_response(message, "CloseRows")
274275
end
276+
277+
def self.next_result_set(pool_id, conn_id, rows_id)
278+
message = NextResultSet(pool_id, conn_id, rows_id)
279+
# This returns Metadata for the next result set, or nil if no more sets exist.
280+
handle_data_response(message, "NextResultSet")
281+
end
275282
end
276283

277284
# rubocop:enable Metrics/ModuleLength

spannerlib/wrappers/spannerlib-ruby/lib/spannerlib/rows.rb

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
# frozen_string_literal: true
1616

17+
require "google/spanner/v1/result_set_pb"
18+
1719
module SpannerLib
1820
class Rows
1921
include Enumerable
@@ -24,6 +26,7 @@ def initialize(connection, rows_id)
2426
@connection = connection
2527
@id = rows_id
2628
@closed = false
29+
@metadata = nil
2730
end
2831

2932
def each
@@ -41,26 +44,46 @@ def next
4144

4245
row_data = SpannerLib.next(connection.pool_id, connection.conn_id, id, 1, 0)
4346

44-
if row_data.nil? || row_data.empty? || (row_data.respond_to?(:values) && row_data.values.empty?)
47+
return nil if row_data.nil? || row_data.empty?
48+
49+
row_data
50+
end
51+
52+
def next_result_set
53+
return nil if @closed
54+
55+
@metadata = nil
56+
57+
res = SpannerLib.next_result_set(connection.pool_id, connection.conn_id, id)
58+
59+
if res.nil? || res.empty?
4560
close
4661
return nil
4762
end
48-
49-
row_data
63+
@metadata = Google::Cloud::Spanner::V1::ResultSetMetadata.decode(res)
5064
end
5165

5266
def metadata
53-
SpannerLib.metadata(connection.pool_id, connection.conn_id, id)
67+
return @metadata if @metadata
68+
69+
raw = SpannerLib.metadata(connection.pool_id, connection.conn_id, id)
70+
return nil if raw.nil? || raw.empty?
71+
72+
@metadata = Google::Cloud::Spanner::V1::ResultSetMetadata.decode(raw)
5473
end
5574

5675
def result_set_stats
57-
SpannerLib.result_set_stats(connection.pool_id, connection.conn_id, id)
76+
raw = SpannerLib.result_set_stats(connection.pool_id, connection.conn_id, id)
77+
return nil if raw.nil? || raw.empty?
78+
79+
Google::Cloud::Spanner::V1::ResultSetStats.decode(raw)
5880
end
5981

6082
def close
6183
return if @closed
6284

6385
SpannerLib.close_rows(connection.pool_id, connection.conn_id, id)
86+
@metadata = nil
6487
@closed = true
6588
end
6689
end

spannerlib/wrappers/spannerlib-ruby/spec/mock_server/spanner_mock_server.rb

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,20 @@ def log_request(request)
4848
@requests << request
4949
return unless @req_file_path
5050

51-
data = {
52-
class: request.class.name,
53-
payload: request.class.encode(request)
54-
}
55-
File.open(@req_file_path, "ab") do |f|
56-
Marshal.dump(data, f)
51+
# Guard clause: Ensure we only attempt to encode Protobuf objects.
52+
return unless request.class.respond_to?(:encode)
53+
54+
begin
55+
data = {
56+
class: request.class.name,
57+
payload: request.class.encode(request)
58+
}
59+
File.open(@req_file_path, "ab") do |f|
60+
Marshal.dump(data, f)
61+
end
62+
rescue StandardError => e
63+
warn "Failed to log request: #{e.message}"
5764
end
58-
rescue StandardError => e
59-
warn "Failed to log request: #{e.message}"
6065
end
6166

6267
def put_statement_result(sql, result)
@@ -123,20 +128,28 @@ def do_execute_sql(request, streaming)
123128

124129
raise @errors[request.sql].pop if @errors[request.sql] && !@errors[request.sql].empty?
125130

126-
result = get_statement_result(request.sql).clone
127-
128-
if result.result_type == StatementResult::EXCEPTION
129-
err_proto = result.result
130-
raise GRPC::BadStatus.new(err_proto.code, err_proto.message) if err_proto.is_a?(Google::Rpc::Status)
131-
132-
raise err_proto
133-
134-
end
131+
results_or_single = get_statement_result(request.sql)
132+
results = results_or_single.is_a?(Array) ? results_or_single : [results_or_single]
135133

134+
# Handle streaming vs non-streaming response
135+
# Streaming: return an enumerator that yields all result sets
136136
if streaming
137-
result.each created_transaction
137+
Enumerator.new do |yielder|
138+
results.each do |entry|
139+
result = entry.clone
140+
raise_if_error!(result)
141+
142+
result.each(created_transaction) do |partial|
143+
yielder << partial
144+
end
145+
end
146+
end
138147
else
139-
result.result created_transaction
148+
# Non-streaming: only return the first result set
149+
entry = results.first
150+
result = entry.clone
151+
raise_if_error!(result)
152+
result.result(created_transaction)
140153
end
141154
end
142155

@@ -331,4 +344,14 @@ def do_create_transaction(session)
331344
end
332345
transaction
333346
end
347+
348+
def raise_if_error!(result)
349+
return unless result.result_type == StatementResult::EXCEPTION
350+
351+
err_proto = result.result
352+
353+
raise GRPC::BadStatus.new(err_proto.code, err_proto.message) if err_proto.is_a?(Google::Rpc::Status)
354+
355+
raise err_proto
356+
end
334357
end

spannerlib/wrappers/spannerlib-ruby/spec/spannerlib_ruby_spec.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,36 @@ def find_transaction_options(requests)
352352
err = _ { @conn.execute_batch(batch_req) }.must_raise StandardError
353353
_(err.message).must_match(/Permission denied/)
354354
end
355+
356+
it "can execute a multi-statement query with multiple result sets" do
357+
sql = "SELECT 1; SELECT 2"
358+
set_mock_result("SELECT 1", StatementResult.create_select1_result)
359+
360+
set_mock_result(" SELECT 2", StatementResult.create_single_int_result_set("Col1", 2))
361+
362+
req = Google::Cloud::Spanner::V1::ExecuteSqlRequest.new(sql: sql)
363+
rows = @conn.execute(req)
364+
365+
row1 = rows.next
366+
_(row1).wont_be_nil
367+
decoded1 = Google::Protobuf::ListValue.decode(row1)
368+
_(decoded1.values[0].string_value).must_equal "1"
369+
370+
_(rows.next).must_be_nil
371+
372+
metadata = rows.next_result_set
373+
_(metadata).wont_be_nil
374+
_(metadata.row_type.fields[0].name).must_equal "Col1"
375+
376+
row2 = rows.next
377+
_(row2).wont_be_nil
378+
decoded2 = Google::Protobuf::ListValue.decode(row2)
379+
_(decoded2.values[0].string_value).must_equal "2"
380+
381+
_(rows.next).must_be_nil
382+
383+
_(rows.next_result_set).must_be_nil
384+
end
355385
end
356386
# rubocop:enable RSpec/NoExpectationExample
357387
# rubocop:enable Style/GlobalVars

0 commit comments

Comments
 (0)