activerecord-spanner-adapter 2.3.0 → 2.5.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -1
  3. data/.github/workflows/acceptance-tests-on-emulator.yaml +4 -2
  4. data/.github/workflows/acceptance-tests-on-production.yaml +4 -4
  5. data/.github/workflows/ci.yaml +4 -2
  6. data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +4 -2
  7. data/.github/workflows/nightly-acceptance-tests-on-production.yaml +3 -3
  8. data/.github/workflows/nightly-unit-tests.yaml +4 -2
  9. data/.github/workflows/rubocop.yaml +3 -3
  10. data/.github/workflows/samples.yaml +1 -1
  11. data/.release-please-manifest.json +1 -1
  12. data/CHANGELOG.md +19 -0
  13. data/Gemfile +3 -2
  14. data/README.md +1 -0
  15. data/acceptance/cases/migration/index_test.rb +2 -2
  16. data/acceptance/cases/tasks/database_tasks_test.rb +25 -4
  17. data/acceptance/cases/transactions/read_write_transactions_test.rb +29 -0
  18. data/acceptance/cases/type/all_types_test.rb +15 -4
  19. data/acceptance/schema/schema.rb +2 -0
  20. data/activerecord-spanner-adapter.gemspec +2 -2
  21. data/examples/snippets/batch-dml/README.md +13 -0
  22. data/examples/snippets/batch-dml/Rakefile +13 -0
  23. data/examples/snippets/batch-dml/application.rb +54 -0
  24. data/examples/snippets/batch-dml/config/database.yml +11 -0
  25. data/examples/snippets/batch-dml/db/migrate/01_create_tables.rb +23 -0
  26. data/examples/snippets/batch-dml/db/seeds.rb +8 -0
  27. data/examples/snippets/batch-dml/models/album.rb +9 -0
  28. data/examples/snippets/batch-dml/models/singer.rb +9 -0
  29. data/examples/snippets/partitioned-dml/application.rb +31 -0
  30. data/lib/active_record/connection_adapters/spanner/column.rb +20 -7
  31. data/lib/active_record/connection_adapters/spanner/database_statements.rb +79 -24
  32. data/lib/active_record/connection_adapters/spanner/errors/transaction_mutation_limit_exceeded_error.rb +25 -0
  33. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +5 -1
  34. data/lib/active_record/connection_adapters/spanner/schema_statements.rb +32 -12
  35. data/lib/active_record/connection_adapters/spanner/type_mapping.rb +26 -0
  36. data/lib/active_record/connection_adapters/spanner_adapter.rb +10 -18
  37. data/lib/active_record/type/spanner/spanner_active_record_converter.rb +1 -0
  38. data/lib/active_record/type/spanner/uuid.rb +19 -0
  39. data/lib/activerecord_spanner_adapter/base.rb +4 -0
  40. data/lib/activerecord_spanner_adapter/connection.rb +129 -11
  41. data/lib/activerecord_spanner_adapter/transaction.rb +25 -4
  42. data/lib/activerecord_spanner_adapter/version.rb +1 -1
  43. data/lib/spanner_client_ext.rb +3 -2
  44. metadata +22 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 35016d190d27c935b9a004570e326d7cdc75af064f877bd619334ea967cf4d7a
4
- data.tar.gz: 1cf91b818edc4d594fcf623f0a3218927406b47523931641fb6e844c88b578ff
3
+ metadata.gz: e5132b6ef0337fa324c42c5b61ff6bee8e745352c58c46afc5f583a253fc6571
4
+ data.tar.gz: 699d2b993d739c78fc95fc12b30189e512517111aa9ae787d929b9d3ac828576
5
5
  SHA512:
6
- metadata.gz: 96dfb48e59b278093e118006e393e03b8981fd370605664830a73ad09873ead412a726a838407595fb903194e64f7b368dc940a443d774847679c316742eefc3
7
- data.tar.gz: 5aa61e971ad3b21f9a9dee3476e7ce9fe5b52f080c2bd507e39cd2aa90719802d0ba5bcd024c3fa521feabc1e8756a249757dda5d3d432c67111ea3479d50e17
6
+ metadata.gz: 3d97e90c71f7dad83a35fc6cfb1921c24989d88b050c88b75ff444dbc58d4c0c2a9abbde2f8197d700dca121dda5c12853e4f6cae6c27bfc0520c7facbff3338
7
+ data.tar.gz: 5dbb5523a45941eff16db888d0af3c5ab7ea3e54af9e94d0f0943cef2a9b6d5e39078df1a1761e05a436910906cd19825f84f298d8e2b9043d79981dff51b4f7
data/.github/CODEOWNERS CHANGED
@@ -4,4 +4,4 @@
4
4
  # For syntax help see:
5
5
  # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax
6
6
 
7
- * @googleapis/ruby-team @olavloite @rahul2393 @ansh0l
7
+ * @googleapis/cloud-sdk-ruby-team @olavloite @rahul2393 @ansh0l
@@ -19,11 +19,13 @@ jobs:
19
19
  max-parallel: 4
20
20
  matrix:
21
21
  ruby: ["3.1", "3.2", "3.3", "3.4"]
22
- ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0"]
22
+ ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0", "~> 8.1.0"]
23
23
  # Exclude combinations that are not supported.
24
24
  exclude:
25
25
  - ruby: "3.1"
26
26
  ar: "~> 8.0.0"
27
+ - ruby: "3.1"
28
+ ar: "~> 8.1.0"
27
29
  - ruby: "3.4"
28
30
  ar: "~> 7.0.0"
29
31
  - ruby: "3.4"
@@ -33,7 +35,7 @@ jobs:
33
35
  env:
34
36
  AR_VERSION: ${{ matrix.ar }}
35
37
  steps:
36
- - uses: actions/checkout@v4
38
+ - uses: actions/checkout@v6
37
39
  - name: Set up Ruby
38
40
  # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby
39
41
  # (see https://github.com/ruby/setup-ruby#versioning):
@@ -26,7 +26,7 @@ jobs:
26
26
  matrix:
27
27
  ruby: [3.3]
28
28
  steps:
29
- - uses: actions/checkout@v4
29
+ - uses: actions/checkout@v6
30
30
  - name: Set up Ruby
31
31
  # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby
32
32
  # (see https://github.com/ruby/setup-ruby#versioning):
@@ -35,17 +35,17 @@ jobs:
35
35
  bundler-cache: true
36
36
  ruby-version: ${{ matrix.ruby }}
37
37
  - name: Authenticate Google Cloud
38
- uses: google-github-actions/auth@v2
38
+ uses: google-github-actions/auth@v3
39
39
  with:
40
40
  credentials_json: ${{ secrets.GCP_SA_KEY }}
41
41
  - name: Setup GCloud
42
- uses: google-github-actions/setup-gcloud@v2
42
+ uses: google-github-actions/setup-gcloud@v3
43
43
  with:
44
44
  project_id: ${{ secrets.GCP_PROJECT_ID }}
45
45
  - name: Install dependencies
46
46
  run: bundle install
47
47
  - name: Run acceptance tests on production
48
- run: bundle exec rake acceptance\[,,,"exclude cases/migration"\]
48
+ run: bundle exec rake acceptance\[,,,"exclude cases/migration"\] TESTOPTS="-v"
49
49
  env:
50
50
  SPANNER_TEST_PROJECT: ${{ secrets.GCP_PROJECT_ID }}
51
51
  SPANNER_TEST_INSTANCE: ruby-activerecord-test
@@ -11,11 +11,13 @@ jobs:
11
11
  max-parallel: 4
12
12
  matrix:
13
13
  ruby: ["3.1", "3.2", "3.3", "3.4"]
14
- ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0"]
14
+ ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0", "~> 8.1.0"]
15
15
  # Exclude combinations that are not supported.
16
16
  exclude:
17
17
  - ruby: "3.1"
18
18
  ar: "~> 8.0.0"
19
+ - ruby: "3.1"
20
+ ar: "~> 8.1.0"
19
21
  - ruby: "3.4"
20
22
  ar: "~> 7.0.0"
21
23
  - ruby: "3.4"
@@ -25,7 +27,7 @@ jobs:
25
27
  env:
26
28
  AR_VERSION: ${{ matrix.ar }}
27
29
  steps:
28
- - uses: actions/checkout@v4
30
+ - uses: actions/checkout@v6
29
31
  - name: Set up Ruby
30
32
  # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby
31
33
  # (see https://github.com/ruby/setup-ruby#versioning):
@@ -19,11 +19,13 @@ jobs:
19
19
  max-parallel: 4
20
20
  matrix:
21
21
  ruby: ["3.1", "3.2", "3.3", "3.4"]
22
- ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0"]
22
+ ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0", "~> 8.1.0"]
23
23
  # Exclude combinations that are not supported.
24
24
  exclude:
25
25
  - ruby: "3.1"
26
26
  ar: "~> 8.0.0"
27
+ - ruby: "3.1"
28
+ ar: "~> 8.1.0"
27
29
  - ruby: "3.4"
28
30
  ar: "~> 7.0.0"
29
31
  - ruby: "3.4"
@@ -33,7 +35,7 @@ jobs:
33
35
  env:
34
36
  AR_VERSION: ${{ matrix.ar }}
35
37
  steps:
36
- - uses: actions/checkout@v4
38
+ - uses: actions/checkout@v6
37
39
  - name: Set up Ruby
38
40
  uses: ruby/setup-ruby@v1
39
41
  with:
@@ -12,7 +12,7 @@ jobs:
12
12
  matrix:
13
13
  ruby: [3.3]
14
14
  steps:
15
- - uses: actions/checkout@v4
15
+ - uses: actions/checkout@v6
16
16
  - name: Set up Ruby
17
17
  # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby
18
18
  # (see https://github.com/ruby/setup-ruby#versioning):
@@ -21,11 +21,11 @@ jobs:
21
21
  bundler-cache: true
22
22
  ruby-version: ${{ matrix.ruby }}
23
23
  - name: Authenticate Google Cloud
24
- uses: google-github-actions/auth@v2
24
+ uses: google-github-actions/auth@v3
25
25
  with:
26
26
  credentials_json: ${{ secrets.GCP_SA_KEY }}
27
27
  - name: Setup GCloud
28
- uses: google-github-actions/setup-gcloud@v2
28
+ uses: google-github-actions/setup-gcloud@v3
29
29
  with:
30
30
  project_id: ${{ secrets.GCP_PROJECT_ID }}
31
31
  - name: Install dependencies
@@ -12,11 +12,13 @@ jobs:
12
12
  matrix:
13
13
  # Run acceptance tests all supported combinations of Ruby and ActiveRecord.
14
14
  ruby: ["3.1", "3.2", "3.3", "3.4"]
15
- ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0"]
15
+ ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0", "~> 8.1.0"]
16
16
  # Exclude combinations that are not supported.
17
17
  exclude:
18
18
  - ruby: "3.1"
19
19
  ar: "~> 8.0.0"
20
+ - ruby: "3.1"
21
+ ar: "~> 8.1.0"
20
22
  - ruby: "3.4"
21
23
  ar: "~> 7.0.0"
22
24
  - ruby: "3.4"
@@ -26,7 +28,7 @@ jobs:
26
28
  env:
27
29
  AR_VERSION: ${{ matrix.ar }}
28
30
  steps:
29
- - uses: actions/checkout@v4
31
+ - uses: actions/checkout@v6
30
32
  - name: Set up Ruby
31
33
  uses: ruby/setup-ruby@v1
32
34
  with:
@@ -12,13 +12,13 @@ jobs:
12
12
  timeout-minutes: 10
13
13
 
14
14
  steps:
15
- - uses: actions/checkout@v4
15
+ - uses: actions/checkout@v6
16
16
  - name: setup ruby
17
17
  uses: ruby/setup-ruby@v1
18
18
  with:
19
- ruby-version: '3.3'
19
+ ruby-version: '3.4.8'
20
20
  - name: cache gems
21
- uses: actions/cache@v4
21
+ uses: actions/cache@v5
22
22
  with:
23
23
  path: vendor/bundle
24
24
  key: ${{ runner.os }}-rubocop-${{ hashFiles('**/Gemfile.lock') }}
@@ -23,7 +23,7 @@ jobs:
23
23
  env:
24
24
  AR_VERSION: ${{ matrix.ar }}
25
25
  steps:
26
- - uses: actions/checkout@v4
26
+ - uses: actions/checkout@v6
27
27
  - name: Set up Ruby
28
28
  uses: ruby/setup-ruby@v1
29
29
  with:
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "2.3.0"
2
+ ".": "2.5.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ### 2.5.0 (2026-02-11)
4
+
5
+ #### Features
6
+
7
+ * add support for the UUID data type ([#387](https://github.com/googleapis/ruby-spanner-activerecord/issues/387))
8
+ * support ActiveRecord 8.1.x ([#385](https://github.com/googleapis/ruby-spanner-activerecord/issues/385))
9
+ #### Documentation
10
+
11
+ * update README to reflect support for Rails 8.1.x ([#388](https://github.com/googleapis/ruby-spanner-activerecord/issues/388))
12
+
13
+ ### 2.4.0 (2026-02-04)
14
+
15
+ #### Features
16
+
17
+ * Add automatic PDML fallback for mutation limit errors ([#369](https://github.com/googleapis/ruby-spanner-activerecord/issues/369))
18
+ * Adding support for exclude_txn_from_change_streams option ([#368](https://github.com/googleapis/ruby-spanner-activerecord/issues/368)) ([1b04c70](https://github.com/googleapis/ruby-spanner-activerecord/commit/1b04c70b4ecec62403449af11827d0f3587acfd0)), closes [#367](https://github.com/googleapis/ruby-spanner-activerecord/issues/367)
19
+ * Batch DML support ([#370](https://github.com/googleapis/ruby-spanner-activerecord/issues/370))
20
+ * support commit_options ([#364](https://github.com/googleapis/ruby-spanner-activerecord/issues/364)) ([0a1020a](https://github.com/googleapis/ruby-spanner-activerecord/commit/0a1020ac60ff9ddaae1634a2178a71bc0de9480c)), closes [#365](https://github.com/googleapis/ruby-spanner-activerecord/issues/365)
21
+
3
22
  ### 2.3.0 (2025-05-30)
4
23
 
5
24
  #### Features
data/Gemfile CHANGED
@@ -6,10 +6,11 @@ gemspec
6
6
  ar_version = ENV.fetch("AR_VERSION", "~> 7.1.0")
7
7
  gem "activerecord", ar_version
8
8
  gem "ostruct"
9
- gem "minitest", "~> 5.25.0"
10
- gem "minitest-rg", "~> 5.3.0"
9
+ gem "minitest", "~> 5.27.0"
10
+ gem "minitest-rg", "~> 5.4.0"
11
11
  gem "pry", "~> 0.14.2"
12
12
  gem "pry-byebug", "~> 3.11.0"
13
+ gem "mutex_m"
13
14
  # Add sqlite3 for testing for compatibility with other adapters.
14
15
  gem 'sqlite3'
15
16
 
data/README.md CHANGED
@@ -15,6 +15,7 @@ This project provides a Spanner adapter for ActiveRecord. It supports the follow
15
15
  - ActiveRecord 7.1.x with Ruby 3.1 and higher.
16
16
  - ActiveRecord 7.2.x with Ruby 3.1 and higher.
17
17
  - ActiveRecord 8.0.x with Ruby 3.2 and higher.
18
+ - ActiveRecord 8.1.x with Ruby 3.2 and higher.
18
19
 
19
20
  Known limitations are listed in the [Limitations](#limitations) section.
20
21
  Please report any problems that you might encounter by [creating a new issue](https://github.com/googleapis/ruby-spanner-activerecord/issues/new).
@@ -57,7 +57,7 @@ module ActiveRecord
57
57
  e = assert_raises(ArgumentError) {
58
58
  connection.rename_index(table_name, "old_idx", too_long_index_name)
59
59
  }
60
- assert_match(/too long; the limit is #{connection.index_name_length} characters/, e.message)
60
+ assert_match(/too long/, e.message)
61
61
 
62
62
  assert connection.index_name_exists?(table_name, "old_idx")
63
63
  end
@@ -79,7 +79,7 @@ module ActiveRecord
79
79
  e = assert_raises(ArgumentError) {
80
80
  connection.add_index(table_name, "foo", name: too_long_index_name)
81
81
  }
82
- assert_match(/too long; the limit is #{connection.index_name_length} characters/, e.message)
82
+ assert_match(/too long/, e.message)
83
83
 
84
84
  assert_not connection.index_name_exists?(table_name, too_long_index_name)
85
85
  connection.add_index(table_name, "foo", name: good_index_name)
@@ -86,6 +86,8 @@ module ActiveRecord
86
86
  filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(config_name, :sql)
87
87
  elsif ActiveRecord::Tasks::DatabaseTasks.respond_to?(:schema_dump_path)
88
88
  filename = ActiveRecord::Tasks::DatabaseTasks.schema_dump_path(db_config, :sql)
89
+ else
90
+ raise "No schema file specified"
89
91
  end
90
92
  ActiveRecord::Tasks::DatabaseTasks.dump_schema db_config, :sql
91
93
  sql = File.read(filename)
@@ -98,10 +100,21 @@ module ActiveRecord
98
100
  else
99
101
  assert_equal expected_schema_sql_on_production, sql, msg = sql
100
102
  end
101
- drop_database
102
- create_database
103
- ActiveRecord::Tasks::DatabaseTasks.load_schema db_config, :sql
104
- assert_equal tables, connection.tables.sort
103
+
104
+ # Skip the drop-and-recreate test on production, for two reasons:
105
+ # 1. It is relatively slow.
106
+ # 2. Dropping and re-creating a database with the same name while keeping the connection open
107
+ # causes 'Database not found' errors to be returned (by the session that is used?)
108
+ if ENV["SPANNER_EMULATOR_HOST"]
109
+ drop_database
110
+ create_database
111
+ begin
112
+ ActiveRecord::Tasks::DatabaseTasks.load_schema db_config, :sql, file = filename
113
+ rescue StandardError => e
114
+ puts "Loading schema failed: #{e}"
115
+ end
116
+ assert_equal tables, connection.tables.sort
117
+ end
105
118
  end
106
119
 
107
120
  def expected_schema_sql_on_emulator
@@ -116,6 +129,7 @@ module ActiveRecord
116
129
  col_date DATE,
117
130
  col_timestamp TIMESTAMP,
118
131
  col_json JSON,
132
+ col_uuid UUID,
119
133
  col_array_string ARRAY<STRING(MAX)>,
120
134
  col_array_int64 ARRAY<INT64>,
121
135
  col_array_float64 ARRAY<FLOAT64>,
@@ -125,6 +139,7 @@ module ActiveRecord
125
139
  col_array_date ARRAY<DATE>,
126
140
  col_array_timestamp ARRAY<TIMESTAMP>,
127
141
  col_array_json ARRAY<JSON>,
142
+ col_array_uuid ARRAY<UUID>,
128
143
  ) PRIMARY KEY(id);
129
144
  CREATE TABLE firms (
130
145
  id INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE),
@@ -274,6 +289,7 @@ INSERT INTO `schema_migrations` (version) VALUES
274
289
  col_date DATE,
275
290
  col_timestamp TIMESTAMP,
276
291
  col_json JSON,
292
+ col_uuid UUID,
277
293
  col_array_string ARRAY<STRING(MAX)>,
278
294
  col_array_int64 ARRAY<INT64>,
279
295
  col_array_float64 ARRAY<FLOAT64>,
@@ -283,6 +299,7 @@ INSERT INTO `schema_migrations` (version) VALUES
283
299
  col_array_date ARRAY<DATE>,
284
300
  col_array_timestamp ARRAY<TIMESTAMP>,
285
301
  col_array_json ARRAY<JSON>,
302
+ col_array_uuid ARRAY<UUID>,
286
303
  ) PRIMARY KEY(id);
287
304
  CREATE TABLE firms (
288
305
  id INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE),
@@ -450,6 +467,7 @@ CREATE TABLE all_types (
450
467
  col_date DATE,
451
468
  col_timestamp TIMESTAMP,
452
469
  col_json JSON,
470
+ col_uuid UUID,
453
471
  col_array_string ARRAY<STRING(MAX)>,
454
472
  col_array_int64 ARRAY<INT64>,
455
473
  col_array_float64 ARRAY<FLOAT64>,
@@ -459,6 +477,7 @@ CREATE TABLE all_types (
459
477
  col_array_date ARRAY<DATE>,
460
478
  col_array_timestamp ARRAY<TIMESTAMP>,
461
479
  col_array_json ARRAY<JSON>,
480
+ col_array_uuid ARRAY<UUID>,
462
481
  ) PRIMARY KEY(id);
463
482
  CREATE TABLE ar_internal_metadata (
464
483
  key STRING(MAX) NOT NULL,
@@ -611,6 +630,7 @@ CREATE TABLE all_types (
611
630
  col_date DATE,
612
631
  col_timestamp TIMESTAMP,
613
632
  col_json JSON,
633
+ col_uuid UUID,
614
634
  col_array_string ARRAY<STRING(MAX)>,
615
635
  col_array_int64 ARRAY<INT64>,
616
636
  col_array_float64 ARRAY<FLOAT64>,
@@ -620,6 +640,7 @@ CREATE TABLE all_types (
620
640
  col_array_date ARRAY<DATE>,
621
641
  col_array_timestamp ARRAY<TIMESTAMP>,
622
642
  col_array_json ARRAY<JSON>,
643
+ col_array_uuid ARRAY<UUID>,
623
644
  ) PRIMARY KEY(id);
624
645
  CREATE TABLE ar_internal_metadata (
625
646
  key STRING(MAX) NOT NULL,
@@ -273,6 +273,35 @@ module ActiveRecord
273
273
  TableWithSequence.connection.use_client_side_id_for_mutations = reset_value
274
274
  end
275
275
  end
276
+
277
+ def test_single_dml_succeeds_with_fallback_to_pdml_enabled
278
+ # This test verifies that a normal, successful DML statement works as
279
+ # expected when the fallback isolation is enabled. Because no mutation
280
+ # limit error occurs, the fallback to PDML should NOT be triggered.
281
+ create_test_records
282
+ assert_equal 1, Author.count
283
+
284
+ Author.transaction isolation: :fallback_to_pdml do
285
+ Author.where(name: "David").delete_all
286
+ end
287
+
288
+ assert_equal 0, Author.count, "The record should have been deleted"
289
+ end
290
+
291
+ def test_other_errors_do_not_trigger_fallback
292
+ # This test ensures that if a transaction with fallback enabled fails
293
+ # for a reason OTHER than the mutation limit, it fails normally and
294
+ # does not attempt to fall back to PDML.
295
+ create_test_records
296
+ initial_author_count = Author.count
297
+
298
+ assert_raises ActiveRecord::StatementInvalid do
299
+ Author.transaction isolation: :fallback_to_pdml do
300
+ Author.create! name: nil
301
+ end
302
+ end
303
+ assert_equal initial_author_count, Author.count, "Transaction should have rolled back"
304
+ end
276
305
  end
277
306
  end
278
307
  end
@@ -30,7 +30,8 @@ module ActiveRecord
30
30
  AllTypes.create col_string: "string", col_int64: 100, col_float64: 3.14, col_numeric: 6.626, col_bool: true,
31
31
  col_bytes: StringIO.new("bytes"), col_date: ::Date.new(2021, 6, 23),
32
32
  col_timestamp: ::Time.new(2021, 6, 23, 17, 8, 21, "+02:00"),
33
- col_json: { kind: "user_renamed", change: %w[jack john]},
33
+ col_json: { kind: "user_renamed", change: %w[jack john] },
34
+ col_uuid: "3840ba25-55df-4cb7-a210-1fe37278954f",
34
35
  col_array_string: ["string1", nil, "string2"],
35
36
  col_array_int64: [100, nil, 200, "300"],
36
37
  col_array_float64: [3.14, nil, 2.0/3.0, "3.14"],
@@ -42,7 +43,8 @@ module ActiveRecord
42
43
  ::Time.new(2021, 6, 24, 17, 8, 21, "+02:00"), "2021-06-25 17:08:21 +02:00"],
43
44
  col_array_json: [{ kind: "user_renamed", change: %w[jack john]}, nil, \
44
45
  { kind: "user_renamed", change: %w[alice meredith]},
45
- "{\"kind\":\"user_renamed\",\"change\":[\"bob\",\"carol\"]}"]
46
+ "{\"kind\":\"user_renamed\",\"change\":[\"bob\",\"carol\"]}"],
47
+ col_array_uuid: ["e986c19c-9bf3-44f8-851d-710ad3d0067b", nil, "a02f50b6-2860-48dc-acd6-49b42abc3094"]
46
48
  end
47
49
 
48
50
  def test_create_record
@@ -67,6 +69,7 @@ module ActiveRecord
67
69
  assert_equal ::Time.new(2021, 6, 23, 17, 8, 21, "+02:00").utc, record.col_timestamp.utc
68
70
  assert_equal ({"kind" => "user_renamed", "change" => %w[jack john]}),
69
71
  record.col_json
72
+ assert_equal "3840ba25-55df-4cb7-a210-1fe37278954f", record.col_uuid
70
73
 
71
74
  assert_equal ["string1", nil, "string2"], record.col_array_string
72
75
  assert_equal [100, nil, 200, 300], record.col_array_int64
@@ -85,6 +88,7 @@ module ActiveRecord
85
88
  {"kind" => "user_renamed", "change" => %w[alice meredith]}, \
86
89
  "{\"kind\":\"user_renamed\",\"change\":[\"bob\",\"carol\"]}"],
87
90
  record.col_array_json
91
+ assert_equal ["e986c19c-9bf3-44f8-851d-710ad3d0067b", nil, "a02f50b6-2860-48dc-acd6-49b42abc3094"], record.col_array_uuid
88
92
  end
89
93
  end
90
94
 
@@ -100,6 +104,7 @@ module ActiveRecord
100
104
  col_date: ::Date.new(2021, 6, 28),
101
105
  col_timestamp: ::Time.new(2021, 6, 28, 11, 22, 21, "+02:00"),
102
106
  col_json: { kind: "user_created", change: %w[jack alice]},
107
+ col_uuid: "b493579f-f4f6-41f6-a520-8373ecf1cde4",
103
108
  col_array_string: ["new string 1", "new string 2"],
104
109
  col_array_int64: [300, 200, 100],
105
110
  col_array_float64: [1.1, 2.2, 3.3],
@@ -108,7 +113,8 @@ module ActiveRecord
108
113
  col_array_bytes: [StringIO.new("new bytes 1"), StringIO.new("new bytes 2")],
109
114
  col_array_date: [::Date.new(2021, 6, 28)],
110
115
  col_array_timestamp: [::Time.utc(2020, 12, 31, 0, 0, 0)],
111
- col_array_json: [{ kind: "user_created", change: %w[jack alice]}]
116
+ col_array_json: [{ kind: "user_created", change: %w[jack alice]}],
117
+ col_array_uuid: ["a664b1eb-4dc7-4795-9ed8-28fab6644cea", nil, "1c794d81-bc6d-4771-9c92-a920d28a7aaa"]
112
118
  end
113
119
 
114
120
  # Verify that the record was updated.
@@ -123,6 +129,7 @@ module ActiveRecord
123
129
  assert_equal ::Time.new(2021, 6, 28, 11, 22, 21, "+02:00").utc, record.col_timestamp.utc
124
130
  assert_equal ({"kind" => "user_created", "change" => %w[jack alice]}),
125
131
  record.col_json
132
+ assert_equal "b493579f-f4f6-41f6-a520-8373ecf1cde4", record.col_uuid
126
133
 
127
134
  assert_equal ["new string 1", "new string 2"], record.col_array_string
128
135
  assert_equal [300, 200, 100], record.col_array_int64
@@ -135,6 +142,8 @@ module ActiveRecord
135
142
  assert_equal [::Time.utc(2020, 12, 31, 0, 0, 0)], record.col_array_timestamp.map(&:utc)
136
143
  assert_equal [{"kind" => "user_created", "change" => %w[jack alice]}],
137
144
  record.col_array_json
145
+ assert_equal ["a664b1eb-4dc7-4795-9ed8-28fab6644cea", nil, "1c794d81-bc6d-4771-9c92-a920d28a7aaa"],
146
+ record.col_array_uuid
138
147
  end
139
148
  end
140
149
 
@@ -151,7 +160,8 @@ module ActiveRecord
151
160
  col_array_bytes: [],
152
161
  col_array_date: [],
153
162
  col_array_timestamp: [],
154
- col_array_json: []
163
+ col_array_json: [],
164
+ col_array_uuid: []
155
165
  end
156
166
 
157
167
  record = AllTypes.find record.id
@@ -164,6 +174,7 @@ module ActiveRecord
164
174
  assert_equal [], record.col_array_date
165
175
  assert_equal [], record.col_array_timestamp
166
176
  assert_equal [], record.col_array_json
177
+ assert_equal [], record.col_array_uuid
167
178
  end
168
179
  end
169
180
  end
@@ -24,6 +24,7 @@ def create_tables_in_test_schema
24
24
  t.column :col_date, :date
25
25
  t.column :col_timestamp, :datetime
26
26
  t.column :col_json, :json
27
+ t.column :col_uuid, :uuid
27
28
 
28
29
  t.column :col_array_string, :string, array: true
29
30
  t.column :col_array_int64, :bigint, array: true
@@ -34,6 +35,7 @@ def create_tables_in_test_schema
34
35
  t.column :col_array_date, :date, array: true
35
36
  t.column :col_array_timestamp, :datetime, array: true
36
37
  t.column :col_array_json, :json, array: true
38
+ t.column :col_array_uuid, :uuid, array: true
37
39
  end
38
40
 
39
41
  create_table :firms do |t|
@@ -29,12 +29,12 @@ Gem::Specification.new do |spec|
29
29
  spec.add_runtime_dependency "activerecord", [">= 7.0", "< 9"]
30
30
 
31
31
  spec.add_development_dependency "autotest-suffix", "~> 1.1"
32
- spec.add_development_dependency "bundler", "~> 2.0"
32
+ spec.add_development_dependency "bundler", [">= 2.0", "< 5.0"]
33
33
  spec.add_development_dependency "google-style", "~> 1.31.0"
34
34
  spec.add_development_dependency "minitest", "~> 5.10"
35
35
  spec.add_development_dependency "minitest-autotest", "~> 1.0"
36
36
  spec.add_development_dependency "minitest-focus", "~> 1.1"
37
- spec.add_development_dependency "minitest-rg", "~> 5.2"
37
+ spec.add_development_dependency "minitest-rg", "~> 5.4"
38
38
  spec.add_development_dependency "rake", "~> 13.0"
39
39
  spec.add_development_dependency "redcarpet", "~> 3.0"
40
40
  spec.add_development_dependency "simplecov", "~> 0.9"
@@ -0,0 +1,13 @@
1
+ # Sample - Batch DML
2
+
3
+ This example shows how to use [Batch DML](https://cloud.google.com/spanner/docs/dml-tasks#use-batch)
4
+ with the Spanner ActiveRecord adapter.
5
+
6
+ The sample will automatically start a Spanner Emulator in a docker container and execute the sample
7
+ against that emulator. The emulator will automatically be stopped when the application finishes.
8
+
9
+ Run the application with the command
10
+
11
+ ```bash
12
+ bundle exec rake run
13
+ ```
@@ -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 use Batch DML with the Spanner ActiveRecord provider."
11
+ task :run do
12
+ Dir.chdir("..") { sh "bundle exec rake run[batch-dml]" }
13
+ end
@@ -0,0 +1,54 @@
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
14
+ first_names = %w[Pete Alice John Ethel Trudy Naomi Wendy Ruben Thomas Elly]
15
+ last_names = %w[Wendelson Allison Peterson Johnson Henderson Ericsson Aronson Tennet Courtou]
16
+
17
+ # Insert 5 new singers using Batch DML.
18
+ ActiveRecord::Base.transaction do
19
+ # The Base.dml_batch function starts a DML batch. All DML statements that are
20
+ # generated by ActiveRecord inside the block that is given will be added to
21
+ # the current batch. The batch is executed at the end of the block. The data
22
+ # that has been written is readable after the block ends.
23
+ ActiveRecord::Base.dml_batch do
24
+ 5.times do
25
+ Singer.create first_name: first_names.sample, last_name: last_names.sample
26
+ end
27
+ end
28
+ # Data that has been inserted/update using Batch DML can be read in the same
29
+ # transaction as the one that added/updated the data. This is different from
30
+ # mutations, as mutations do not support read-your-writes.
31
+ singers = Singer.all
32
+ puts "Inserted #{singers.count} singers in one batch"
33
+ end
34
+
35
+ # Batch DML can also be used to update existing data.
36
+ ActiveRecord::Base.transaction do
37
+ # Start a DML batch.
38
+ singers = nil
39
+ ActiveRecord::Base.dml_batch do
40
+ # Queries can be executed inside a DML batch.
41
+ # These are executed directly and do not affect the DML batch.
42
+ singers = Singer.all
43
+ singers.each do |singer|
44
+ singer.picture = Base64.encode64 SecureRandom.alphanumeric(SecureRandom.random_number(10..200))
45
+ singer.save
46
+ end
47
+ end
48
+ puts "Updated #{singers.count} singers in one batch"
49
+ end
50
+ end
51
+ end
52
+
53
+
54
+ Application.run
@@ -0,0 +1,11 @@
1
+ # Batch DML Example Configuration File
2
+ # This file is used to configure the database connection for the batch DML example.
3
+ development:
4
+ adapter: spanner
5
+ emulator_host: localhost:9010
6
+ project: test-project
7
+ instance: test-instance
8
+ database: testdb
9
+ pool: 5
10
+ timeout: 5000
11
+ schema_dump: false
@@ -0,0 +1,23 @@
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
+ 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
+ t.binary :picture
14
+ end
15
+
16
+ create_table :albums do |t|
17
+ t.string :title
18
+ t.numeric :marketing_budget
19
+ t.references :singer, index: false, foreign_key: true
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,8 @@
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
+ # This file is intentionally kept empty, as this sample
8
+ # does not need any initial data.