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 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