Skip to content

Commit bb87d96

Browse files
authored
feat(ruby): Add Ruby FFI wrapper for spannerlib (#545)
* feat(ruby): add Ruby FFI wrapper for spannerlib
1 parent fba2986 commit bb87d96

File tree

22 files changed

+1020
-3
lines changed

22 files changed

+1020
-3
lines changed

.github/workflows/integration-tests-on-emulator.yml

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ on:
22
push:
33
branches: [ main ]
44
pull_request:
5+
56
name: Integration tests on emulator
7+
68
jobs:
7-
test:
9+
# This is the original job, renamed for clarity
10+
test-go:
11+
name: Go Integration Tests
812
runs-on: ubuntu-latest
913
services:
1014
emulator:
@@ -18,11 +22,37 @@ jobs:
1822
with:
1923
go-version: 1.25.x
2024
- name: Checkout code
21-
uses: actions/checkout@v5
22-
- name: Run integration tests on emulator
25+
uses: actions/checkout@v4
26+
- name: Run Go integration tests on emulator
2327
run: go test -race
2428
env:
2529
JOB_TYPE: test
2630
SPANNER_EMULATOR_HOST: localhost:9010
2731
SPANNER_TEST_PROJECT: emulator-test-project
2832
SPANNER_TEST_INSTANCE: test-instance
33+
34+
test-ruby:
35+
name: Ruby Integration Tests
36+
runs-on: ubuntu-latest
37+
services:
38+
emulator:
39+
image: gcr.io/cloud-spanner-emulator/emulator:latest
40+
ports:
41+
- 9010:9010
42+
- 9020:9020
43+
steps:
44+
- name: Checkout code
45+
uses: actions/checkout@v5
46+
- name: Set up Ruby
47+
uses: ruby/setup-ruby@v1
48+
with:
49+
ruby-version: '3.3.1'
50+
working-directory: spannerlib/wrappers/spannerlib-ruby
51+
bundler-cache: true
52+
- name: Compile and Run Ruby Integration Tests
53+
working-directory: spannerlib/wrappers/spannerlib-ruby
54+
env:
55+
SPANNER_EMULATOR_HOST: localhost:9010
56+
run: |
57+
bundle exec rake compile
58+
bundle exec rspec spec/integration/

spannerlib/.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
spannerlib.h
22
spannerlib.so
33
grpc_server
4+
vendor/bundle
5+
shared/
6+
*.gem
7+
.DS_Store
8+
*.swp
9+
ext/
10+
Gemfile.lock
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/.bundle/
2+
/.yardoc
3+
/_yardoc/
4+
/coverage/
5+
/doc/
6+
/pkg/
7+
/spec/reports/
8+
/tmp/
9+
10+
.rspec_status
11+
/vendor/bundle
12+
/shared/
13+
*.gem
14+
15+
.DS_Store
16+
*.swp
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
--format documentation
2+
--color
3+
--require spec_helper
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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 "bundler/gem_tasks"
18+
require "rspec/core/rake_task"
19+
require "rubocop/rake_task"
20+
require "rbconfig"
21+
22+
RSpec::Core::RakeTask.new(:spec)
23+
24+
RuboCop::RakeTask.new
25+
26+
task :compile do
27+
go_source_dir = File.expand_path("../../shared", __dir__)
28+
target_dir = File.expand_path("lib/spannerlib/#{RbConfig::CONFIG['arch']}", __dir__)
29+
output_file = File.join(target_dir, "spannerlib.#{RbConfig::CONFIG['SOEXT']}")
30+
31+
mkdir_p target_dir
32+
33+
command = [
34+
"go", "build",
35+
"-buildmode=c-shared",
36+
"-o", output_file,
37+
go_source_dir
38+
].join(" ")
39+
40+
puts command
41+
sh command
42+
end
43+
44+
task default: %i[compile spec rubocop]
45+
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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__)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
IFS=$'\n\t'
4+
set -vx
5+
6+
bundle install
7+
8+
# Do any other automated setup that you need to do here
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
19+
class Connection
20+
attr_reader :pool_id, :conn_id
21+
22+
def initialize(pool_id, conn_id)
23+
@pool_id = pool_id
24+
@conn_id = conn_id
25+
end
26+
27+
# Accepts either an object that responds to `to_proto` or a raw string/bytes
28+
# containing the serialized mutation proto. We avoid requiring the protobuf
29+
# definitions at load time so specs that don't need them can run.
30+
def write_mutations(mutation_group)
31+
req_bytes = if mutation_group.respond_to?(:to_proto)
32+
mutation_group.to_proto
33+
elsif mutation_group.is_a?(String)
34+
mutation_group
35+
else
36+
mutation_group.to_s
37+
end
38+
39+
SpannerLib.write_mutations(@pool_id, @conn_id, req_bytes)
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)
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+
SpannerLib.execute(@pool_id, @conn_id, bytes)
72+
end
73+
74+
# Execute batch DML/DDL request. Returns ExecuteBatchDmlResponse bytes (or nil).
75+
def execute_batch(request)
76+
bytes = if request.respond_to?(:to_proto)
77+
request.to_proto
78+
else
79+
request.is_a?(String) ? request : request.to_s
80+
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
96+
97+
def close_rows(rows_id)
98+
SpannerLib.close_rows(@pool_id, @conn_id, rows_id)
99+
end
100+
101+
# Closes this connection. Any active transaction on the connection is rolled back.
102+
def close
103+
SpannerLib.close_connection(@pool_id, @conn_id)
104+
nil
105+
end
106+
end

0 commit comments

Comments
 (0)