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.
- checksums.yaml +4 -4
- data/.github/CODEOWNERS +1 -1
- data/.github/workflows/acceptance-tests-on-emulator.yaml +4 -2
- data/.github/workflows/acceptance-tests-on-production.yaml +4 -4
- data/.github/workflows/ci.yaml +4 -2
- data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +4 -2
- data/.github/workflows/nightly-acceptance-tests-on-production.yaml +3 -3
- data/.github/workflows/nightly-unit-tests.yaml +4 -2
- data/.github/workflows/rubocop.yaml +3 -3
- data/.github/workflows/samples.yaml +1 -1
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +19 -0
- data/Gemfile +3 -2
- data/README.md +1 -0
- data/acceptance/cases/migration/index_test.rb +2 -2
- data/acceptance/cases/tasks/database_tasks_test.rb +25 -4
- data/acceptance/cases/transactions/read_write_transactions_test.rb +29 -0
- data/acceptance/cases/type/all_types_test.rb +15 -4
- data/acceptance/schema/schema.rb +2 -0
- data/activerecord-spanner-adapter.gemspec +2 -2
- data/examples/snippets/batch-dml/README.md +13 -0
- data/examples/snippets/batch-dml/Rakefile +13 -0
- data/examples/snippets/batch-dml/application.rb +54 -0
- data/examples/snippets/batch-dml/config/database.yml +11 -0
- data/examples/snippets/batch-dml/db/migrate/01_create_tables.rb +23 -0
- data/examples/snippets/batch-dml/db/seeds.rb +8 -0
- data/examples/snippets/batch-dml/models/album.rb +9 -0
- data/examples/snippets/batch-dml/models/singer.rb +9 -0
- data/examples/snippets/partitioned-dml/application.rb +31 -0
- data/lib/active_record/connection_adapters/spanner/column.rb +20 -7
- data/lib/active_record/connection_adapters/spanner/database_statements.rb +79 -24
- data/lib/active_record/connection_adapters/spanner/errors/transaction_mutation_limit_exceeded_error.rb +25 -0
- data/lib/active_record/connection_adapters/spanner/schema_creation.rb +5 -1
- data/lib/active_record/connection_adapters/spanner/schema_statements.rb +32 -12
- data/lib/active_record/connection_adapters/spanner/type_mapping.rb +26 -0
- data/lib/active_record/connection_adapters/spanner_adapter.rb +10 -18
- data/lib/active_record/type/spanner/spanner_active_record_converter.rb +1 -0
- data/lib/active_record/type/spanner/uuid.rb +19 -0
- data/lib/activerecord_spanner_adapter/base.rb +4 -0
- data/lib/activerecord_spanner_adapter/connection.rb +129 -11
- data/lib/activerecord_spanner_adapter/transaction.rb +25 -4
- data/lib/activerecord_spanner_adapter/version.rb +1 -1
- data/lib/spanner_client_ext.rb +3 -2
- metadata +22 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e5132b6ef0337fa324c42c5b61ff6bee8e745352c58c46afc5f583a253fc6571
|
|
4
|
+
data.tar.gz: 699d2b993d739c78fc95fc12b30189e512517111aa9ae787d929b9d3ac828576
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3d97e90c71f7dad83a35fc6cfb1921c24989d88b050c88b75ff444dbc58d4c0c2a9abbde2f8197d700dca121dda5c12853e4f6cae6c27bfc0520c7facbff3338
|
|
7
|
+
data.tar.gz: 5dbb5523a45941eff16db888d0af3c5ab7ea3e54af9e94d0f0943cef2a9b6d5e39078df1a1761e05a436910906cd19825f84f298d8e2b9043d79981dff51b4f7
|
data/.github/CODEOWNERS
CHANGED
|
@@ -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@
|
|
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@
|
|
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@
|
|
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@
|
|
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
|
data/.github/workflows/ci.yaml
CHANGED
|
@@ -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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
15
|
+
- uses: actions/checkout@v6
|
|
16
16
|
- name: setup ruby
|
|
17
17
|
uses: ruby/setup-ruby@v1
|
|
18
18
|
with:
|
|
19
|
-
ruby-version: '3.
|
|
19
|
+
ruby-version: '3.4.8'
|
|
20
20
|
- name: cache gems
|
|
21
|
-
uses: actions/cache@
|
|
21
|
+
uses: actions/cache@v5
|
|
22
22
|
with:
|
|
23
23
|
path: vendor/bundle
|
|
24
24
|
key: ${{ runner.os }}-rubocop-${{ hashFiles('**/Gemfile.lock') }}
|
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.
|
|
10
|
-
gem "minitest-rg", "~> 5.
|
|
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
|
|
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
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
data/acceptance/schema/schema.rb
CHANGED
|
@@ -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", "
|
|
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.
|
|
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.
|