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
|
@@ -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,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
|
+
require_relative "ruby/version"
|
|
18
|
+
|
|
19
|
+
module Spannerlib
|
|
20
|
+
module Ruby
|
|
21
|
+
class Error < StandardError; end
|
|
22
|
+
# Your code goes here...
|
|
23
|
+
end
|
|
24
|
+
end
|
|
Binary file
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/spannerlib/ruby/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "spannerlib-ruby"
|
|
7
|
+
spec.version = Spannerlib::Ruby::VERSION
|
|
8
|
+
spec.authors = ["Google LLC"]
|
|
9
|
+
spec.email = ["cloud-spanner-developers@googlegroups.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Ruby wrapper for the Spanner native library"
|
|
12
|
+
spec.description = "Lightweight Ruby FFI bindings for the Spanner native library produced from the Go implementation."
|
|
13
|
+
spec.homepage = "https://github.com/googleapis/go-sql-spanner/tree/main/spannerlib/wrappers/spannerlib-ruby"
|
|
14
|
+
spec.license = "Apache 2.0 License"
|
|
15
|
+
spec.required_ruby_version = ">= 3.1.0"
|
|
16
|
+
|
|
17
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
18
|
+
|
|
19
|
+
# Include both git-tracked files (for local development) and any built native libraries
|
|
20
|
+
# that exist on disk (for CI). We do this so:
|
|
21
|
+
# - During local development, spec.files is driven by `git ls-files` (keeps the gem manifest clean).
|
|
22
|
+
# - In CI we build native shared libraries into lib/spannerlib/<platform>/ and those files are
|
|
23
|
+
# not checked into git; we therefore also glob lib/spannerlib/** to pick up the CI-built binaries
|
|
24
|
+
# so the gem produced in CI actually contains the native libraries.
|
|
25
|
+
# - We explicitly filter out common build artifacts and non-distributable files to avoid accidentally
|
|
26
|
+
# packaging object files, headers, or temporary files.
|
|
27
|
+
# This allows us to publish a single multi-platform gem that contains prebuilt shared libraries
|
|
28
|
+
|
|
29
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
30
|
+
files = []
|
|
31
|
+
# prefer git-tracked files when available (local dev), but also pick up built files present on disk (CI)
|
|
32
|
+
files += `git ls-files -z`.split("\x0") if system("git rev-parse --is-inside-work-tree > /dev/null 2>&1")
|
|
33
|
+
|
|
34
|
+
# include any built native libs (CI places them under lib/spannerlib/)
|
|
35
|
+
files += Dir.glob("lib/spannerlib/**/*").select { |f| File.file?(f) }
|
|
36
|
+
|
|
37
|
+
# dedupe and reject unwanted entries
|
|
38
|
+
files.map! { |f| f.sub(%r{\A\./}, "") }.uniq!
|
|
39
|
+
files.reject do |f|
|
|
40
|
+
f.match(%r{^(pkg|Gemfile\.lock|.*\.gem|Rakefile|spec/|.*\.o|.*\.h)$})
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
spec.bindir = "exe"
|
|
45
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
46
|
+
spec.require_paths = ["lib"]
|
|
47
|
+
|
|
48
|
+
spec.add_dependency "ffi"
|
|
49
|
+
spec.add_dependency "google-cloud-spanner-v1", "~> 1.7"
|
|
50
|
+
spec.add_dependency "google-protobuf", "~> 3.19"
|
|
51
|
+
spec.add_dependency "grpc", "~> 1.60"
|
|
52
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
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 "spec_helper"
|
|
18
|
+
require "google/cloud/spanner/v1"
|
|
19
|
+
|
|
20
|
+
RSpec.describe "Batch API test", :integration do
|
|
21
|
+
before(:all) do
|
|
22
|
+
@emulator_host = ENV.fetch("SPANNER_EMULATOR_HOST", nil)
|
|
23
|
+
skip "SPANNER_EMULATOR_HOST not set" unless @emulator_host && !@emulator_host.empty?
|
|
24
|
+
|
|
25
|
+
require "spannerlib/pool"
|
|
26
|
+
@dsn = "projects/your-project-id/instances/test-instance/databases/test-database?autoConfigEmulator=true"
|
|
27
|
+
|
|
28
|
+
@pool = Pool.create_pool(@dsn)
|
|
29
|
+
conn = @pool.create_connection
|
|
30
|
+
ddl_batch_req = Google::Cloud::Spanner::V1::ExecuteBatchDmlRequest.new(
|
|
31
|
+
statements: [
|
|
32
|
+
Google::Cloud::Spanner::V1::ExecuteBatchDmlRequest::Statement.new(sql: "DROP TABLE IF EXISTS test_table"),
|
|
33
|
+
Google::Cloud::Spanner::V1::ExecuteBatchDmlRequest::Statement.new(
|
|
34
|
+
sql: "CREATE TABLE test_table (id INT64 NOT NULL, name STRING(100)) PRIMARY KEY(id)"
|
|
35
|
+
)
|
|
36
|
+
]
|
|
37
|
+
)
|
|
38
|
+
conn.execute_batch(ddl_batch_req)
|
|
39
|
+
conn.close
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
after(:all) do
|
|
43
|
+
@pool&.close
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
before do
|
|
47
|
+
@conn = @pool.create_connection
|
|
48
|
+
delete_req = Google::Cloud::Spanner::V1::BatchWriteRequest::MutationGroup.new(
|
|
49
|
+
mutations: [
|
|
50
|
+
Google::Cloud::Spanner::V1::Mutation.new(
|
|
51
|
+
delete: Google::Cloud::Spanner::V1::Mutation::Delete.new(
|
|
52
|
+
table: "test_table",
|
|
53
|
+
key_set: Google::Cloud::Spanner::V1::KeySet.new(all: true)
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
]
|
|
57
|
+
)
|
|
58
|
+
@conn.write_mutations(delete_req)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
after do
|
|
62
|
+
@conn&.close
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "tests a batch DML request" do
|
|
66
|
+
dml_batch_req = Google::Cloud::Spanner::V1::ExecuteBatchDmlRequest.new(
|
|
67
|
+
statements: [
|
|
68
|
+
Google::Cloud::Spanner::V1::ExecuteBatchDmlRequest::Statement.new(
|
|
69
|
+
sql: "INSERT INTO test_table (id, name) VALUES (1, 'name1')"
|
|
70
|
+
),
|
|
71
|
+
Google::Cloud::Spanner::V1::ExecuteBatchDmlRequest::Statement.new(
|
|
72
|
+
sql: "INSERT INTO test_table (id, name) VALUES (2, 'name2')"
|
|
73
|
+
),
|
|
74
|
+
Google::Cloud::Spanner::V1::ExecuteBatchDmlRequest::Statement.new(
|
|
75
|
+
sql: "UPDATE test_table SET name='name3' WHERE id=1"
|
|
76
|
+
)
|
|
77
|
+
]
|
|
78
|
+
)
|
|
79
|
+
resp = @conn.execute_batch(dml_batch_req)
|
|
80
|
+
expect(resp.result_sets.length).to eq 3
|
|
81
|
+
|
|
82
|
+
select_req = Google::Cloud::Spanner::V1::ExecuteSqlRequest.new(
|
|
83
|
+
sql: "SELECT id, name FROM test_table ORDER BY id"
|
|
84
|
+
)
|
|
85
|
+
rows = @conn.execute(select_req)
|
|
86
|
+
all_rows = rows.map { |row_bytes| Google::Protobuf::ListValue.decode(row_bytes) }
|
|
87
|
+
|
|
88
|
+
expect(all_rows.length).to eq 2
|
|
89
|
+
expect(all_rows[0].values[0].string_value).to eq "1"
|
|
90
|
+
expect(all_rows[0].values[1].string_value).to eq "name3"
|
|
91
|
+
expect(all_rows[1].values[0].string_value).to eq "2"
|
|
92
|
+
expect(all_rows[1].values[1].string_value).to eq "name2"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "tests a batch DDL request" do
|
|
96
|
+
ddl_batch_req = Google::Cloud::Spanner::V1::ExecuteBatchDmlRequest.new(
|
|
97
|
+
statements: [
|
|
98
|
+
Google::Cloud::Spanner::V1::ExecuteBatchDmlRequest::Statement.new(
|
|
99
|
+
sql: "DROP TABLE IF EXISTS test_table"
|
|
100
|
+
),
|
|
101
|
+
Google::Cloud::Spanner::V1::ExecuteBatchDmlRequest::Statement.new(
|
|
102
|
+
sql: "CREATE TABLE test_table (key INT64 NOT NULL, data STRING(MAX)) PRIMARY KEY(key)"
|
|
103
|
+
)
|
|
104
|
+
]
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
expect { @conn.execute_batch(ddl_batch_req) }.not_to raise_error
|
|
108
|
+
|
|
109
|
+
insert_req = Google::Cloud::Spanner::V1::BatchWriteRequest::MutationGroup.new(
|
|
110
|
+
mutations: [
|
|
111
|
+
Google::Cloud::Spanner::V1::Mutation.new(
|
|
112
|
+
insert: Google::Cloud::Spanner::V1::Mutation::Write.new(
|
|
113
|
+
table: "test_table",
|
|
114
|
+
columns: %w[key data],
|
|
115
|
+
values: [
|
|
116
|
+
Google::Protobuf::ListValue.new(values: [
|
|
117
|
+
Google::Protobuf::Value.new(string_value: "101"),
|
|
118
|
+
Google::Protobuf::Value.new(string_value: "VerificationData")
|
|
119
|
+
])
|
|
120
|
+
]
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
]
|
|
124
|
+
)
|
|
125
|
+
expect { @conn.write_mutations(insert_req) }.not_to raise_error
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "queries data using parameters" do
|
|
129
|
+
insert_req = Google::Cloud::Spanner::V1::BatchWriteRequest::MutationGroup.new(
|
|
130
|
+
mutations: [
|
|
131
|
+
Google::Cloud::Spanner::V1::Mutation.new(
|
|
132
|
+
insert: Google::Cloud::Spanner::V1::Mutation::Write.new(
|
|
133
|
+
table: "test_table",
|
|
134
|
+
columns: %w[key data],
|
|
135
|
+
values: [
|
|
136
|
+
Google::Protobuf::ListValue.new(values: [Google::Protobuf::Value.new(string_value: "1"),
|
|
137
|
+
Google::Protobuf::Value.new(string_value: "Alice")]),
|
|
138
|
+
Google::Protobuf::ListValue.new(values: [Google::Protobuf::Value.new(string_value: "2"),
|
|
139
|
+
Google::Protobuf::Value.new(string_value: "Bob")])
|
|
140
|
+
]
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
]
|
|
144
|
+
)
|
|
145
|
+
@conn.write_mutations(insert_req)
|
|
146
|
+
|
|
147
|
+
# Execute the parameterized query.
|
|
148
|
+
select_req = Google::Cloud::Spanner::V1::ExecuteSqlRequest.new(
|
|
149
|
+
sql: "SELECT key, data FROM test_table WHERE data = @dataParam",
|
|
150
|
+
params: Google::Protobuf::Struct.new(
|
|
151
|
+
fields: {
|
|
152
|
+
"dataParam" => Google::Protobuf::Value.new(string_value: "Alice")
|
|
153
|
+
}
|
|
154
|
+
),
|
|
155
|
+
param_types: {
|
|
156
|
+
"dataParam" => Google::Cloud::Spanner::V1::Type.new(code: Google::Cloud::Spanner::V1::TypeCode::STRING)
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
rows = @conn.execute(select_req)
|
|
160
|
+
all_rows = rows.map { |row_bytes| Google::Protobuf::ListValue.decode(row_bytes) }
|
|
161
|
+
|
|
162
|
+
expect(all_rows.length).to eq(1)
|
|
163
|
+
expect(all_rows[0].values[1].string_value).to eq("Alice")
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
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 "spec_helper"
|
|
18
|
+
require "google/cloud/spanner/v1"
|
|
19
|
+
|
|
20
|
+
RSpec.describe "Connection APIs against Spanner emulator", :integration do
|
|
21
|
+
before(:all) do
|
|
22
|
+
@emulator_host = ENV.fetch("SPANNER_EMULATOR_HOST", nil)
|
|
23
|
+
skip "SPANNER_EMULATOR_HOST not set" unless @emulator_host && !@emulator_host.empty?
|
|
24
|
+
|
|
25
|
+
require "spannerlib/pool"
|
|
26
|
+
@dsn = "projects/your-project-id/instances/test-instance/databases/test-database?autoConfigEmulator=true"
|
|
27
|
+
|
|
28
|
+
pool = Pool.create_pool(@dsn)
|
|
29
|
+
conn = pool.create_connection
|
|
30
|
+
ddl_batch_req = Google::Cloud::Spanner::V1::ExecuteBatchDmlRequest.new(
|
|
31
|
+
statements: [
|
|
32
|
+
Google::Cloud::Spanner::V1::ExecuteBatchDmlRequest::Statement.new(sql: "DROP TABLE IF EXISTS test_table"),
|
|
33
|
+
Google::Cloud::Spanner::V1::ExecuteBatchDmlRequest::Statement.new(
|
|
34
|
+
sql: "CREATE TABLE test_table (id INT64 NOT NULL, name STRING(100)) PRIMARY KEY(id)"
|
|
35
|
+
)
|
|
36
|
+
]
|
|
37
|
+
)
|
|
38
|
+
conn.execute_batch(ddl_batch_req)
|
|
39
|
+
conn.close
|
|
40
|
+
pool.close
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
before do
|
|
44
|
+
@pool = Pool.create_pool(@dsn)
|
|
45
|
+
@conn = @pool.create_connection
|
|
46
|
+
delete_req = Google::Cloud::Spanner::V1::BatchWriteRequest::MutationGroup.new(
|
|
47
|
+
mutations: [
|
|
48
|
+
Google::Cloud::Spanner::V1::Mutation.new(
|
|
49
|
+
delete: Google::Cloud::Spanner::V1::Mutation::Delete.new(
|
|
50
|
+
table: "test_table",
|
|
51
|
+
key_set: Google::Cloud::Spanner::V1::KeySet.new(all: true)
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
]
|
|
55
|
+
)
|
|
56
|
+
@conn.write_mutations(delete_req)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
after do
|
|
60
|
+
@conn.close
|
|
61
|
+
@pool.close
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "creates a connection pool" do
|
|
65
|
+
expect(@pool.id).to be > 0
|
|
66
|
+
expect(@conn.conn_id).to be > 0
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "creates two connections from the same pool" do
|
|
70
|
+
conn2 = @pool.create_connection
|
|
71
|
+
expect(@conn.conn_id).not_to eq(conn2.conn_id)
|
|
72
|
+
expect(conn2.conn_id).to be > 0
|
|
73
|
+
conn2.close
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "writes and reads data in a read-write transaction" do
|
|
77
|
+
@conn.begin_transaction
|
|
78
|
+
insert_data_req = Google::Cloud::Spanner::V1::BatchWriteRequest::MutationGroup.new(
|
|
79
|
+
mutations: [
|
|
80
|
+
Google::Cloud::Spanner::V1::Mutation.new(
|
|
81
|
+
insert: Google::Cloud::Spanner::V1::Mutation::Write.new(
|
|
82
|
+
table: "test_table",
|
|
83
|
+
columns: %w[id name],
|
|
84
|
+
values: [
|
|
85
|
+
Google::Protobuf::ListValue.new(values: [Google::Protobuf::Value.new(string_value: "1"),
|
|
86
|
+
Google::Protobuf::Value.new(string_value: "Alice")]),
|
|
87
|
+
Google::Protobuf::ListValue.new(values: [Google::Protobuf::Value.new(string_value: "2"),
|
|
88
|
+
Google::Protobuf::Value.new(string_value: "Bob")])
|
|
89
|
+
]
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
]
|
|
93
|
+
)
|
|
94
|
+
@conn.write_mutations(insert_data_req)
|
|
95
|
+
@conn.commit
|
|
96
|
+
|
|
97
|
+
select_req = Google::Cloud::Spanner::V1::ExecuteSqlRequest.new(sql: "SELECT id, name FROM test_table ORDER BY id")
|
|
98
|
+
rows = @conn.execute(select_req)
|
|
99
|
+
|
|
100
|
+
all_rows = rows.map { |row_bytes| Google::Protobuf::ListValue.decode(row_bytes) }
|
|
101
|
+
|
|
102
|
+
expect(all_rows.length).to eq(2)
|
|
103
|
+
expect(all_rows[0].values[1].string_value).to eq("Alice")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it "writes and reads data without an explicit transaction (autocommit)" do
|
|
107
|
+
insert_data_req = Google::Cloud::Spanner::V1::BatchWriteRequest::MutationGroup.new(
|
|
108
|
+
mutations: [
|
|
109
|
+
Google::Cloud::Spanner::V1::Mutation.new(
|
|
110
|
+
insert: Google::Cloud::Spanner::V1::Mutation::Write.new(
|
|
111
|
+
table: "test_table",
|
|
112
|
+
columns: %w[id name],
|
|
113
|
+
values: [
|
|
114
|
+
Google::Protobuf::ListValue.new(values: [Google::Protobuf::Value.new(string_value: "3"),
|
|
115
|
+
Google::Protobuf::Value.new(string_value: "Charlie")]),
|
|
116
|
+
Google::Protobuf::ListValue.new(values: [Google::Protobuf::Value.new(string_value: "4"),
|
|
117
|
+
Google::Protobuf::Value.new(string_value: "David")])
|
|
118
|
+
]
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
]
|
|
122
|
+
)
|
|
123
|
+
@conn.write_mutations(insert_data_req)
|
|
124
|
+
|
|
125
|
+
select_req = Google::Cloud::Spanner::V1::ExecuteSqlRequest.new(sql: "SELECT id, name FROM test_table ORDER BY id")
|
|
126
|
+
rows = @conn.execute(select_req)
|
|
127
|
+
|
|
128
|
+
all_rows = rows.map { |row_bytes| Google::Protobuf::ListValue.decode(row_bytes) }
|
|
129
|
+
|
|
130
|
+
expect(all_rows.length).to eq(2)
|
|
131
|
+
expect(all_rows[0].values[1].string_value).to eq("Charlie")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "raises an error when writing in a read-only transaction" do
|
|
135
|
+
transaction_options = Google::Cloud::Spanner::V1::TransactionOptions.new(
|
|
136
|
+
read_only: Google::Cloud::Spanner::V1::TransactionOptions::ReadOnly.new(strong: true)
|
|
137
|
+
)
|
|
138
|
+
@conn.begin_transaction(transaction_options)
|
|
139
|
+
|
|
140
|
+
insert_data_req = Google::Cloud::Spanner::V1::BatchWriteRequest::MutationGroup.new(
|
|
141
|
+
mutations: [
|
|
142
|
+
Google::Cloud::Spanner::V1::Mutation.new(
|
|
143
|
+
insert: Google::Cloud::Spanner::V1::Mutation::Write.new(table: "test_table")
|
|
144
|
+
)
|
|
145
|
+
]
|
|
146
|
+
)
|
|
147
|
+
expect { @conn.write_mutations(insert_data_req) }.to raise_error(SpannerLibException, /read-only transactions cannot write/)
|
|
148
|
+
|
|
149
|
+
@conn.rollback
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
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 "spec_helper"
|
|
18
|
+
|
|
19
|
+
RSpec.describe "Connection APIs against Spanner emulator", :integration do
|
|
20
|
+
before(:all) do
|
|
21
|
+
@emulator_host = ENV.fetch("SPANNER_EMULATOR_HOST", nil)
|
|
22
|
+
skip "SPANNER_EMULATOR_HOST not set; skipping emulator integration tests" unless @emulator_host && !@emulator_host.empty?
|
|
23
|
+
|
|
24
|
+
begin
|
|
25
|
+
require "spannerlib/pool"
|
|
26
|
+
rescue LoadError, StandardError => e
|
|
27
|
+
skip "Could not load native spanner library; skipping emulator integration tests: #{e.class}: #{e.message}"
|
|
28
|
+
end
|
|
29
|
+
@dsn = "projects/your-project-id/instances/test-instance/databases/test-database?autoConfigEmulator=true"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "creates a pool and a connection against the emulator" do
|
|
33
|
+
pool = Pool.create_pool(@dsn)
|
|
34
|
+
expect(pool.id).to be > 0
|
|
35
|
+
|
|
36
|
+
conn = pool.create_connection
|
|
37
|
+
expect(conn).to respond_to(:pool_id)
|
|
38
|
+
expect(conn).to respond_to(:conn_id)
|
|
39
|
+
|
|
40
|
+
expect { pool.close }.not_to raise_error
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
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 gove
|
|
13
|
+
|
|
14
|
+
# frozen_string_literal: true
|
|
15
|
+
|
|
16
|
+
require "spec_helper"
|
|
17
|
+
require "spannerlib/pool"
|
|
18
|
+
require "spannerlib/ffi"
|
|
19
|
+
require "spannerlib/exceptions"
|
|
20
|
+
|
|
21
|
+
RSpec.describe Connection do
|
|
22
|
+
let(:dsn) { "localhost:1234/projects/p/instances/i/databases/d?usePlainText=true" }
|
|
23
|
+
let(:pool) { Pool.create_pool(dsn) }
|
|
24
|
+
|
|
25
|
+
before do
|
|
26
|
+
allow(SpannerLib).to receive(:create_pool).and_return(1)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
describe "creation" do
|
|
30
|
+
it "is created by a Pool" do
|
|
31
|
+
allow(SpannerLib).to receive(:create_connection).with(1).and_return(2)
|
|
32
|
+
|
|
33
|
+
# The object under test is the one returned by `pool.create_connection`
|
|
34
|
+
conn = pool.create_connection
|
|
35
|
+
|
|
36
|
+
expect(conn).to be_a(described_class)
|
|
37
|
+
expect(conn.conn_id).to eq(2)
|
|
38
|
+
expect(conn.pool_id).to eq(1)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "raises a SpannerLibException when the FFI call fails" do
|
|
42
|
+
allow(SpannerLib).to receive(:create_connection).with(1).and_raise(StandardError.new("boom"))
|
|
43
|
+
|
|
44
|
+
expect { pool.create_connection }.to raise_error(SpannerLibException)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "raises when the FFI call returns a non-positive id" do
|
|
48
|
+
allow(SpannerLib).to receive(:create_connection).with(1).and_return(0)
|
|
49
|
+
|
|
50
|
+
expect { pool.create_connection }.to raise_error(SpannerLibException)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
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 "spec_helper"
|
|
18
|
+
require "spannerlib/pool"
|
|
19
|
+
require "spannerlib/ffi"
|
|
20
|
+
require "spannerlib/exceptions"
|
|
21
|
+
|
|
22
|
+
RSpec.describe Pool do
|
|
23
|
+
let(:dsn) { "localhost:1234/projects/p/instances/i/databases/d?usePlainText=true" }
|
|
24
|
+
|
|
25
|
+
describe ".create_pool" do
|
|
26
|
+
it "creates a pool and returns an object with id > 0" do
|
|
27
|
+
allow(SpannerLib).to receive(:create_pool).with(dsn).and_return(42)
|
|
28
|
+
|
|
29
|
+
pool = described_class.create_pool(dsn)
|
|
30
|
+
|
|
31
|
+
expect(pool).to be_a(described_class)
|
|
32
|
+
expect(pool.id).to be > 0
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "raises a SpannerLibException when create_session/create_pool fails" do
|
|
36
|
+
allow(SpannerLib).to receive(:create_pool).with(dsn).and_raise(StandardError.new("Not allowed"))
|
|
37
|
+
|
|
38
|
+
expect { described_class.create_pool(dsn) }.to raise_error(SpannerLibException)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "raises when create_pool returns nil" do
|
|
42
|
+
allow(SpannerLib).to receive(:create_pool).with(dsn).and_return(nil)
|
|
43
|
+
|
|
44
|
+
expect { described_class.create_pool(dsn) }.to raise_error(SpannerLibException)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "raises when create_pool returns a non-positive id" do
|
|
48
|
+
allow(SpannerLib).to receive(:create_pool).with(dsn).and_return(0)
|
|
49
|
+
|
|
50
|
+
expect { described_class.create_pool(dsn) }.to raise_error(SpannerLibException)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
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 "rubygems"
|
|
18
|
+
require "bundler/setup"
|
|
19
|
+
|
|
20
|
+
require "spannerlib/ruby"
|
|
21
|
+
|
|
22
|
+
RSpec.configure do |config|
|
|
23
|
+
# Enable flags like --only-failures and --next-failure
|
|
24
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
|
25
|
+
|
|
26
|
+
# Disable RSpec exposing methods globally on `Module` and `main`
|
|
27
|
+
config.disable_monkey_patching!
|
|
28
|
+
|
|
29
|
+
config.expect_with :rspec do |c|
|
|
30
|
+
c.syntax = :expect
|
|
31
|
+
end
|
|
32
|
+
end
|