activerecord-spanner-adapter 2.1.0 → 2.2.0
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 +4 -4
- data/.github/workflows/acceptance-tests-on-emulator.yaml +1 -1
- data/.github/workflows/ci.yaml +1 -1
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +6 -0
- data/Gemfile +1 -1
- data/acceptance/cases/tasks/database_tasks_test.rb +1 -1
- data/acceptance/test_helper.rb +12 -0
- data/activerecord-spanner-adapter.gemspec +2 -3
- data/examples/snippets/isolation-level/README.md +39 -0
- data/examples/snippets/isolation-level/Rakefile +13 -0
- data/examples/snippets/isolation-level/application.rb +36 -0
- data/examples/snippets/isolation-level/config/database.yml +10 -0
- data/examples/snippets/isolation-level/db/migrate/01_create_tables.rb +22 -0
- data/examples/snippets/isolation-level/db/seeds.rb +25 -0
- data/examples/snippets/isolation-level/models/album.rb +9 -0
- data/examples/snippets/isolation-level/models/singer.rb +9 -0
- data/lib/active_record/connection_adapters/spanner/database_statements.rb +1 -1
- data/lib/active_record/connection_adapters/spanner_adapter.rb +6 -1
- data/lib/activerecord_spanner_adapter/connection.rb +6 -1
- data/lib/activerecord_spanner_adapter/transaction.rb +12 -2
- data/lib/activerecord_spanner_adapter/version.rb +1 -1
- metadata +17 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cef9bfb6f1e3665c69a3d256455af031c38907bf03d0e2567f9910a2c06b308c
|
4
|
+
data.tar.gz: 2111cd2b6ae24a0f9c7aab481a94228503e3324e517891c0cde832565f60c7ec
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f2acaa0e795fa93b2681bc039df21517798a289b7dd71e3d02e10eab411684d0716f7b749145c73d6bf195bd1f803fbe90149130bb9c13612a03b55ebff69f93
|
7
|
+
data.tar.gz: fbd6cfe74223e2755c671eca16672dfd2558cb5bb43bdfd87536e1fa4a34be45c5f946e96be48e77391dcf646d8293b0d3d1249068d53fd61eb55908a0530884
|
@@ -38,7 +38,7 @@ jobs:
|
|
38
38
|
- name: Install dependencies
|
39
39
|
run: bundle install
|
40
40
|
- name: Run acceptance tests on emulator
|
41
|
-
run: bundle exec rake acceptance
|
41
|
+
run: bundle exec rake acceptance TESTOPTS="-v"
|
42
42
|
env:
|
43
43
|
SPANNER_EMULATOR_HOST: localhost:9010
|
44
44
|
SPANNER_TEST_PROJECT: test-project
|
data/.github/workflows/ci.yaml
CHANGED
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
@@ -65,9 +65,9 @@ module ActiveRecord
|
|
65
65
|
end
|
66
66
|
|
67
67
|
def drop_database
|
68
|
+
spanner_instance.database(@database_id)&.drop
|
68
69
|
ActiveRecord::Base.connection_pool.disconnect!
|
69
70
|
ActiveRecordSpannerAdapter::Connection.reset_information_schemas!
|
70
|
-
spanner_instance.database(@database_id)&.drop
|
71
71
|
end
|
72
72
|
|
73
73
|
def test_structure_dump_and_load
|
data/acceptance/test_helper.rb
CHANGED
@@ -188,6 +188,7 @@ module SpannerAdapter
|
|
188
188
|
unless @skip_test_table_create
|
189
189
|
connection.drop_table :test_models, if_exists: true
|
190
190
|
end
|
191
|
+
ActiveRecord::Base.connection_pool.disconnect!
|
191
192
|
|
192
193
|
super
|
193
194
|
end
|
@@ -273,8 +274,19 @@ module SpannerAdapter
|
|
273
274
|
ActiveSupport::Notifications.subscribe("sql.active_record", SQLCounter.new)
|
274
275
|
end
|
275
276
|
|
277
|
+
module Kernel
|
278
|
+
# Monkey-patch Kernel.exit to call exit! instead.
|
279
|
+
# This prevents the tests from getting stuck after running (probably) due to
|
280
|
+
# gRPC connections that have not been closed yet.
|
281
|
+
def exit status = true
|
282
|
+
exit! status
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
276
286
|
Minitest.after_run do
|
277
287
|
drop_test_database
|
288
|
+
ActiveRecord::Base.connection_pool.disconnect!
|
289
|
+
ActiveRecordSpannerAdapter::Connection.reset_information_schemas!
|
278
290
|
end
|
279
291
|
|
280
292
|
create_test_database
|
@@ -24,9 +24,8 @@ Gem::Specification.new do |spec|
|
|
24
24
|
|
25
25
|
spec.required_ruby_version = ">= 3.1"
|
26
26
|
|
27
|
-
spec.add_dependency "google-cloud-spanner", "~> 2.
|
28
|
-
|
29
|
-
spec.add_dependency "grpc", "1.64.3"
|
27
|
+
spec.add_dependency "google-cloud-spanner", "~> 2.25"
|
28
|
+
spec.add_dependency "google-cloud-spanner-v1", "~> 1.7"
|
30
29
|
spec.add_runtime_dependency "activerecord", [">= 7.0", "< 9"]
|
31
30
|
|
32
31
|
spec.add_development_dependency "autotest-suffix", "~> 1.1"
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# Sample - Isolation Level
|
2
|
+
|
3
|
+
This example shows how to use a specific isolation level for read/write transactions
|
4
|
+
using the Spanner ActiveRecord adapter.
|
5
|
+
|
6
|
+
You can specify the isolation level in two ways:
|
7
|
+
|
8
|
+
1. Set a default in the database configuration:
|
9
|
+
|
10
|
+
```yaml
|
11
|
+
development:
|
12
|
+
adapter: spanner
|
13
|
+
emulator_host: localhost:9010
|
14
|
+
project: test-project
|
15
|
+
instance: test-instance
|
16
|
+
database: testdb
|
17
|
+
isolation_level: :serializable,
|
18
|
+
pool: 5
|
19
|
+
timeout: 5000
|
20
|
+
schema_dump: false
|
21
|
+
```
|
22
|
+
|
23
|
+
2. Specify the isolation level for a specific transaction. This will override any
|
24
|
+
default that is set in the database configuration.
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
ActiveRecord::Base.transaction isolation: :repeatable_read do
|
28
|
+
# Execute transaction code...
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
The sample will automatically start a Spanner Emulator in a Docker container and execute the sample
|
33
|
+
against that emulator. The emulator will automatically be stopped when the application finishes.
|
34
|
+
|
35
|
+
Run the application with the command
|
36
|
+
|
37
|
+
```bash
|
38
|
+
bundle exec rake run
|
39
|
+
```
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# Copyright 2025 Google LLC
|
2
|
+
#
|
3
|
+
# Use of this source code is governed by an MIT-style
|
4
|
+
# license that can be found in the LICENSE file or at
|
5
|
+
# https://opensource.org/licenses/MIT.
|
6
|
+
|
7
|
+
require_relative "../config/environment"
|
8
|
+
require "sinatra/activerecord/rake"
|
9
|
+
|
10
|
+
desc "Sample showing how to specify a transaction isolation level for Spanner with ActiveRecord."
|
11
|
+
task :run do
|
12
|
+
Dir.chdir("..") { sh "bundle exec rake run[isolation-level]" }
|
13
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# Copyright 2025 Google LLC
|
2
|
+
#
|
3
|
+
# Use of this source code is governed by an MIT-style
|
4
|
+
# license that can be found in the LICENSE file or at
|
5
|
+
# https://opensource.org/licenses/MIT.
|
6
|
+
|
7
|
+
require "io/console"
|
8
|
+
require_relative "../config/environment"
|
9
|
+
require_relative "models/singer"
|
10
|
+
require_relative "models/album"
|
11
|
+
|
12
|
+
class Application
|
13
|
+
def self.run # rubocop:disable Metrics/AbcSize
|
14
|
+
from_album = nil
|
15
|
+
to_album = nil
|
16
|
+
# Execute a read/write transaction using isolation level repeatable read.
|
17
|
+
puts "Executing a read/write transaction using isolation level repeatable read"
|
18
|
+
ActiveRecord::Base.transaction isolation: :repeatable_read do
|
19
|
+
# Transfer a marketing budget of 10,000 from one album to another.
|
20
|
+
from_album = Album.all.sample
|
21
|
+
to_album = Album.where.not(id: from_album.id).sample
|
22
|
+
|
23
|
+
puts ""
|
24
|
+
puts "Transferring 10,000 marketing budget from #{from_album.title} (#{from_album.marketing_budget}) " \
|
25
|
+
"to #{to_album.title} (#{to_album.marketing_budget})"
|
26
|
+
from_album.update marketing_budget: from_album.marketing_budget - 10000
|
27
|
+
to_album.update marketing_budget: to_album.marketing_budget + 10000
|
28
|
+
end
|
29
|
+
puts ""
|
30
|
+
puts "Budgets after update:"
|
31
|
+
puts "Marketing budget #{from_album.title}: #{from_album.reload.marketing_budget}"
|
32
|
+
puts "Marketing budget #{to_album.title}: #{to_album.reload.marketing_budget}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
Application.run
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# Copyright 2021 Google LLC
|
2
|
+
#
|
3
|
+
# Use of this source code is governed by an MIT-style
|
4
|
+
# license that can be found in the LICENSE file or at
|
5
|
+
# https://opensource.org/licenses/MIT.
|
6
|
+
|
7
|
+
class CreateTables < ActiveRecord::Migration[6.0]
|
8
|
+
def change
|
9
|
+
connection.ddl_batch do
|
10
|
+
create_table :singers do |t|
|
11
|
+
t.string :first_name
|
12
|
+
t.string :last_name
|
13
|
+
end
|
14
|
+
|
15
|
+
create_table :albums do |t|
|
16
|
+
t.string :title
|
17
|
+
t.numeric :marketing_budget
|
18
|
+
t.references :singer, index: false, foreign_key: true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# Copyright 2021 Google LLC
|
2
|
+
#
|
3
|
+
# Use of this source code is governed by an MIT-style
|
4
|
+
# license that can be found in the LICENSE file or at
|
5
|
+
# https://opensource.org/licenses/MIT.
|
6
|
+
|
7
|
+
require_relative "../../config/environment"
|
8
|
+
require_relative "../models/singer"
|
9
|
+
require_relative "../models/album"
|
10
|
+
|
11
|
+
first_names = ["Pete", "Alice", "John", "Ethel", "Trudy", "Naomi", "Wendy", "Ruben", "Thomas", "Elly"]
|
12
|
+
last_names = ["Wendelson", "Allison", "Peterson", "Johnson", "Henderson", "Ericsson", "Aronson", "Tennet", "Courtou"]
|
13
|
+
|
14
|
+
adjectives = ["daily", "happy", "blue", "generous", "cooked", "bad", "open"]
|
15
|
+
nouns = ["windows", "potatoes", "bank", "street", "tree", "glass", "bottle"]
|
16
|
+
budgets = [15000, 25000, 10000, 20000, 30000, 12000, 13000]
|
17
|
+
|
18
|
+
5.times do
|
19
|
+
Singer.create first_name: first_names.sample, last_name: last_names.sample
|
20
|
+
end
|
21
|
+
|
22
|
+
20.times do
|
23
|
+
singer_id = Singer.all.sample.id
|
24
|
+
Album.create title: "#{adjectives.sample} #{nouns.sample}", marketing_budget: budgets.sample, singer_id: singer_id
|
25
|
+
end
|
@@ -292,7 +292,7 @@ module ActiveRecord
|
|
292
292
|
if isolation.count != 1
|
293
293
|
else
|
294
294
|
raise "Unsupported isolation level: #{isolation}" unless
|
295
|
-
[:serializable, :read_only, :buffered_mutations, :pdml].include? isolation
|
295
|
+
[:serializable, :repeatable_read, :read_only, :buffered_mutations, :pdml].include? isolation
|
296
296
|
end
|
297
297
|
|
298
298
|
log "BEGIN #{isolation}" do
|
@@ -150,7 +150,8 @@ module ActiveRecord
|
|
150
150
|
end
|
151
151
|
|
152
152
|
# Spanner Connection API
|
153
|
-
delegate :ddl_batch, :ddl_batch?, :start_batch_ddl, :abort_batch, :run_batch,
|
153
|
+
delegate :ddl_batch, :ddl_batch?, :start_batch_ddl, :abort_batch, :run_batch,
|
154
|
+
:isolation_level, :isolation_level=, to: :@connection
|
154
155
|
|
155
156
|
def current_spanner_transaction
|
156
157
|
@connection.current_transaction
|
@@ -170,6 +171,10 @@ module ActiveRecord
|
|
170
171
|
false
|
171
172
|
end
|
172
173
|
|
174
|
+
def supports_transaction_isolation?
|
175
|
+
true
|
176
|
+
end
|
177
|
+
|
173
178
|
def supports_foreign_keys?
|
174
179
|
true
|
175
180
|
end
|
@@ -14,10 +14,12 @@ module ActiveRecordSpannerAdapter
|
|
14
14
|
attr_reader :database_id
|
15
15
|
attr_reader :spanner
|
16
16
|
attr_accessor :current_transaction
|
17
|
+
attr_accessor :isolation_level
|
17
18
|
|
18
19
|
def initialize config
|
19
20
|
@instance_id = config[:instance]
|
20
21
|
@database_id = config[:database]
|
22
|
+
@isolation_level = config[:isolation_level]
|
21
23
|
@spanner = self.class.spanners config
|
22
24
|
end
|
23
25
|
|
@@ -42,6 +44,9 @@ module ActiveRecordSpannerAdapter
|
|
42
44
|
# Call this method if you drop and recreate a database with the same name
|
43
45
|
# to prevent the cached information to be used for the new database.
|
44
46
|
def self.reset_information_schemas!
|
47
|
+
@information_schemas.each_value do |info_schema|
|
48
|
+
info_schema.connection.disconnect!
|
49
|
+
end
|
45
50
|
@information_schemas = {}
|
46
51
|
end
|
47
52
|
|
@@ -271,7 +276,7 @@ module ActiveRecordSpannerAdapter
|
|
271
276
|
|
272
277
|
def begin_transaction isolation = nil
|
273
278
|
raise "Nested transactions are not allowed" if current_transaction&.active?
|
274
|
-
self.current_transaction = Transaction.new self, isolation
|
279
|
+
self.current_transaction = Transaction.new self, isolation || @isolation_level
|
275
280
|
current_transaction.begin
|
276
281
|
current_transaction
|
277
282
|
end
|
@@ -55,11 +55,12 @@ module ActiveRecordSpannerAdapter
|
|
55
55
|
when :pdml
|
56
56
|
@grpc_transaction = @connection.session.create_pdml
|
57
57
|
else
|
58
|
+
grpc_isolation = _transaction_isolation_level_to_grpc @isolation
|
58
59
|
@begin_transaction_selector = Google::Cloud::Spanner::V1::TransactionSelector.new \
|
59
60
|
begin: Google::Cloud::Spanner::V1::TransactionOptions.new(
|
60
|
-
read_write: Google::Cloud::Spanner::V1::TransactionOptions::ReadWrite.new
|
61
|
+
read_write: Google::Cloud::Spanner::V1::TransactionOptions::ReadWrite.new,
|
62
|
+
isolation_level: grpc_isolation
|
61
63
|
)
|
62
|
-
|
63
64
|
end
|
64
65
|
@state = :STARTED
|
65
66
|
rescue Google::Cloud::NotFoundError => e
|
@@ -75,6 +76,15 @@ module ActiveRecordSpannerAdapter
|
|
75
76
|
end
|
76
77
|
end
|
77
78
|
|
79
|
+
def _transaction_isolation_level_to_grpc isolation
|
80
|
+
case isolation
|
81
|
+
when :serializable
|
82
|
+
Google::Cloud::Spanner::V1::TransactionOptions::IsolationLevel::SERIALIZABLE
|
83
|
+
when :repeatable_read
|
84
|
+
Google::Cloud::Spanner::V1::TransactionOptions::IsolationLevel::REPEATABLE_READ
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
78
88
|
# Forces a BeginTransaction RPC for a read/write transaction. This is used by a
|
79
89
|
# connection if the first statement of a transaction failed.
|
80
90
|
def force_begin_read_write
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-spanner-adapter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Google LLC
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-04-16 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: google-cloud-spanner
|
@@ -15,28 +15,28 @@ dependencies:
|
|
15
15
|
requirements:
|
16
16
|
- - "~>"
|
17
17
|
- !ruby/object:Gem::Version
|
18
|
-
version: '2.
|
18
|
+
version: '2.25'
|
19
19
|
type: :runtime
|
20
20
|
prerelease: false
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
22
22
|
requirements:
|
23
23
|
- - "~>"
|
24
24
|
- !ruby/object:Gem::Version
|
25
|
-
version: '2.
|
25
|
+
version: '2.25'
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
|
-
name:
|
27
|
+
name: google-cloud-spanner-v1
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
29
29
|
requirements:
|
30
|
-
- -
|
30
|
+
- - "~>"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: 1.
|
32
|
+
version: '1.7'
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
36
36
|
requirements:
|
37
|
-
- -
|
37
|
+
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version: 1.
|
39
|
+
version: '1.7'
|
40
40
|
- !ruby/object:Gem::Dependency
|
41
41
|
name: activerecord
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
@@ -439,6 +439,14 @@ files:
|
|
439
439
|
- examples/snippets/interleaved-tables/models/album.rb
|
440
440
|
- examples/snippets/interleaved-tables/models/singer.rb
|
441
441
|
- examples/snippets/interleaved-tables/models/track.rb
|
442
|
+
- examples/snippets/isolation-level/README.md
|
443
|
+
- examples/snippets/isolation-level/Rakefile
|
444
|
+
- examples/snippets/isolation-level/application.rb
|
445
|
+
- examples/snippets/isolation-level/config/database.yml
|
446
|
+
- examples/snippets/isolation-level/db/migrate/01_create_tables.rb
|
447
|
+
- examples/snippets/isolation-level/db/seeds.rb
|
448
|
+
- examples/snippets/isolation-level/models/album.rb
|
449
|
+
- examples/snippets/isolation-level/models/singer.rb
|
442
450
|
- examples/snippets/migrations/README.md
|
443
451
|
- examples/snippets/migrations/Rakefile
|
444
452
|
- examples/snippets/migrations/application.rb
|