Skip to content

Commit e8de7f7

Browse files
authored
refactor(ruby): Introduce MessageHandler and Rows classes (#553)
* refactor: add new message handler in ruby wrapper
1 parent 49e945e commit e8de7f7

File tree

8 files changed

+318
-76
lines changed

8 files changed

+318
-76
lines changed

spannerlib/wrappers/spannerlib-ruby/Rakefile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,3 @@ task :compile do
4242
end
4343

4444
task default: %i[compile spec rubocop]
45-

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

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# frozen_string_literal: true
1616

1717
require_relative "ffi"
18+
require_relative "rows"
1819

1920
class Connection
2021
attr_reader :pool_id, :conn_id
@@ -35,8 +36,7 @@ def write_mutations(mutation_group)
3536
else
3637
mutation_group.to_s
3738
end
38-
39-
SpannerLib.write_mutations(@pool_id, @conn_id, req_bytes)
39+
SpannerLib.write_mutations(@pool_id, @conn_id, req_bytes, proto_klass: Google::Cloud::Spanner::V1::CommitResponse)
4040
end
4141

4242
# Begin a read/write transaction on this connection. Accepts TransactionOptions proto or bytes.
@@ -52,7 +52,7 @@ def begin_transaction(transaction_options = nil)
5252

5353
# Commit the current transaction. Returns CommitResponse bytes or nil.
5454
def commit
55-
SpannerLib.commit(@pool_id, @conn_id)
55+
SpannerLib.commit(@pool_id, @conn_id, proto_klass: Google::Cloud::Spanner::V1::CommitResponse)
5656
end
5757

5858
# Rollback the current transaction.
@@ -68,7 +68,8 @@ def execute(request)
6868
else
6969
request.is_a?(String) ? request : request.to_s
7070
end
71-
SpannerLib.execute(@pool_id, @conn_id, bytes)
71+
rows_id = SpannerLib.execute(@pool_id, @conn_id, bytes)
72+
SpannerLib::Rows.new(self, rows_id)
7273
end
7374

7475
# Execute batch DML/DDL request. Returns ExecuteBatchDmlResponse bytes (or nil).
@@ -78,24 +79,8 @@ def execute_batch(request)
7879
else
7980
request.is_a?(String) ? request : request.to_s
8081
end
81-
SpannerLib.execute_batch(@pool_id, @conn_id, bytes)
82-
end
83-
84-
# Rows helpers — return raw message bytes (caller should parse them).
85-
def metadata(rows_id)
86-
SpannerLib.metadata(@pool_id, @conn_id, rows_id)
87-
end
88-
89-
def next_rows(rows_id, num_rows, encoding = 0)
90-
SpannerLib.next(@pool_id, @conn_id, rows_id, num_rows, encoding)
91-
end
92-
93-
def result_set_stats(rows_id)
94-
SpannerLib.result_set_stats(@pool_id, @conn_id, rows_id)
95-
end
9682

97-
def close_rows(rows_id)
98-
SpannerLib.close_rows(@pool_id, @conn_id, rows_id)
83+
SpannerLib.execute_batch(@pool_id, @conn_id, bytes, proto_klass: Google::Cloud::Spanner::V1::ExecuteBatchDmlResponse)
9984
end
10085

10186
# Closes this connection. Any active transaction on the connection is rolled back.

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

Lines changed: 17 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
require "google/rpc/status_pb"
2424

2525
require "ffi"
26+
require_relative "message_handler"
2627

2728
module SpannerLib
2829
extend FFI::Library
@@ -128,45 +129,25 @@ def self.ensure_release(message)
128129
end
129130
end
130131

131-
def self.handle_object_id_response(message, func_name)
132+
def self.handle_object_id_response(message, _func_name)
132133
ensure_release(message) do
133-
if message[:code] != 0
134-
error_msg = read_error_message(message)
135-
raise "#{func_name} failed with code #{message[:code]}: #{error_msg}"
136-
end
137-
message[:objectId]
134+
MessageHandler.new(message).object_id
138135
end
139136
end
140137

141-
def self.handle_status_response(message, func_name)
138+
def self.handle_status_response(message, _func_name)
142139
ensure_release(message) do
143-
if message[:code] != 0
144-
error_msg = read_error_message(message)
145-
raise "#{func_name} failed with code #{message[:code]}: #{error_msg}"
146-
end
140+
MessageHandler.new(message).throw_if_error!
147141
end
148142
nil
149143
end
150144

151-
# rubocop:disable Metrics/MethodLength
152-
def self.handle_data_response(message, func_name)
145+
def self.handle_data_response(message, _func_name, options = {})
146+
proto_klass = options[:proto_klass]
153147
ensure_release(message) do
154-
if message[:code] != 0
155-
error_msg = read_error_message(message)
156-
raise "#{func_name} failed with code #{message[:code]}: #{error_msg}"
157-
end
158-
159-
len = message[:length]
160-
ptr = message[:pointer]
161-
162-
if len.positive? && !ptr.null?
163-
ptr.read_bytes(len)
164-
else
165-
""
166-
end
148+
MessageHandler.new(message).data(proto_klass: proto_klass)
167149
end
168150
end
169-
# rubocop:enable Metrics/MethodLength
170151

171152
# rubocop:disable Metrics/MethodLength
172153
def self.read_error_message(message)
@@ -187,10 +168,11 @@ def self.read_error_message(message)
187168
end
188169
# rubocop:enable Metrics/MethodLength
189170

190-
def self.write_mutations(pool_id, conn_id, proto_bytes)
171+
def self.write_mutations(pool_id, conn_id, proto_bytes, options = {})
172+
proto_klass = options[:proto_klass]
191173
with_gobytes(proto_bytes) do |gobytes|
192174
message = WriteMutations(pool_id, conn_id, gobytes)
193-
handle_data_response(message, "WriteMutations")
175+
handle_data_response(message, "WriteMutations", proto_klass: proto_klass)
194176
end
195177
end
196178

@@ -201,9 +183,10 @@ def self.begin_transaction(pool_id, conn_id, proto_bytes)
201183
end
202184
end
203185

204-
def self.commit(pool_id, conn_id)
186+
def self.commit(pool_id, conn_id, options = {})
187+
proto_klass = options[:proto_klass]
205188
message = Commit(pool_id, conn_id)
206-
handle_data_response(message, "Commit")
189+
handle_data_response(message, "Commit", proto_klass: proto_klass)
207190
end
208191

209192
def self.rollback(pool_id, conn_id)
@@ -218,10 +201,11 @@ def self.execute(pool_id, conn_id, proto_bytes)
218201
end
219202
end
220203

221-
def self.execute_batch(pool_id, conn_id, proto_bytes)
204+
def self.execute_batch(pool_id, conn_id, proto_bytes, options = {})
205+
proto_klass = options[:proto_klass]
222206
with_gobytes(proto_bytes) do |gobytes|
223207
message = ExecuteBatch(pool_id, conn_id, gobytes)
224-
handle_data_response(message, "ExecuteBatch")
208+
handle_data_response(message, "ExecuteBatch", proto_klass: proto_klass)
225209
end
226210
end
227211

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# frozen_string_literal: true
16+
17+
# lib/spannerlib/message_handler.rb
18+
19+
require "spannerlib/exceptions"
20+
21+
module SpannerLib
22+
class MessageHandler
23+
def initialize(message)
24+
@message = message
25+
end
26+
27+
def object_id
28+
throw_if_error!
29+
@message[:objectId]
30+
end
31+
32+
# Returns the data payload from the message.
33+
# If a proto_klass is provided, it decodes the bytes into a Protobuf object.
34+
# Otherwise, it returns the raw bytes as a string.
35+
def data(proto_klass: nil)
36+
throw_if_error!
37+
38+
len = @message[:length]
39+
ptr = @message[:pointer]
40+
41+
return (proto_klass ? proto_klass.new : "") unless len.positive? && !ptr.null?
42+
43+
bytes = ptr.read_string(len)
44+
45+
proto_klass ? proto_klass.decode(bytes) : bytes
46+
end
47+
48+
def throw_if_error!
49+
code = @message[:code]
50+
return if code.zero?
51+
52+
error_msg = SpannerLib.read_error_message(@message)
53+
raise SpannerLibException, "Call failed with code #{code}: #{error_msg}"
54+
end
55+
end
56+
end
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# frozen_string_literal: true
16+
17+
module SpannerLib
18+
class Rows
19+
include Enumerable
20+
21+
attr_reader :id, :connection
22+
23+
def initialize(connection, rows_id)
24+
@connection = connection
25+
@id = rows_id
26+
@closed = false
27+
end
28+
29+
def each
30+
return enum_for(:each) unless block_given?
31+
32+
while (row = self.next)
33+
yield row
34+
end
35+
ensure
36+
close
37+
end
38+
39+
def next
40+
return nil if @closed
41+
42+
row_data = SpannerLib.next(connection.pool_id, connection.conn_id, id, 1, 0)
43+
44+
if row_data.nil? || row_data.empty? || (row_data.respond_to?(:values) && row_data.values.empty?)
45+
close
46+
return nil
47+
end
48+
49+
row_data
50+
end
51+
52+
def metadata
53+
SpannerLib.metadata(connection.pool_id, connection.conn_id, id)
54+
end
55+
56+
def result_set_stats
57+
SpannerLib.result_set_stats(connection.pool_id, connection.conn_id, id)
58+
end
59+
60+
def close
61+
return if @closed
62+
63+
SpannerLib.close_rows(connection.pool_id, connection.conn_id, id)
64+
@closed = true
65+
end
66+
end
67+
end

0 commit comments

Comments
 (0)