spannerlib-ruby 0.1.0.alpha1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rspec +3 -0
- data/.rubocop.yml +32 -0
- data/Gemfile +14 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/lib/spannerlib/connection.rb +91 -0
- data/lib/spannerlib/darwin-binaries/aarch64-darwin/spannerlib.dylib +0 -0
- data/lib/spannerlib/darwin-binaries/x86_64-darwin/spannerlib.dylib +0 -0
- data/lib/spannerlib/exceptions.rb +24 -0
- data/lib/spannerlib/ffi.rb +290 -0
- data/lib/spannerlib/linux-binaries/aarch64-linux/spannerlib.so +0 -0
- data/lib/spannerlib/linux-binaries/x86_64-linux/spannerlib.so +0 -0
- data/lib/spannerlib/message_handler.rb +56 -0
- data/lib/spannerlib/pool.rb +63 -0
- data/lib/spannerlib/rows.rb +67 -0
- data/lib/spannerlib/ruby/version.rb +7 -0
- data/lib/spannerlib/ruby.rb +24 -0
- data/lib/spannerlib/windows-binaries/spannerlib.dll +0 -0
- data/sig/spannerlib/ruby.rbs +6 -0
- data/spannerlib-ruby.gemspec +52 -0
- data/spec/integration/batch_emulator_spec.rb +165 -0
- data/spec/integration/connection_emulator_spec.rb +151 -0
- data/spec/integration/pool_emulator_spec.rb +42 -0
- data/spec/spannerlib/connection_spec.rb +53 -0
- data/spec/spannerlib/pool_spec.rb +53 -0
- data/spec/spec_helper.rb +32 -0
- metadata +128 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 287861a42225f8adaa1798a29d737b323817c19c005abbbbe0b2d11ee8f2a7f8
|
|
4
|
+
data.tar.gz: 1ff412d54ad8567f3422d621cae5078ddf9cff11cdd0da5abeb40c80990ae549
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2d7fab0308ec1aa0989febf32dcb3096215e2ebc356aeb944fed7a02aa334325c6c88fdbd2f55e939a5ae11f40952d1f3539a2bc06febac94b9efd649b6e784a
|
|
7
|
+
data.tar.gz: 8f131835872c9aacdaba9c7af151f98be51096eae2851369695b83f628b31c9d96af34fdc688fcc5f676f726b37f10d0f799bad5be75b09b2954b5345b5f94a5
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
NewCops: enable
|
|
3
|
+
SuggestExtensions: false
|
|
4
|
+
Exclude:
|
|
5
|
+
- 'lib/spanner_pb.rb'
|
|
6
|
+
- 'vendor/**/*'
|
|
7
|
+
|
|
8
|
+
plugins:
|
|
9
|
+
- rubocop-rspec
|
|
10
|
+
|
|
11
|
+
Layout/LineLength:
|
|
12
|
+
Max: 150
|
|
13
|
+
|
|
14
|
+
Style/Documentation:
|
|
15
|
+
Enabled: false
|
|
16
|
+
|
|
17
|
+
RSpec/ExampleLength:
|
|
18
|
+
Enabled: false
|
|
19
|
+
RSpec/MultipleExpectations:
|
|
20
|
+
Enabled: false
|
|
21
|
+
|
|
22
|
+
# Add this block to disable the 'let' rule
|
|
23
|
+
RSpec/InstanceVariable:
|
|
24
|
+
Enabled: false
|
|
25
|
+
RSpec/BeforeAfterAll:
|
|
26
|
+
Enabled: false
|
|
27
|
+
RSpec/DescribeClass:
|
|
28
|
+
Exclude:
|
|
29
|
+
- 'spec/integration/**/*'
|
|
30
|
+
|
|
31
|
+
Style/StringLiterals:
|
|
32
|
+
EnforcedStyle: double_quotes
|
data/Gemfile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gemspec
|
|
6
|
+
|
|
7
|
+
gem "rake", "~> 13.0"
|
|
8
|
+
|
|
9
|
+
group :development, :test do
|
|
10
|
+
gem "rake-compiler", "~> 1.0"
|
|
11
|
+
gem "rspec", "~> 3.0"
|
|
12
|
+
gem "rubocop", require: false
|
|
13
|
+
gem "rubocop-rspec", require: false
|
|
14
|
+
end
|
data/bin/console
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "bundler/setup"
|
|
5
|
+
require "spannerlib/ruby"
|
|
6
|
+
|
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
9
|
+
|
|
10
|
+
require "irb"
|
|
11
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
require_relative "ffi"
|
|
18
|
+
require_relative "rows"
|
|
19
|
+
|
|
20
|
+
class Connection
|
|
21
|
+
attr_reader :pool_id, :conn_id
|
|
22
|
+
|
|
23
|
+
def initialize(pool_id, conn_id)
|
|
24
|
+
@pool_id = pool_id
|
|
25
|
+
@conn_id = conn_id
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Accepts either an object that responds to `to_proto` or a raw string/bytes
|
|
29
|
+
# containing the serialized mutation proto. We avoid requiring the protobuf
|
|
30
|
+
# definitions at load time so specs that don't need them can run.
|
|
31
|
+
def write_mutations(mutation_group)
|
|
32
|
+
req_bytes = if mutation_group.respond_to?(:to_proto)
|
|
33
|
+
mutation_group.to_proto
|
|
34
|
+
elsif mutation_group.is_a?(String)
|
|
35
|
+
mutation_group
|
|
36
|
+
else
|
|
37
|
+
mutation_group.to_s
|
|
38
|
+
end
|
|
39
|
+
SpannerLib.write_mutations(@pool_id, @conn_id, req_bytes, proto_klass: Google::Cloud::Spanner::V1::CommitResponse)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Begin a read/write transaction on this connection. Accepts TransactionOptions proto or bytes.
|
|
43
|
+
# Returns message bytes (or nil) — higher-level parsing not implemented here.
|
|
44
|
+
def begin_transaction(transaction_options = nil)
|
|
45
|
+
bytes = if transaction_options.respond_to?(:to_proto)
|
|
46
|
+
transaction_options.to_proto
|
|
47
|
+
else
|
|
48
|
+
transaction_options.is_a?(String) ? transaction_options : transaction_options&.to_s
|
|
49
|
+
end
|
|
50
|
+
SpannerLib.begin_transaction(@pool_id, @conn_id, bytes)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Commit the current transaction. Returns CommitResponse bytes or nil.
|
|
54
|
+
def commit
|
|
55
|
+
SpannerLib.commit(@pool_id, @conn_id, proto_klass: Google::Cloud::Spanner::V1::CommitResponse)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Rollback the current transaction.
|
|
59
|
+
def rollback
|
|
60
|
+
SpannerLib.rollback(@pool_id, @conn_id)
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Execute SQL request (expects a request object with to_proto or raw bytes). Returns message bytes (or nil).
|
|
65
|
+
def execute(request)
|
|
66
|
+
bytes = if request.respond_to?(:to_proto)
|
|
67
|
+
request.to_proto
|
|
68
|
+
else
|
|
69
|
+
request.is_a?(String) ? request : request.to_s
|
|
70
|
+
end
|
|
71
|
+
rows_id = SpannerLib.execute(@pool_id, @conn_id, bytes)
|
|
72
|
+
SpannerLib::Rows.new(self, rows_id)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Execute batch DML/DDL request. Returns ExecuteBatchDmlResponse bytes (or nil).
|
|
76
|
+
def execute_batch(request)
|
|
77
|
+
bytes = if request.respond_to?(:to_proto)
|
|
78
|
+
request.to_proto
|
|
79
|
+
else
|
|
80
|
+
request.is_a?(String) ? request : request.to_s
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
SpannerLib.execute_batch(@pool_id, @conn_id, bytes, proto_klass: Google::Cloud::Spanner::V1::ExecuteBatchDmlResponse)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Closes this connection. Any active transaction on the connection is rolled back.
|
|
87
|
+
def close
|
|
88
|
+
SpannerLib.close_connection(@pool_id, @conn_id)
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
end
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
class SpannerLibException < StandardError
|
|
18
|
+
attr_reader :status
|
|
19
|
+
|
|
20
|
+
def initialize(msg = nil, status = nil)
|
|
21
|
+
super(msg)
|
|
22
|
+
@status = status
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,290 @@
|
|
|
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
|
+
# rubocop:disable Metrics/ModuleLength
|
|
18
|
+
|
|
19
|
+
require "rubygems"
|
|
20
|
+
require "bundler/setup"
|
|
21
|
+
|
|
22
|
+
require "google/protobuf"
|
|
23
|
+
require "google/rpc/status_pb"
|
|
24
|
+
|
|
25
|
+
require "ffi"
|
|
26
|
+
require_relative "message_handler"
|
|
27
|
+
|
|
28
|
+
module SpannerLib
|
|
29
|
+
extend FFI::Library
|
|
30
|
+
|
|
31
|
+
ENV_OVERRIDE = ENV.fetch("SPANNERLIB_PATH", nil)
|
|
32
|
+
|
|
33
|
+
def self.platform_dir_from_host
|
|
34
|
+
host_os = RbConfig::CONFIG["host_os"]
|
|
35
|
+
host_cpu = RbConfig::CONFIG["host_cpu"]
|
|
36
|
+
|
|
37
|
+
case host_os
|
|
38
|
+
when /darwin/
|
|
39
|
+
host_cpu =~ /arm|aarch64/ ? "aarch64-darwin" : "x86_64-darwin"
|
|
40
|
+
when /linux/
|
|
41
|
+
host_cpu =~ /arm|aarch64/ ? "aarch64-linux" : "x86_64-linux"
|
|
42
|
+
when /mswin|mingw|cygwin/
|
|
43
|
+
"x64-mingw32"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Build list of candidate paths (ordered): env override, platform-specific, any packaged lib, system library
|
|
48
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
49
|
+
def self.library_path
|
|
50
|
+
if ENV_OVERRIDE && !ENV_OVERRIDE.empty?
|
|
51
|
+
return ENV_OVERRIDE if File.file?(ENV_OVERRIDE)
|
|
52
|
+
|
|
53
|
+
warn "SPANNERLIB_PATH set to #{ENV_OVERRIDE} but file not found"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
lib_dir = File.expand_path(__dir__)
|
|
57
|
+
ext = FFI::Platform::LIBSUFFIX
|
|
58
|
+
|
|
59
|
+
platform = platform_dir_from_host
|
|
60
|
+
if platform
|
|
61
|
+
candidate = File.join(lib_dir, platform, "spannerlib.#{ext}")
|
|
62
|
+
return candidate if File.exist?(candidate)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# 3) Any matching packaged binary (first match)
|
|
66
|
+
glob_candidates = Dir.glob(File.join(lib_dir, "*", "spannerlib.#{ext}"))
|
|
67
|
+
return glob_candidates.first unless glob_candidates.empty?
|
|
68
|
+
|
|
69
|
+
# 4) Try loading system-wide library (so users who installed shared lib separately can use it)
|
|
70
|
+
begin
|
|
71
|
+
# Attempt to open system lib name; if succeeds, return bare name so ffi_lib can resolve it
|
|
72
|
+
FFI::DynamicLibrary.open("spannerlib", FFI::DynamicLibrary::RTLD_LAZY | FFI::DynamicLibrary::RTLD_GLOBAL)
|
|
73
|
+
return "spannerlib"
|
|
74
|
+
rescue LoadError
|
|
75
|
+
# This is intentional. If the system library fails to load,
|
|
76
|
+
# we'll proceed to the final LoadError with all search paths.
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
searched = []
|
|
80
|
+
searched << "ENV SPANNERLIB_PATH=#{ENV_OVERRIDE}" if ENV_OVERRIDE && !ENV_OVERRIDE.empty?
|
|
81
|
+
searched << File.join(lib_dir, platform || "<detected-platform?>", "spannerlib.#{ext}")
|
|
82
|
+
searched << File.join(lib_dir, "*", "spannerlib.#{ext}")
|
|
83
|
+
|
|
84
|
+
raise LoadError, <<~ERR
|
|
85
|
+
Could not locate the spannerlib native library. Tried:
|
|
86
|
+
- #{searched.join("\n - ")}
|
|
87
|
+
If you are using the packaged gem, ensure the gem includes lib/spannerlib/<platform>/spannerlib.#{ext}.
|
|
88
|
+
You can set SPANNERLIB_PATH to the absolute path of the library file, or install a platform-specific native gem.
|
|
89
|
+
ERR
|
|
90
|
+
end
|
|
91
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
92
|
+
|
|
93
|
+
ffi_lib library_path
|
|
94
|
+
|
|
95
|
+
class GoString < FFI::Struct
|
|
96
|
+
layout :p, :pointer,
|
|
97
|
+
:len, :long
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# GoBytes is the Ruby representation of a Go byte slice
|
|
101
|
+
class GoBytes < FFI::Struct
|
|
102
|
+
layout :p, :pointer,
|
|
103
|
+
:len, :long,
|
|
104
|
+
:cap, :long
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Message is the common return type for all native functions.
|
|
108
|
+
class Message < FFI::Struct
|
|
109
|
+
layout :pinner, :long_long,
|
|
110
|
+
:code, :int,
|
|
111
|
+
:objectId, :long_long,
|
|
112
|
+
:length, :int,
|
|
113
|
+
:pointer, :pointer
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# --- Native Function Signatures ---
|
|
117
|
+
attach_function :CreatePool, [GoString.by_value], Message.by_value
|
|
118
|
+
attach_function :ClosePool, [:int64], Message.by_value
|
|
119
|
+
attach_function :CreateConnection, [:int64], Message.by_value
|
|
120
|
+
attach_function :CloseConnection, %i[int64 int64], Message.by_value
|
|
121
|
+
attach_function :WriteMutations, [:int64, :int64, GoBytes.by_value], Message.by_value
|
|
122
|
+
attach_function :BeginTransaction, [:int64, :int64, GoBytes.by_value], Message.by_value
|
|
123
|
+
attach_function :Commit, %i[int64 int64], Message.by_value
|
|
124
|
+
attach_function :Rollback, %i[int64 int64], Message.by_value
|
|
125
|
+
attach_function :Execute, [:int64, :int64, GoBytes.by_value], Message.by_value
|
|
126
|
+
attach_function :ExecuteBatch, [:int64, :int64, GoBytes.by_value], Message.by_value
|
|
127
|
+
attach_function :Metadata, %i[int64 int64 int64], Message.by_value
|
|
128
|
+
attach_function :Next, %i[int64 int64 int64 int32 int32], Message.by_value
|
|
129
|
+
attach_function :ResultSetStats, %i[int64 int64 int64], Message.by_value
|
|
130
|
+
attach_function :CloseRows, %i[int64 int64 int64], Message.by_value
|
|
131
|
+
attach_function :Release, [:int64], :void
|
|
132
|
+
|
|
133
|
+
# --- Ruby-friendly Wrappers ---
|
|
134
|
+
|
|
135
|
+
def self.create_pool(dsn)
|
|
136
|
+
dsn_str = dsn.to_s.dup
|
|
137
|
+
dsn_ptr = FFI::MemoryPointer.from_string(dsn_str)
|
|
138
|
+
|
|
139
|
+
go_dsn = GoString.new
|
|
140
|
+
go_dsn[:p] = dsn_ptr
|
|
141
|
+
go_dsn[:len] = dsn_str.bytesize
|
|
142
|
+
|
|
143
|
+
message = CreatePool(go_dsn)
|
|
144
|
+
handle_object_id_response(message, "CreatePool")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def self.close_pool(pool_id)
|
|
148
|
+
message = ClosePool(pool_id)
|
|
149
|
+
handle_status_response(message, "ClosePool")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def self.create_connection(pool_id)
|
|
153
|
+
message = CreateConnection(pool_id)
|
|
154
|
+
handle_object_id_response(message, "CreateConnection")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def self.close_connection(pool_id, conn_id)
|
|
158
|
+
message = CloseConnection(pool_id, conn_id)
|
|
159
|
+
handle_status_response(message, "CloseConnection")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def self.release(pinner)
|
|
163
|
+
Release(pinner)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def self.with_gobytes(bytes)
|
|
167
|
+
bytes ||= ""
|
|
168
|
+
len = bytes.bytesize
|
|
169
|
+
ptr = FFI::MemoryPointer.new(len)
|
|
170
|
+
ptr.write_bytes(bytes, 0, len) if len.positive?
|
|
171
|
+
|
|
172
|
+
go_bytes = GoBytes.new
|
|
173
|
+
go_bytes[:p] = ptr
|
|
174
|
+
go_bytes[:len] = len
|
|
175
|
+
go_bytes[:cap] = len
|
|
176
|
+
|
|
177
|
+
yield(go_bytes)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def self.ensure_release(message)
|
|
181
|
+
pinner = message[:pinner]
|
|
182
|
+
begin
|
|
183
|
+
yield
|
|
184
|
+
ensure
|
|
185
|
+
release(pinner) if pinner != 0
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def self.handle_object_id_response(message, _func_name)
|
|
190
|
+
ensure_release(message) do
|
|
191
|
+
MessageHandler.new(message).object_id
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def self.handle_status_response(message, _func_name)
|
|
196
|
+
ensure_release(message) do
|
|
197
|
+
MessageHandler.new(message).throw_if_error!
|
|
198
|
+
end
|
|
199
|
+
nil
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def self.handle_data_response(message, _func_name, options = {})
|
|
203
|
+
proto_klass = options[:proto_klass]
|
|
204
|
+
ensure_release(message) do
|
|
205
|
+
MessageHandler.new(message).data(proto_klass: proto_klass)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# rubocop:disable Metrics/MethodLength
|
|
210
|
+
def self.read_error_message(message)
|
|
211
|
+
len = message[:length]
|
|
212
|
+
ptr = message[:pointer]
|
|
213
|
+
if len.positive? && !ptr.null?
|
|
214
|
+
raw_bytes = ptr.read_bytes(len)
|
|
215
|
+
begin
|
|
216
|
+
status_proto = ::Google::Rpc::Status.decode(raw_bytes)
|
|
217
|
+
"Status Proto { code: #{status_proto.code}, message: '#{status_proto.message}' }"
|
|
218
|
+
rescue StandardError => e
|
|
219
|
+
clean_string = raw_bytes.encode("UTF-8", invalid: :replace, undef: :replace, replace: "?").strip
|
|
220
|
+
"Failed to decode Status proto (code #{message[:code]}): #{e.class}: #{e.message} | Raw: #{clean_string}"
|
|
221
|
+
end
|
|
222
|
+
else
|
|
223
|
+
"No error message provided"
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
# rubocop:enable Metrics/MethodLength
|
|
227
|
+
|
|
228
|
+
def self.write_mutations(pool_id, conn_id, proto_bytes, options = {})
|
|
229
|
+
proto_klass = options[:proto_klass]
|
|
230
|
+
with_gobytes(proto_bytes) do |gobytes|
|
|
231
|
+
message = WriteMutations(pool_id, conn_id, gobytes)
|
|
232
|
+
handle_data_response(message, "WriteMutations", proto_klass: proto_klass)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def self.begin_transaction(pool_id, conn_id, proto_bytes)
|
|
237
|
+
with_gobytes(proto_bytes) do |gobytes|
|
|
238
|
+
message = BeginTransaction(pool_id, conn_id, gobytes)
|
|
239
|
+
handle_data_response(message, "BeginTransaction")
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def self.commit(pool_id, conn_id, options = {})
|
|
244
|
+
proto_klass = options[:proto_klass]
|
|
245
|
+
message = Commit(pool_id, conn_id)
|
|
246
|
+
handle_data_response(message, "Commit", proto_klass: proto_klass)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def self.rollback(pool_id, conn_id)
|
|
250
|
+
message = Rollback(pool_id, conn_id)
|
|
251
|
+
handle_status_response(message, "Rollback")
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def self.execute(pool_id, conn_id, proto_bytes)
|
|
255
|
+
with_gobytes(proto_bytes) do |gobytes|
|
|
256
|
+
message = Execute(pool_id, conn_id, gobytes)
|
|
257
|
+
handle_object_id_response(message, "Execute")
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def self.execute_batch(pool_id, conn_id, proto_bytes, options = {})
|
|
262
|
+
proto_klass = options[:proto_klass]
|
|
263
|
+
with_gobytes(proto_bytes) do |gobytes|
|
|
264
|
+
message = ExecuteBatch(pool_id, conn_id, gobytes)
|
|
265
|
+
handle_data_response(message, "ExecuteBatch", proto_klass: proto_klass)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def self.metadata(pool_id, conn_id, rows_id)
|
|
270
|
+
message = Metadata(pool_id, conn_id, rows_id)
|
|
271
|
+
handle_data_response(message, "Metadata")
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def self.next(pool_id, conn_id, rows_id, max_rows, fetch_size)
|
|
275
|
+
message = Next(pool_id, conn_id, rows_id, max_rows, fetch_size)
|
|
276
|
+
handle_data_response(message, "Next")
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def self.result_set_stats(pool_id, conn_id, rows_id)
|
|
280
|
+
message = ResultSetStats(pool_id, conn_id, rows_id)
|
|
281
|
+
handle_data_response(message, "ResultSetStats")
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def self.close_rows(pool_id, conn_id, rows_id)
|
|
285
|
+
message = CloseRows(pool_id, conn_id, rows_id)
|
|
286
|
+
handle_status_response(message, "CloseRows")
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# rubocop:enable Metrics/ModuleLength
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
require_relative "ffi"
|
|
18
|
+
require_relative "connection"
|
|
19
|
+
|
|
20
|
+
class Pool
|
|
21
|
+
attr_reader :id
|
|
22
|
+
|
|
23
|
+
def initialize(id)
|
|
24
|
+
@id = id
|
|
25
|
+
@closed = false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Create a new Pool given a DSN string. Raises SpannerLibException on failure.
|
|
29
|
+
def self.create_pool(dsn)
|
|
30
|
+
begin
|
|
31
|
+
pool_id = SpannerLib.create_pool(dsn)
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
raise SpannerLibException, e.message
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
raise SpannerLibException, "failed to create pool" if pool_id.nil? || pool_id <= 0
|
|
37
|
+
|
|
38
|
+
Pool.new(pool_id)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Close this pool and free native resources.
|
|
42
|
+
def close
|
|
43
|
+
return if @closed
|
|
44
|
+
|
|
45
|
+
SpannerLib.close_pool(@id)
|
|
46
|
+
@closed = true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Create a new Connection associated with this Pool.
|
|
50
|
+
def create_connection
|
|
51
|
+
raise SpannerLibException, "pool closed" if @closed
|
|
52
|
+
|
|
53
|
+
begin
|
|
54
|
+
conn_id = SpannerLib.create_connection(@id)
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
raise SpannerLibException, e.message
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
raise SpannerLibException, "failed to create connection" if conn_id.nil? || conn_id <= 0
|
|
60
|
+
|
|
61
|
+
Connection.new(@id, conn_id)
|
|
62
|
+
end
|
|
63
|
+
end
|