activerecord-spanner-adapter 1.0.1 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|