activerecord-spanner-adapter 1.0.1 → 1.1.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 +7 -5
- data/.github/workflows/acceptance-tests-on-production.yaml +1 -1
- data/.github/workflows/ci.yaml +7 -5
- data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +9 -5
- data/.github/workflows/nightly-acceptance-tests-on-production.yaml +2 -2
- data/.github/workflows/nightly-unit-tests.yaml +9 -5
- data/.github/workflows/release-please.yml +2 -2
- data/.github/workflows/rubocop.yaml +3 -3
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +6 -0
- data/Gemfile +1 -0
- data/acceptance/cases/models/insert_all_test.rb +150 -0
- data/acceptance/cases/type/all_types_test.rb +10 -13
- data/acceptance/cases/type/json_test.rb +0 -2
- data/acceptance/schema/schema.rb +2 -4
- data/acceptance/test_helper.rb +1 -1
- data/activerecord-spanner-adapter.gemspec +1 -1
- data/lib/active_record/connection_adapters/spanner/schema_creation.rb +10 -4
- data/lib/active_record/connection_adapters/spanner_adapter.rb +64 -31
- data/lib/activerecord_spanner_adapter/base.rb +72 -5
- data/lib/activerecord_spanner_adapter/version.rb +1 -1
- data/lib/arel/visitors/spanner.rb +10 -0
- metadata +14 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9a2ada6b62b88752f24052c1071bb3df30e1057e711045d26ecf1403b891d5b7
|
|
4
|
+
data.tar.gz: 831ec65b5bb4b25ce13970213a5f626e327d9ab6972f86f21076363218fc3ef7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 781bb93f7a9d505864b7b242088b51921df1386c4859e112057bd8b1d3c7bcbee1e3f941af2bcf99324537c136b667464c6328630aa6bd7a769fb9ee52ace690
|
|
7
|
+
data.tar.gz: 485369ec08bd2d3dac01edb57425dd003cace09581bf01fd4d7300a1d4142bc8a701a4d83f18b56c740c7b52f9d2c58d7af0eb3683bafb3e4e81afc59e2e2dc5
|
|
@@ -19,13 +19,17 @@ jobs:
|
|
|
19
19
|
max-parallel: 4
|
|
20
20
|
matrix:
|
|
21
21
|
ruby: [2.6, 2.7, 3.0]
|
|
22
|
-
ar: [6.0.4, 6.1.4]
|
|
23
|
-
# Exclude
|
|
22
|
+
ar: [6.0.4, 6.1.4, 7.0.2.4]
|
|
23
|
+
# Exclude combinations that are not supported.
|
|
24
24
|
exclude:
|
|
25
25
|
- ruby: 3.0
|
|
26
26
|
ar: 6.0.4
|
|
27
|
+
- ruby: 2.6
|
|
28
|
+
ar: 7.0.2.4
|
|
29
|
+
env:
|
|
30
|
+
AR_VERSION: ${{ matrix.ar }}
|
|
27
31
|
steps:
|
|
28
|
-
- uses: actions/checkout@
|
|
32
|
+
- uses: actions/checkout@v3
|
|
29
33
|
- name: Set up Ruby
|
|
30
34
|
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby
|
|
31
35
|
# (see https://github.com/ruby/setup-ruby#versioning):
|
|
@@ -33,8 +37,6 @@ jobs:
|
|
|
33
37
|
with:
|
|
34
38
|
bundler-cache: false
|
|
35
39
|
ruby-version: ${{ matrix.ruby }}
|
|
36
|
-
- name: Set ActiveRecord version
|
|
37
|
-
run: sed -i "s/\"activerecord\", \"~> 6.1.4\"/\"activerecord\", \"${{ matrix.ar }}\"/" activerecord-spanner-adapter.gemspec
|
|
38
40
|
- name: Install dependencies
|
|
39
41
|
run: bundle install
|
|
40
42
|
- name: Run acceptance tests on emulator
|
|
@@ -26,7 +26,7 @@ jobs:
|
|
|
26
26
|
matrix:
|
|
27
27
|
ruby: [3.0]
|
|
28
28
|
steps:
|
|
29
|
-
- uses: actions/checkout@
|
|
29
|
+
- uses: actions/checkout@v3
|
|
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):
|
data/.github/workflows/ci.yaml
CHANGED
|
@@ -11,13 +11,17 @@ jobs:
|
|
|
11
11
|
max-parallel: 4
|
|
12
12
|
matrix:
|
|
13
13
|
ruby: [2.6, 2.7, 3.0]
|
|
14
|
-
ar: [6.0.4, 6.1.4]
|
|
15
|
-
# Exclude
|
|
14
|
+
ar: [6.0.4, 6.1.4, 7.0.2.4]
|
|
15
|
+
# Exclude combinations that are not supported.
|
|
16
16
|
exclude:
|
|
17
17
|
- ruby: 3.0
|
|
18
18
|
ar: 6.0.4
|
|
19
|
+
- ruby: 2.6
|
|
20
|
+
ar: 7.0.2.4
|
|
21
|
+
env:
|
|
22
|
+
AR_VERSION: ${{ matrix.ar }}
|
|
19
23
|
steps:
|
|
20
|
-
- uses: actions/checkout@
|
|
24
|
+
- uses: actions/checkout@v3
|
|
21
25
|
- name: Set up Ruby
|
|
22
26
|
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby
|
|
23
27
|
# (see https://github.com/ruby/setup-ruby#versioning):
|
|
@@ -25,8 +29,6 @@ jobs:
|
|
|
25
29
|
with:
|
|
26
30
|
bundler-cache: false
|
|
27
31
|
ruby-version: ${{ matrix.ruby }}
|
|
28
|
-
- name: Set ActiveRecord version
|
|
29
|
-
run: sed -i "s/\"activerecord\", \"~> 6.1.4\"/\"activerecord\", \"${{ matrix.ar }}\"/" activerecord-spanner-adapter.gemspec
|
|
30
32
|
- name: Install dependencies
|
|
31
33
|
run: bundle install
|
|
32
34
|
- name: Run tests
|
|
@@ -19,8 +19,8 @@ jobs:
|
|
|
19
19
|
matrix:
|
|
20
20
|
# Run acceptance tests all supported combinations of Ruby and ActiveRecord.
|
|
21
21
|
ruby: [2.5, 2.6, 2.7, 3.0]
|
|
22
|
-
ar: [6.0.0, 6.0.1, 6.0.2.2, 6.0.3.7, 6.0.4, 6.1.0, 6.1.1, 6.1.2.1, 6.1.3.2, 6.1.4]
|
|
23
|
-
# Exclude
|
|
22
|
+
ar: [6.0.0, 6.0.1, 6.0.2.2, 6.0.3.7, 6.0.4, 6.1.0, 6.1.1, 6.1.2.1, 6.1.3.2, 6.1.4, 7.0.2.4]
|
|
23
|
+
# Exclude combinations that are not supported.
|
|
24
24
|
exclude:
|
|
25
25
|
- ruby: 3.0
|
|
26
26
|
ar: 6.0.0
|
|
@@ -32,16 +32,20 @@ jobs:
|
|
|
32
32
|
ar: 6.0.3.7
|
|
33
33
|
- ruby: 3.0
|
|
34
34
|
ar: 6.0.4
|
|
35
|
+
- ruby: 2.5
|
|
36
|
+
ar: 7.0.2.4
|
|
37
|
+
- ruby: 2.6
|
|
38
|
+
ar: 7.0.2.4
|
|
39
|
+
env:
|
|
40
|
+
AR_VERSION: ${{ matrix.ar }}
|
|
35
41
|
steps:
|
|
36
|
-
- uses: actions/checkout@
|
|
42
|
+
- uses: actions/checkout@v3
|
|
37
43
|
- name: Set up Ruby
|
|
38
44
|
uses: ruby/setup-ruby@v1
|
|
39
45
|
with:
|
|
40
46
|
# Disable caching as we are overriding the ActiveRecord below.
|
|
41
47
|
bundler-cache: false
|
|
42
48
|
ruby-version: ${{ matrix.ruby }}
|
|
43
|
-
- name: Set ActiveRecord version
|
|
44
|
-
run: sed -i "s/\"activerecord\", \"~> 6.1.4\"/\"activerecord\", \"${{ matrix.ar }}\"/" activerecord-spanner-adapter.gemspec
|
|
45
49
|
- name: Install dependencies
|
|
46
50
|
run: bundle install
|
|
47
51
|
- name: Run acceptance tests on emulator
|
|
@@ -12,7 +12,7 @@ jobs:
|
|
|
12
12
|
matrix:
|
|
13
13
|
ruby: [3.0]
|
|
14
14
|
steps:
|
|
15
|
-
- uses: actions/checkout@
|
|
15
|
+
- uses: actions/checkout@v3
|
|
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,7 +21,7 @@ jobs:
|
|
|
21
21
|
bundler-cache: true
|
|
22
22
|
ruby-version: ${{ matrix.ruby }}
|
|
23
23
|
- name: Setup GCloud
|
|
24
|
-
uses: google-github-actions/setup-gcloud@
|
|
24
|
+
uses: google-github-actions/setup-gcloud@v0
|
|
25
25
|
with:
|
|
26
26
|
project_id: ${{ secrets.GCP_PROJECT_ID }}
|
|
27
27
|
service_account_key: ${{ secrets.GCP_SA_KEY }}
|
|
@@ -11,8 +11,8 @@ jobs:
|
|
|
11
11
|
matrix:
|
|
12
12
|
# Run unit tests all supported combinations of Ruby and ActiveRecord.
|
|
13
13
|
ruby: [2.5, 2.6, 2.7, 3.0]
|
|
14
|
-
ar: [6.0.0, 6.0.1, 6.0.2.2, 6.0.3.7, 6.0.4, 6.1.0, 6.1.1, 6.1.2.1, 6.1.3.2, 6.1.4]
|
|
15
|
-
# Exclude
|
|
14
|
+
ar: [6.0.0, 6.0.1, 6.0.2.2, 6.0.3.7, 6.0.4, 6.1.0, 6.1.1, 6.1.2.1, 6.1.3.2, 6.1.4, 7.0.2.4]
|
|
15
|
+
# Exclude combinations that are not supported.
|
|
16
16
|
exclude:
|
|
17
17
|
- ruby: 3.0
|
|
18
18
|
ar: 6.0.0
|
|
@@ -24,16 +24,20 @@ jobs:
|
|
|
24
24
|
ar: 6.0.3.7
|
|
25
25
|
- ruby: 3.0
|
|
26
26
|
ar: 6.0.4
|
|
27
|
+
- ruby: 2.5
|
|
28
|
+
ar: 7.0.2.4
|
|
29
|
+
- ruby: 2.6
|
|
30
|
+
ar: 7.0.2.4
|
|
31
|
+
env:
|
|
32
|
+
AR_VERSION: ${{ matrix.ar }}
|
|
27
33
|
steps:
|
|
28
|
-
- uses: actions/checkout@
|
|
34
|
+
- uses: actions/checkout@v3
|
|
29
35
|
- name: Set up Ruby
|
|
30
36
|
uses: ruby/setup-ruby@v1
|
|
31
37
|
with:
|
|
32
38
|
# Disable caching as we are overriding the ActiveRecord below.
|
|
33
39
|
bundler-cache: false
|
|
34
40
|
ruby-version: ${{ matrix.ruby }}
|
|
35
|
-
- name: Set ActiveRecord version
|
|
36
|
-
run: sed -i "s/\"activerecord\", \"~> 6.1.4\"/\"activerecord\", \"${{ matrix.ar }}\"/" activerecord-spanner-adapter.gemspec
|
|
37
41
|
- name: Install dependencies
|
|
38
42
|
run: bundle install
|
|
39
43
|
- name: Run tests
|
|
@@ -20,13 +20,13 @@ jobs:
|
|
|
20
20
|
RELEASE_PLEASE_DISABLE: ${{ secrets.RELEASE_PLEASE_DISABLE }}
|
|
21
21
|
steps:
|
|
22
22
|
- name: Checkout repo
|
|
23
|
-
uses: actions/checkout@
|
|
23
|
+
uses: actions/checkout@v3
|
|
24
24
|
- name: Install Ruby 3.0
|
|
25
25
|
uses: ruby/setup-ruby@v1
|
|
26
26
|
with:
|
|
27
27
|
ruby-version: "3.0"
|
|
28
28
|
- name: Install NodeJS 16.x
|
|
29
|
-
uses: actions/setup-node@
|
|
29
|
+
uses: actions/setup-node@v3
|
|
30
30
|
with:
|
|
31
31
|
node-version: "16.x"
|
|
32
32
|
- name: Install tools
|
|
@@ -12,13 +12,13 @@ jobs:
|
|
|
12
12
|
timeout-minutes: 10
|
|
13
13
|
|
|
14
14
|
steps:
|
|
15
|
-
- uses: actions/checkout@
|
|
15
|
+
- uses: actions/checkout@v3
|
|
16
16
|
- name: setup ruby
|
|
17
17
|
uses: ruby/setup-ruby@v1
|
|
18
18
|
with:
|
|
19
|
-
ruby-version: '2.
|
|
19
|
+
ruby-version: '2.7'
|
|
20
20
|
- name: cache gems
|
|
21
|
-
uses: actions/cache@
|
|
21
|
+
uses: actions/cache@v3
|
|
22
22
|
with:
|
|
23
23
|
path: vendor/bundle
|
|
24
24
|
key: ${{ runner.os }}-rubocop-${{ hashFiles('**/Gemfile.lock') }}
|
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Copyright 2022 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
|
+
# frozen_string_literal: true
|
|
8
|
+
|
|
9
|
+
require "test_helper"
|
|
10
|
+
require "models/author"
|
|
11
|
+
|
|
12
|
+
module ActiveRecord
|
|
13
|
+
module Model
|
|
14
|
+
class InsertAllTest < SpannerAdapter::TestCase
|
|
15
|
+
include SpannerAdapter::Associations::TestHelper
|
|
16
|
+
|
|
17
|
+
def setup
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def teardown
|
|
22
|
+
super
|
|
23
|
+
Author.destroy_all
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_insert_all
|
|
27
|
+
values = [
|
|
28
|
+
{ id: Author.next_sequence_value, name: "Alice" },
|
|
29
|
+
{ id: Author.next_sequence_value, name: "Bob" },
|
|
30
|
+
{ id: Author.next_sequence_value, name: "Carol" },
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
assert_raise(NotImplementedError) { Author.insert_all(values) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_insert_all!
|
|
37
|
+
values = [
|
|
38
|
+
{ id: Author.next_sequence_value, name: "Alice" },
|
|
39
|
+
{ id: Author.next_sequence_value, name: "Bob" },
|
|
40
|
+
{ id: Author.next_sequence_value, name: "Carol" },
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
Author.insert_all!(values)
|
|
44
|
+
|
|
45
|
+
authors = Author.all.order(:name)
|
|
46
|
+
|
|
47
|
+
assert_equal "Alice", authors[0].name
|
|
48
|
+
assert_equal "Bob", authors[1].name
|
|
49
|
+
assert_equal "Carol", authors[2].name
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_insert_all_with_transaction
|
|
53
|
+
values = [
|
|
54
|
+
{ id: Author.next_sequence_value, name: "Alice" },
|
|
55
|
+
{ id: Author.next_sequence_value, name: "Bob" },
|
|
56
|
+
{ id: Author.next_sequence_value, name: "Carol" },
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
ActiveRecord::Base.transaction do
|
|
60
|
+
Author.insert_all!(values)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
authors = Author.all.order(:name)
|
|
64
|
+
|
|
65
|
+
assert_equal "Alice", authors[0].name
|
|
66
|
+
assert_equal "Bob", authors[1].name
|
|
67
|
+
assert_equal "Carol", authors[2].name
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_insert_all_with_buffered_mutation_transaction
|
|
71
|
+
values = [
|
|
72
|
+
{ id: Author.next_sequence_value, name: "Alice" },
|
|
73
|
+
{ id: Author.next_sequence_value, name: "Bob" },
|
|
74
|
+
{ id: Author.next_sequence_value, name: "Carol" },
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
ActiveRecord::Base.transaction isolation: :buffered_mutations do
|
|
78
|
+
Author.insert_all!(values)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
authors = Author.all.order(:name)
|
|
82
|
+
|
|
83
|
+
assert_equal "Alice", authors[0].name
|
|
84
|
+
assert_equal "Bob", authors[1].name
|
|
85
|
+
assert_equal "Carol", authors[2].name
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def test_upsert_all
|
|
89
|
+
Author.create id: 1, name: "David"
|
|
90
|
+
authors = Author.all.order(:name)
|
|
91
|
+
assert_equal 1, authors.length
|
|
92
|
+
assert_equal "David", authors[0].name
|
|
93
|
+
|
|
94
|
+
values = [
|
|
95
|
+
{ id: 1, name: "Alice" },
|
|
96
|
+
{ id: 2, name: "Bob" },
|
|
97
|
+
{ id: 3, name: "Carol" },
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
Author.upsert_all(values)
|
|
101
|
+
|
|
102
|
+
authors = Author.all.order(:name)
|
|
103
|
+
|
|
104
|
+
assert_equal 3, authors.length
|
|
105
|
+
assert_equal "Alice", authors[0].name
|
|
106
|
+
assert_equal "Bob", authors[1].name
|
|
107
|
+
assert_equal "Carol", authors[2].name
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def test_upsert_all_with_transaction
|
|
111
|
+
values = [
|
|
112
|
+
{ id: Author.next_sequence_value, name: "Alice" },
|
|
113
|
+
{ id: Author.next_sequence_value, name: "Bob" },
|
|
114
|
+
{ id: Author.next_sequence_value, name: "Carol" },
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
err = assert_raise(NotImplementedError) do
|
|
118
|
+
ActiveRecord::Base.transaction do
|
|
119
|
+
Author.upsert_all(values)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
assert_match "Use upsert outside a transaction block", err.message
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def test_upsert_all_with_buffered_mutation_transaction
|
|
126
|
+
Author.create id: 1, name: "David"
|
|
127
|
+
authors = Author.all.order(:name)
|
|
128
|
+
assert_equal 1, authors.length
|
|
129
|
+
assert_equal "David", authors[0].name
|
|
130
|
+
|
|
131
|
+
values = [
|
|
132
|
+
{ id: 1, name: "Alice" },
|
|
133
|
+
{ id: 2, name: "Bob" },
|
|
134
|
+
{ id: 3, name: "Carol" },
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
ActiveRecord::Base.transaction isolation: :buffered_mutations do
|
|
138
|
+
Author.upsert_all(values)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
authors = Author.all.order(:name)
|
|
142
|
+
|
|
143
|
+
assert_equal 3, authors.length
|
|
144
|
+
assert_equal "Alice", authors[0].name
|
|
145
|
+
assert_equal "Bob", authors[1].name
|
|
146
|
+
assert_equal "Carol", authors[2].name
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -30,7 +30,7 @@ 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:
|
|
33
|
+
col_json: { kind: "user_renamed", change: %w[jack john]},
|
|
34
34
|
col_array_string: ["string1", nil, "string2"],
|
|
35
35
|
col_array_int64: [100, nil, 200, "300"],
|
|
36
36
|
col_array_float64: [3.14, nil, 2.0/3.0, "3.14"],
|
|
@@ -40,8 +40,7 @@ module ActiveRecord
|
|
|
40
40
|
col_array_date: [::Date.new(2021, 6, 23), nil, ::Date.new(2021, 6, 24), "2021-06-25"],
|
|
41
41
|
col_array_timestamp: [::Time.new(2021, 6, 23, 17, 8, 21, "+02:00"), nil, \
|
|
42
42
|
::Time.new(2021, 6, 24, 17, 8, 21, "+02:00"), "2021-06-25 17:08:21 +02:00"],
|
|
43
|
-
col_array_json:
|
|
44
|
-
[{ kind: "user_renamed", change: %w[jack john]}, nil, \
|
|
43
|
+
col_array_json: [{ kind: "user_renamed", change: %w[jack john]}, nil, \
|
|
45
44
|
{ kind: "user_renamed", change: %w[alice meredith]},
|
|
46
45
|
"{\"kind\":\"user_renamed\",\"change\":[\"bob\",\"carol\"]}"]
|
|
47
46
|
end
|
|
@@ -67,7 +66,7 @@ module ActiveRecord
|
|
|
67
66
|
assert_equal ::Date.new(2021, 6, 23), record.col_date
|
|
68
67
|
assert_equal ::Time.new(2021, 6, 23, 17, 8, 21, "+02:00").utc, record.col_timestamp.utc
|
|
69
68
|
assert_equal ({"kind" => "user_renamed", "change" => %w[jack john]}),
|
|
70
|
-
record.col_json
|
|
69
|
+
record.col_json
|
|
71
70
|
|
|
72
71
|
assert_equal ["string1", nil, "string2"], record.col_array_string
|
|
73
72
|
assert_equal [100, nil, 200, 300], record.col_array_int64
|
|
@@ -83,9 +82,9 @@ module ActiveRecord
|
|
|
83
82
|
record.col_array_timestamp.map { |timestamp| timestamp&.utc}
|
|
84
83
|
assert_equal [{"kind" => "user_renamed", "change" => %w[jack john]}, \
|
|
85
84
|
nil, \
|
|
86
|
-
{"kind" => "user_renamed", "change" => %w[alice meredith]},
|
|
87
|
-
{"kind"
|
|
88
|
-
record.col_array_json
|
|
85
|
+
{"kind" => "user_renamed", "change" => %w[alice meredith]}, \
|
|
86
|
+
"{\"kind\":\"user_renamed\",\"change\":[\"bob\",\"carol\"]}"],
|
|
87
|
+
record.col_array_json
|
|
89
88
|
end
|
|
90
89
|
end
|
|
91
90
|
|
|
@@ -100,7 +99,7 @@ module ActiveRecord
|
|
|
100
99
|
col_bool: false, col_bytes: StringIO.new("new bytes"),
|
|
101
100
|
col_date: ::Date.new(2021, 6, 28),
|
|
102
101
|
col_timestamp: ::Time.new(2021, 6, 28, 11, 22, 21, "+02:00"),
|
|
103
|
-
col_json:
|
|
102
|
+
col_json: { kind: "user_created", change: %w[jack alice]},
|
|
104
103
|
col_array_string: ["new string 1", "new string 2"],
|
|
105
104
|
col_array_int64: [300, 200, 100],
|
|
106
105
|
col_array_float64: [1.1, 2.2, 3.3],
|
|
@@ -109,9 +108,7 @@ module ActiveRecord
|
|
|
109
108
|
col_array_bytes: [StringIO.new("new bytes 1"), StringIO.new("new bytes 2")],
|
|
110
109
|
col_array_date: [::Date.new(2021, 6, 28)],
|
|
111
110
|
col_array_timestamp: [::Time.utc(2020, 12, 31, 0, 0, 0)],
|
|
112
|
-
col_array_json:
|
|
113
|
-
[""] : \
|
|
114
|
-
[{ kind: "user_created", change: %w[jack alice]}]
|
|
111
|
+
col_array_json: [{ kind: "user_created", change: %w[jack alice]}]
|
|
115
112
|
end
|
|
116
113
|
|
|
117
114
|
# Verify that the record was updated.
|
|
@@ -125,7 +122,7 @@ module ActiveRecord
|
|
|
125
122
|
assert_equal ::Date.new(2021, 6, 28), record.col_date
|
|
126
123
|
assert_equal ::Time.new(2021, 6, 28, 11, 22, 21, "+02:00").utc, record.col_timestamp.utc
|
|
127
124
|
assert_equal ({"kind" => "user_created", "change" => %w[jack alice]}),
|
|
128
|
-
|
|
125
|
+
record.col_json
|
|
129
126
|
|
|
130
127
|
assert_equal ["new string 1", "new string 2"], record.col_array_string
|
|
131
128
|
assert_equal [300, 200, 100], record.col_array_int64
|
|
@@ -137,7 +134,7 @@ module ActiveRecord
|
|
|
137
134
|
assert_equal [::Date.new(2021, 6, 28)], record.col_array_date
|
|
138
135
|
assert_equal [::Time.utc(2020, 12, 31, 0, 0, 0)], record.col_array_timestamp.map(&:utc)
|
|
139
136
|
assert_equal [{"kind" => "user_created", "change" => %w[jack alice]}],
|
|
140
|
-
record.col_array_json
|
|
137
|
+
record.col_array_json
|
|
141
138
|
end
|
|
142
139
|
end
|
|
143
140
|
|
data/acceptance/schema/schema.rb
CHANGED
|
@@ -17,8 +17,7 @@ ActiveRecord::Schema.define do
|
|
|
17
17
|
t.column :col_bytes, :binary
|
|
18
18
|
t.column :col_date, :date
|
|
19
19
|
t.column :col_timestamp, :datetime
|
|
20
|
-
t.column :col_json, :json
|
|
21
|
-
t.column :col_json, :string if ENV["SPANNER_EMULATOR_HOST"]
|
|
20
|
+
t.column :col_json, :json
|
|
22
21
|
|
|
23
22
|
t.column :col_array_string, :string, array: true
|
|
24
23
|
t.column :col_array_int64, :bigint, array: true
|
|
@@ -28,8 +27,7 @@ ActiveRecord::Schema.define do
|
|
|
28
27
|
t.column :col_array_bytes, :binary, array: true
|
|
29
28
|
t.column :col_array_date, :date, array: true
|
|
30
29
|
t.column :col_array_timestamp, :datetime, array: true
|
|
31
|
-
t.column :col_array_json, :json, array: true
|
|
32
|
-
t.column :col_array_json, :string, array: true if ENV["SPANNER_EMULATOR_HOST"]
|
|
30
|
+
t.column :col_array_json, :json, array: true
|
|
33
31
|
end
|
|
34
32
|
|
|
35
33
|
create_table :firms do |t|
|
data/acceptance/test_helper.rb
CHANGED
|
@@ -25,7 +25,7 @@ Gem::Specification.new do |spec|
|
|
|
25
25
|
spec.required_ruby_version = ">= 2.5"
|
|
26
26
|
|
|
27
27
|
spec.add_dependency "google-cloud-spanner", "~> 2.10"
|
|
28
|
-
spec.add_runtime_dependency "activerecord", "
|
|
28
|
+
spec.add_runtime_dependency "activerecord", [">= 6.0.0", "< 7.1"]
|
|
29
29
|
|
|
30
30
|
spec.add_development_dependency "autotest-suffix", "~> 1.1"
|
|
31
31
|
spec.add_development_dependency "bundler", "~> 2.0"
|
|
@@ -10,14 +10,20 @@ module ActiveRecord
|
|
|
10
10
|
class SchemaCreation < SchemaCreation
|
|
11
11
|
private
|
|
12
12
|
|
|
13
|
-
# rubocop:disable Naming/MethodName, Metrics/AbcSize
|
|
13
|
+
# rubocop:disable Naming/MethodName, Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
14
14
|
|
|
15
15
|
def visit_TableDefinition o
|
|
16
16
|
create_sql = +"CREATE TABLE #{quote_table_name o.name} "
|
|
17
17
|
statements = o.columns.map { |c| accept c }
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
if ActiveRecord::VERSION::MAJOR >= 7
|
|
20
|
+
o.foreign_keys.each do |fk|
|
|
21
|
+
statements << accept(fk)
|
|
22
|
+
end
|
|
23
|
+
else
|
|
24
|
+
o.foreign_keys.each do |to_table, options|
|
|
25
|
+
statements << foreign_key_in_create(o.name, to_table, options)
|
|
26
|
+
end
|
|
21
27
|
end
|
|
22
28
|
|
|
23
29
|
create_sql << "(#{statements.join ', '}) " if statements.any?
|
|
@@ -106,7 +112,7 @@ module ActiveRecord
|
|
|
106
112
|
sql
|
|
107
113
|
end
|
|
108
114
|
|
|
109
|
-
# rubocop:enable Naming/MethodName, Metrics/AbcSize
|
|
115
|
+
# rubocop:enable Naming/MethodName, Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
110
116
|
|
|
111
117
|
def add_column_options! sql, options
|
|
112
118
|
if options[:null] == false || options[:primary_key] == true
|
|
@@ -8,6 +8,7 @@ require "securerandom"
|
|
|
8
8
|
require "google/cloud/spanner"
|
|
9
9
|
require "spanner_client_ext"
|
|
10
10
|
require "active_record/connection_adapters/abstract_adapter"
|
|
11
|
+
require "active_record/connection_adapters/abstract/connection_pool"
|
|
11
12
|
require "active_record/connection_adapters/spanner/database_statements"
|
|
12
13
|
require "active_record/connection_adapters/spanner/schema_statements"
|
|
13
14
|
require "active_record/connection_adapters/spanner/schema_cache"
|
|
@@ -43,9 +44,9 @@ module ActiveRecord
|
|
|
43
44
|
module ConnectionAdapters
|
|
44
45
|
module AbstractPool
|
|
45
46
|
def get_schema_cache connection
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
self.schema_cache ||= SpannerSchemaCache.new connection
|
|
48
|
+
schema_cache.connection = connection
|
|
49
|
+
schema_cache
|
|
49
50
|
end
|
|
50
51
|
end
|
|
51
52
|
|
|
@@ -178,41 +179,73 @@ module ActiveRecord
|
|
|
178
179
|
Arel::Visitors::Spanner.new self
|
|
179
180
|
end
|
|
180
181
|
|
|
181
|
-
|
|
182
|
+
def build_insert_sql insert
|
|
183
|
+
if current_spanner_transaction&.isolation == :buffered_mutations
|
|
184
|
+
raise "ActiveRecordSpannerAdapter does not support insert_sql with buffered_mutations transaction."
|
|
185
|
+
end
|
|
182
186
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
m, %r{^BYTES}i, ActiveRecord::Type::Spanner::Bytes
|
|
187
|
-
)
|
|
188
|
-
m.register_type "DATE", Type::Date.new
|
|
189
|
-
m.register_type "FLOAT64", Type::Float.new
|
|
190
|
-
m.register_type "NUMERIC", Type::Decimal.new
|
|
191
|
-
m.register_type "INT64", Type::Integer.new(limit: 8)
|
|
192
|
-
register_class_with_limit m, %r{^STRING}i, Type::String
|
|
193
|
-
m.register_type "TIMESTAMP", ActiveRecord::Type::Spanner::Time.new
|
|
194
|
-
m.register_type "JSON", ActiveRecord::Type::Json.new
|
|
187
|
+
if insert.skip_duplicates? || insert.update_duplicates?
|
|
188
|
+
raise NotImplementedError, "CloudSpanner does not support skip_duplicates and update_duplicates."
|
|
189
|
+
end
|
|
195
190
|
|
|
196
|
-
|
|
191
|
+
values_list, = insert.values_list
|
|
192
|
+
"INSERT #{insert.into} #{values_list}"
|
|
197
193
|
end
|
|
198
194
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
m
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
195
|
+
module TypeMapBuilder
|
|
196
|
+
private
|
|
197
|
+
|
|
198
|
+
def initialize_type_map m = type_map
|
|
199
|
+
m.register_type "BOOL", Type::Boolean.new
|
|
200
|
+
register_class_with_limit(
|
|
201
|
+
m, %r{^BYTES}i, ActiveRecord::Type::Spanner::Bytes
|
|
202
|
+
)
|
|
203
|
+
m.register_type "DATE", Type::Date.new
|
|
204
|
+
m.register_type "FLOAT64", Type::Float.new
|
|
205
|
+
m.register_type "NUMERIC", Type::Decimal.new
|
|
206
|
+
m.register_type "INT64", Type::Integer.new(limit: 8)
|
|
207
|
+
register_class_with_limit m, %r{^STRING}i, Type::String
|
|
208
|
+
m.register_type "TIMESTAMP", ActiveRecord::Type::Spanner::Time.new
|
|
209
|
+
m.register_type "JSON", ActiveRecord::Type::Json.new
|
|
210
|
+
|
|
211
|
+
register_array_types m
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def register_array_types m
|
|
215
|
+
m.register_type %r{^ARRAY<BOOL>}i, Type::Spanner::Array.new(Type::Boolean.new)
|
|
216
|
+
m.register_type %r{^ARRAY<BYTES\((MAX|d+)\)>}i,
|
|
217
|
+
Type::Spanner::Array.new(ActiveRecord::Type::Spanner::Bytes.new)
|
|
218
|
+
m.register_type %r{^ARRAY<DATE>}i, Type::Spanner::Array.new(Type::Date.new)
|
|
219
|
+
m.register_type %r{^ARRAY<FLOAT64>}i, Type::Spanner::Array.new(Type::Float.new)
|
|
220
|
+
m.register_type %r{^ARRAY<NUMERIC>}i, Type::Spanner::Array.new(Type::Decimal.new)
|
|
221
|
+
m.register_type %r{^ARRAY<INT64>}i, Type::Spanner::Array.new(Type::Integer.new(limit: 8))
|
|
222
|
+
m.register_type %r{^ARRAY<STRING\((MAX|d+)\)>}i, Type::Spanner::Array.new(Type::String.new)
|
|
223
|
+
m.register_type %r{^ARRAY<TIMESTAMP>}i, Type::Spanner::Array.new(ActiveRecord::Type::Spanner::Time.new)
|
|
224
|
+
m.register_type %r{^ARRAY<JSON>}i, Type::Spanner::Array.new(ActiveRecord::Type::Json.new)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def extract_limit sql_type
|
|
228
|
+
value = /\((.*)\)/.match sql_type
|
|
229
|
+
return unless value
|
|
230
|
+
|
|
231
|
+
value[1] == "MAX" ? "MAX" : value[1].to_i
|
|
232
|
+
end
|
|
209
233
|
end
|
|
210
234
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
235
|
+
if ActiveRecord::VERSION::MAJOR >= 7
|
|
236
|
+
class << self
|
|
237
|
+
include TypeMapBuilder
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map m }
|
|
241
|
+
|
|
242
|
+
private
|
|
214
243
|
|
|
215
|
-
|
|
244
|
+
def type_map
|
|
245
|
+
TYPE_MAP
|
|
246
|
+
end
|
|
247
|
+
else
|
|
248
|
+
include TypeMapBuilder
|
|
216
249
|
end
|
|
217
250
|
|
|
218
251
|
def translate_exception exception, message:, sql:, binds:
|
|
@@ -41,14 +41,69 @@ module ActiveRecord
|
|
|
41
41
|
spanner_adapter? && connection&.current_spanner_transaction&.isolation == :buffered_mutations
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
def self.insert_all _attributes, _returning: nil, _unique_by: nil
|
|
45
|
+
raise NotImplementedError, "Cloud Spanner does not support skip_duplicates."
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.insert_all! attributes, returning: nil
|
|
49
|
+
return super unless spanner_adapter?
|
|
50
|
+
return super if active_transaction? && !buffered_mutations?
|
|
51
|
+
|
|
52
|
+
# This might seem inefficient, but is actually not, as it is only buffering a mutation locally.
|
|
53
|
+
# The mutations will be sent as one batch when the transaction is committed.
|
|
54
|
+
if active_transaction?
|
|
55
|
+
attributes.each do |record|
|
|
56
|
+
_insert_record record
|
|
57
|
+
end
|
|
58
|
+
else
|
|
59
|
+
transaction isolation: :buffered_mutations do
|
|
60
|
+
attributes.each do |record|
|
|
61
|
+
_insert_record record
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.upsert_all attributes, returning: nil, unique_by: nil
|
|
68
|
+
return super unless spanner_adapter?
|
|
69
|
+
if active_transaction? && !buffered_mutations?
|
|
70
|
+
raise NotImplementedError, "Cloud Spanner does not support upsert using DML. " \
|
|
71
|
+
"Use upsert outside a transaction block or in a transaction " \
|
|
72
|
+
"block with isolation: :buffered_mutations"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# This might seem inefficient, but is actually not, as it is only buffering a mutation locally.
|
|
76
|
+
# The mutations will be sent as one batch when the transaction is committed.
|
|
77
|
+
if active_transaction?
|
|
78
|
+
attributes.each do |record|
|
|
79
|
+
_upsert_record record
|
|
80
|
+
end
|
|
81
|
+
else
|
|
82
|
+
transaction isolation: :buffered_mutations do
|
|
83
|
+
attributes.each do |record|
|
|
84
|
+
_upsert_record record
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
44
90
|
def self._insert_record values
|
|
45
91
|
return super unless buffered_mutations?
|
|
46
92
|
|
|
93
|
+
_buffer_record values, :insert
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self._upsert_record values
|
|
97
|
+
_buffer_record values, :insert_or_update
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def self._buffer_record values, method
|
|
47
101
|
primary_key = self.primary_key
|
|
48
102
|
primary_key_value = nil
|
|
49
103
|
|
|
50
104
|
if primary_key && values.is_a?(Hash)
|
|
51
105
|
primary_key_value = values[primary_key]
|
|
106
|
+
primary_key_value ||= values[:"#{primary_key}"]
|
|
52
107
|
|
|
53
108
|
if !primary_key_value && prefetch_primary_key?
|
|
54
109
|
primary_key_value = next_sequence_value
|
|
@@ -59,13 +114,15 @@ module ActiveRecord
|
|
|
59
114
|
metadata = TableMetadata.new self, arel_table
|
|
60
115
|
columns, grpc_values = _create_grpc_values_for_insert metadata, values
|
|
61
116
|
|
|
117
|
+
write = Google::Cloud::Spanner::V1::Mutation::Write.new(
|
|
118
|
+
table: arel_table.name,
|
|
119
|
+
columns: columns,
|
|
120
|
+
values: [grpc_values.list_value]
|
|
121
|
+
)
|
|
62
122
|
mutation = Google::Cloud::Spanner::V1::Mutation.new(
|
|
63
|
-
|
|
64
|
-
table: arel_table.name,
|
|
65
|
-
columns: columns,
|
|
66
|
-
values: [grpc_values.list_value]
|
|
67
|
-
)
|
|
123
|
+
"#{method}": write
|
|
68
124
|
)
|
|
125
|
+
|
|
69
126
|
connection.current_spanner_transaction.buffer mutation
|
|
70
127
|
|
|
71
128
|
primary_key_value
|
|
@@ -87,6 +144,14 @@ module ActiveRecord
|
|
|
87
144
|
!(current_transaction.nil? || current_transaction.is_a?(ConnectionAdapters::NullTransaction))
|
|
88
145
|
end
|
|
89
146
|
|
|
147
|
+
def self.unwrap_attribute attr_or_value
|
|
148
|
+
if attr_or_value.is_a? ActiveModel::Attribute
|
|
149
|
+
attr_or_value.value
|
|
150
|
+
else
|
|
151
|
+
attr_or_value
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
90
155
|
# Updates the given attributes of the object in the database. This method will use mutations instead
|
|
91
156
|
# of DML if there is no active transaction, or if the active transaction has been created with the option
|
|
92
157
|
# isolation: :buffered_mutations.
|
|
@@ -117,6 +182,7 @@ module ActiveRecord
|
|
|
117
182
|
serialized_values = []
|
|
118
183
|
columns = []
|
|
119
184
|
values.each_pair do |k, v|
|
|
185
|
+
v = unwrap_attribute v
|
|
120
186
|
type = metadata.type k
|
|
121
187
|
serialized_values << (type.method(:serialize).arity < 0 ? type.serialize(v, :mutation) : type.serialize(v))
|
|
122
188
|
columns << metadata.arel_table[k].name
|
|
@@ -168,6 +234,7 @@ module ActiveRecord
|
|
|
168
234
|
all_values.each do |h|
|
|
169
235
|
h.each_pair do |k, v|
|
|
170
236
|
type = metadata.type k
|
|
237
|
+
v = self.class.unwrap_attribute v
|
|
171
238
|
has_serialize_options = type.method(:serialize).arity < 0
|
|
172
239
|
all_serialized_values << (has_serialize_options ? type.serialize(v, :mutation) : type.serialize(v))
|
|
173
240
|
all_columns << metadata.arel_table[k].name
|
|
@@ -98,6 +98,16 @@ module Arel # :nodoc: all
|
|
|
98
98
|
end
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
+
# For ActiveRecord 7.0
|
|
102
|
+
def visit_ActiveModel_Attribute o, collector
|
|
103
|
+
# Do not generate a query parameter if the value should be set to the PENDING_COMMIT_TIMESTAMP(), as that is
|
|
104
|
+
# not supported as a parameter value by Cloud Spanner.
|
|
105
|
+
return collector << "PENDING_COMMIT_TIMESTAMP()" \
|
|
106
|
+
if o.type.is_a?(ActiveRecord::Type::Spanner::Time) && o.value == :commit_timestamp
|
|
107
|
+
collector.add_bind(o, &bind_block)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# For ActiveRecord 6.x
|
|
101
111
|
def visit_Arel_Nodes_BindParam o, collector
|
|
102
112
|
# Do not generate a query parameter if the value should be set to the PENDING_COMMIT_TIMESTAMP(), as that is
|
|
103
113
|
# not supported as a parameter value by Cloud Spanner.
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activerecord-spanner-adapter
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Google LLC
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2022-
|
|
11
|
+
date: 2022-06-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: google-cloud-spanner
|
|
@@ -28,16 +28,22 @@ dependencies:
|
|
|
28
28
|
name: activerecord
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
|
-
- - "
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 6.0.0
|
|
34
|
+
- - "<"
|
|
32
35
|
- !ruby/object:Gem::Version
|
|
33
|
-
version:
|
|
36
|
+
version: '7.1'
|
|
34
37
|
type: :runtime
|
|
35
38
|
prerelease: false
|
|
36
39
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
40
|
requirements:
|
|
38
|
-
- - "
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: 6.0.0
|
|
44
|
+
- - "<"
|
|
39
45
|
- !ruby/object:Gem::Version
|
|
40
|
-
version:
|
|
46
|
+
version: '7.1'
|
|
41
47
|
- !ruby/object:Gem::Dependency
|
|
42
48
|
name: autotest-suffix
|
|
43
49
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -264,6 +270,7 @@ files:
|
|
|
264
270
|
- acceptance/cases/migration/rename_column_test.rb
|
|
265
271
|
- acceptance/cases/models/calculation_query_test.rb
|
|
266
272
|
- acceptance/cases/models/generated_column_test.rb
|
|
273
|
+
- acceptance/cases/models/insert_all_test.rb
|
|
267
274
|
- acceptance/cases/models/mutation_test.rb
|
|
268
275
|
- acceptance/cases/models/query_test.rb
|
|
269
276
|
- acceptance/cases/sessions/session_not_found_test.rb
|
|
@@ -512,7 +519,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
512
519
|
- !ruby/object:Gem::Version
|
|
513
520
|
version: '0'
|
|
514
521
|
requirements: []
|
|
515
|
-
rubygems_version: 3.3.
|
|
522
|
+
rubygems_version: 3.3.14
|
|
516
523
|
signing_key:
|
|
517
524
|
specification_version: 4
|
|
518
525
|
summary: Rails ActiveRecord connector for Google Spanner Database
|