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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bcc2624f18893422bf8ac9c23d07fff039e79cee5f51b975054314f49820b694
4
- data.tar.gz: 7145b48f67236eb7ee9aac20bd1dfa907268419f262abf82cd6973acba25ab64
3
+ metadata.gz: 9a2ada6b62b88752f24052c1071bb3df30e1057e711045d26ecf1403b891d5b7
4
+ data.tar.gz: 831ec65b5bb4b25ce13970213a5f626e327d9ab6972f86f21076363218fc3ef7
5
5
  SHA512:
6
- metadata.gz: 49aed5257b191aee1e9271c1d04e91c2ff1663c78e9b1e70e89acda430587324991a10cb1b2aabdfd9b1e9d70ab862b3a1579fda5c032023175303fa260deeed
7
- data.tar.gz: 1764369d2a370d4c8d35cbbcd3b9c45130514cc6e76bac67e5a9dafbf2bf020734b973be44f354b13da9acaaffdebaba78e31046da56c6e62efe0546aca30611
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 Ruby 3.0 and ActiveRecord 6.0.x as that combination is not supported.
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@v2
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@v2
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):
@@ -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 Ruby 3.0 and ActiveRecord 6.0.x as that combination is not supported.
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@v2
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 Ruby 3.0 and ActiveRecord 6.0.x as that combination is not supported.
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@v2
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@v2
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@master
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 Ruby 3.0 and ActiveRecord 6.0.x as that combination is not supported.
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@v2
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@v2
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@v2
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@v2
15
+ - uses: actions/checkout@v3
16
16
  - name: setup ruby
17
17
  uses: ruby/setup-ruby@v1
18
18
  with:
19
- ruby-version: '2.5'
19
+ ruby-version: '2.7'
20
20
  - name: cache gems
21
- uses: actions/cache@v2
21
+ uses: actions/cache@v3
22
22
  with:
23
23
  path: vendor/bundle
24
24
  key: ${{ runner.os }}-rubocop-${{ hashFiles('**/Gemfile.lock') }}
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "1.0.1"
2
+ ".": "1.1.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ### 1.1.0 (2022-06-24)
4
+
5
+ #### Features
6
+
7
+ * Support insert_all and upsert_all with DML and mutations
8
+
3
9
  ### 1.0.1 (2022-04-21)
4
10
 
5
11
  #### Bug Fixes
data/Gemfile CHANGED
@@ -3,6 +3,7 @@ source "https://rubygems.org"
3
3
  # Specify your gem's dependencies in activerecord-spanner.gemspec
4
4
  gemspec
5
5
 
6
+ gem "activerecord", ENV.fetch("AR_VERSION", "~> 6.1.4")
6
7
  gem "minitest", "~> 5.15.0"
7
8
  gem "pry", "~> 0.13.0"
8
9
  gem "pry-byebug", "~> 3.9.0"
@@ -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: ENV["SPANNER_EMULATOR_HOST"] ? "" : { kind: "user_renamed", change: %w[jack john]},
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: ENV["SPANNER_EMULATOR_HOST"] ? [""] : \
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 unless ENV["SPANNER_EMULATOR_HOST"]
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" => "user_renamed", "change" => %w[bob carol]}],
88
- record.col_array_json unless ENV["SPANNER_EMULATOR_HOST"]
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: ENV["SPANNER_EMULATOR_HOST"] ? "" : { kind: "user_created", change: %w[jack alice]},
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: ENV["SPANNER_EMULATOR_HOST"] ?
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
- record.col_json unless ENV["SPANNER_EMULATOR_HOST"]
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 unless ENV["SPANNER_EMULATOR_HOST"]
137
+ record.col_array_json
141
138
  end
142
139
  end
143
140
 
@@ -18,8 +18,6 @@ module ActiveRecord
18
18
  end
19
19
 
20
20
  def test_set_json
21
- return if ENV["SPANNER_EMULATOR_HOST"]
22
-
23
21
  expected_hash = {"key"=>"value", "array_key"=>%w[value1 value2]}
24
22
  record = TestTypeModel.new details: {key: "value", array_key: %w[value1 value2]}
25
23
 
@@ -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 unless ENV["SPANNER_EMULATOR_HOST"]
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 unless ENV["SPANNER_EMULATOR_HOST"]
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|
@@ -208,7 +208,7 @@ module SpannerAdapter
208
208
  t.date :start_date
209
209
  t.datetime :start_datetime
210
210
  t.time :start_time
211
- t.json :details unless ENV["SPANNER_EMULATOR_HOST"]
211
+ t.json :details
212
212
  end
213
213
  end
214
214
 
@@ -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", "~> 6.1.4"
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
- o.foreign_keys.each do |to_table, options|
20
- statements << foreign_key_in_create(o.name, to_table, options)
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
- @schema_cache ||= SpannerSchemaCache.new connection
47
- @schema_cache.connection = connection
48
- @schema_cache
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
- private
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
- def initialize_type_map m = type_map
184
- m.register_type "BOOL", Type::Boolean.new
185
- register_class_with_limit(
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
- register_array_types m
191
+ values_list, = insert.values_list
192
+ "INSERT #{insert.into} #{values_list}"
197
193
  end
198
194
 
199
- def register_array_types m
200
- m.register_type %r{^ARRAY<BOOL>}i, Type::Spanner::Array.new(Type::Boolean.new)
201
- m.register_type %r{^ARRAY<BYTES\((MAX|d+)\)>}i, Type::Spanner::Array.new(ActiveRecord::Type::Spanner::Bytes.new)
202
- m.register_type %r{^ARRAY<DATE>}i, Type::Spanner::Array.new(Type::Date.new)
203
- m.register_type %r{^ARRAY<FLOAT64>}i, Type::Spanner::Array.new(Type::Float.new)
204
- m.register_type %r{^ARRAY<NUMERIC>}i, Type::Spanner::Array.new(Type::Decimal.new)
205
- m.register_type %r{^ARRAY<INT64>}i, Type::Spanner::Array.new(Type::Integer.new(limit: 8))
206
- m.register_type %r{^ARRAY<STRING\((MAX|d+)\)>}i, Type::Spanner::Array.new(Type::String.new)
207
- m.register_type %r{^ARRAY<TIMESTAMP>}i, Type::Spanner::Array.new(ActiveRecord::Type::Spanner::Time.new)
208
- m.register_type %r{^ARRAY<JSON>}i, Type::Spanner::Array.new(ActiveRecord::Type::Json.new)
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
- def extract_limit sql_type
212
- value = /\((.*)\)/.match sql_type
213
- return unless value
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
- value[1] == "MAX" ? "MAX" : value[1].to_i
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
- insert: Google::Cloud::Spanner::V1::Mutation::Write.new(
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
@@ -5,5 +5,5 @@
5
5
  # https://opensource.org/licenses/MIT.
6
6
 
7
7
  module ActiveRecordSpannerAdapter
8
- VERSION = "1.0.1".freeze
8
+ VERSION = "1.1.0".freeze
9
9
  end
@@ -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.1
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-04-21 00:00:00.000000000 Z
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: 6.1.4
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: 6.1.4
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.5
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