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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 539c47de17139481981cfaaaf2b65d43d8f0b27642ad68663658123d0053a8fb
4
- data.tar.gz: c4eb45d075689d08da773ebc11bd1e24df9c6cb7b6ab79a2a352c5c4f4442fbf
3
+ metadata.gz: cef9bfb6f1e3665c69a3d256455af031c38907bf03d0e2567f9910a2c06b308c
4
+ data.tar.gz: 2111cd2b6ae24a0f9c7aab481a94228503e3324e517891c0cde832565f60c7ec
5
5
  SHA512:
6
- metadata.gz: a7d32fa8e2aeeb7785a35f4d11c2dd1ae2db7298d030b6d0da3f08ef5acb2c19833f4f80bc4b5e4b3b174ff9a34121fe97f50ddae902a3af1505eacec311b790
7
- data.tar.gz: 1ec5e7a0370d3bbcc3dd8b0c2b7412f81a57f03703784a0b20476d20d9016d409efcd4f3dc8ecd234e64d1cbedcfe804497fd6b65fd277d7feca014393627edf
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
@@ -30,4 +30,4 @@ jobs:
30
30
  - name: Install dependencies
31
31
  run: bundle install
32
32
  - name: Run tests
33
- run: bundle exec rake test
33
+ run: bundle exec rake test TESTOPTS="-v"
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "2.1.0"
2
+ ".": "2.2.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ### 2.2.0 (2025-04-03)
4
+
5
+ #### Features
6
+
7
+ * transaction isolation level ([#355](https://github.com/googleapis/ruby-spanner-activerecord/issues/355))
8
+
3
9
  ### 2.1.0 (2025-03-17)
4
10
 
5
11
  #### Features
data/Gemfile CHANGED
@@ -9,7 +9,7 @@ gem "ostruct"
9
9
  gem "minitest", "~> 5.25.0"
10
10
  gem "minitest-rg", "~> 5.3.0"
11
11
  gem "pry", "~> 0.14.2"
12
- gem "pry-byebug", "~> 3.10.1"
12
+ gem "pry-byebug", "~> 3.11.0"
13
13
  # Add sqlite3 for testing for compatibility with other adapters.
14
14
  gem 'sqlite3'
15
15
 
@@ -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
@@ -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.18"
28
- # Pin gRPC to 1.64.3, as 1.65 and 1.66 cause test runs to hang randomly.
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,10 @@
1
+ development:
2
+ adapter: spanner
3
+ emulator_host: localhost:9010
4
+ project: test-project
5
+ instance: test-instance
6
+ database: testdb
7
+ isolation_level: :serializable,
8
+ pool: 5
9
+ timeout: 5000
10
+ schema_dump: false
@@ -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
@@ -0,0 +1,9 @@
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 Album < ActiveRecord::Base
8
+ belongs_to :singer
9
+ end
@@ -0,0 +1,9 @@
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 Singer < ActiveRecord::Base
8
+ has_many :albums
9
+ 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, to: :@connection
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
@@ -5,5 +5,5 @@
5
5
  # https://opensource.org/licenses/MIT.
6
6
 
7
7
  module ActiveRecordSpannerAdapter
8
- VERSION = "2.1.0".freeze
8
+ VERSION = "2.2.0".freeze
9
9
  end
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.1.0
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-03-17 00:00:00.000000000 Z
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'
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.18'
25
+ version: '2.25'
26
26
  - !ruby/object:Gem::Dependency
27
- name: grpc
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.64.3
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.64.3
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