activerecord-spanner-adapter 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/acceptance-tests-on-emulator.yaml +9 -13
- data/.github/workflows/acceptance-tests-on-production.yaml +4 -3
- data/.github/workflows/ci.yaml +8 -12
- data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +9 -13
- data/.github/workflows/nightly-acceptance-tests-on-production.yaml +4 -3
- data/.github/workflows/nightly-unit-tests.yaml +8 -12
- data/.github/workflows/rubocop.yaml +3 -3
- data/.github/workflows/samples.yaml +8 -12
- data/.gitignore +0 -1
- data/.release-please-manifest.json +1 -1
- data/.rubocop.yml +2 -0
- data/Appraisals +15 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +12 -22
- data/Gemfile.lock +479 -0
- data/acceptance/cases/migration/change_table_test.rb +4 -14
- data/acceptance/cases/models/generated_column_test.rb +8 -26
- data/acceptance/test_helper.rb +10 -22
- data/activerecord-spanner-adapter.gemspec +2 -1
- data/benchmarks/db/migrate/01_create_tables.rb +1 -1
- data/examples/rails/README.md +1 -1
- data/examples/snippets/Rakefile +0 -7
- data/examples/snippets/array-data-type/db/migrate/01_create_tables.rb +1 -1
- data/examples/snippets/batch-dml/db/migrate/01_create_tables.rb +1 -1
- data/examples/snippets/bulk-insert/db/migrate/01_create_tables.rb +1 -1
- data/examples/snippets/commit-timestamp/db/migrate/01_create_tables.rb +1 -1
- data/examples/snippets/create-records/db/migrate/01_create_tables.rb +1 -1
- data/examples/snippets/date-data-type/db/migrate/01_create_tables.rb +1 -1
- data/examples/snippets/generated-column/db/migrate/01_create_tables.rb +1 -1
- data/examples/snippets/hints/db/migrate/01_create_tables.rb +1 -1
- data/examples/snippets/interleaved-tables/README.md +2 -5
- data/examples/snippets/isolation-level/db/migrate/01_create_tables.rb +1 -1
- data/examples/snippets/migrations/db/migrate/01_create_tables.rb +1 -1
- data/examples/snippets/mutations/db/migrate/01_create_tables.rb +1 -1
- data/examples/snippets/optimistic-locking/db/migrate/01_create_tables.rb +1 -1
- data/examples/snippets/partitioned-dml/db/migrate/01_create_tables.rb +1 -1
- data/examples/snippets/query-logs/application.rb +3 -3
- data/examples/snippets/query-logs/db/migrate/01_create_tables.rb +1 -1
- data/examples/snippets/quickstart/db/migrate/01_create_tables.rb +1 -1
- data/examples/snippets/read-only-transactions/db/migrate/01_create_tables.rb +1 -1
- data/examples/snippets/read-write-transactions/db/migrate/01_create_tables.rb +1 -1
- data/examples/snippets/stale-reads/db/migrate/01_create_tables.rb +1 -1
- data/examples/snippets/tags/db/migrate/01_create_tables.rb +1 -1
- data/examples/snippets/timestamp-data-type/db/migrate/01_create_tables.rb +1 -1
- data/gemfiles/7.1.gemfile +18 -0
- data/gemfiles/7.1.gemfile.lock +389 -0
- data/gemfiles/7.2.gemfile +18 -0
- data/gemfiles/7.2.gemfile.lock +479 -0
- data/gemfiles/8.0.gemfile +18 -0
- data/gemfiles/8.0.gemfile.lock +480 -0
- data/gemfiles/8.1.gemfile +18 -0
- data/gemfiles/8.1.gemfile.lock +478 -0
- data/lib/active_record/connection_adapters/spanner/database_statements.rb +62 -95
- data/lib/active_record/connection_adapters/spanner/quoting.rb +0 -22
- data/lib/active_record/connection_adapters/spanner/schema_creation.rb +3 -15
- data/lib/active_record/connection_adapters/spanner/schema_statements.rb +0 -4
- data/lib/active_record/connection_adapters/spanner_adapter.rb +12 -14
- data/lib/active_record/type/spanner/spanner_active_record_converter.rb +33 -2
- data/lib/activerecord_spanner_adapter/base.rb +5 -27
- data/lib/activerecord_spanner_adapter/connection.rb +0 -20
- data/lib/activerecord_spanner_adapter/information_schema.rb +1 -4
- data/lib/activerecord_spanner_adapter/table.rb +0 -22
- data/lib/activerecord_spanner_adapter/version.rb +1 -1
- data/lib/arel/visitors/spanner.rb +12 -15
- data/lib/spanner_client_ext.rb +6 -2
- metadata +27 -17
- data/acceptance/cases/interleaved_associations/has_many_associations_using_interleaved_test.rb +0 -221
- data/acceptance/cases/models/interleave_test.rb +0 -113
- data/acceptance/cases/transactions/optimistic_locking_test.rb +0 -152
- data/acceptance/models/album.rb +0 -15
- data/acceptance/models/track.rb +0 -23
- data/examples/snippets/interleaved-tables-before-7.1/README.md +0 -167
- data/examples/snippets/interleaved-tables-before-7.1/Rakefile +0 -13
- data/examples/snippets/interleaved-tables-before-7.1/application.rb +0 -122
- data/examples/snippets/interleaved-tables-before-7.1/config/database.yml +0 -9
- data/examples/snippets/interleaved-tables-before-7.1/db/migrate/01_create_tables.rb +0 -44
- data/examples/snippets/interleaved-tables-before-7.1/db/seeds.rb +0 -40
- data/examples/snippets/interleaved-tables-before-7.1/models/album.rb +0 -20
- data/examples/snippets/interleaved-tables-before-7.1/models/singer.rb +0 -18
- data/examples/snippets/interleaved-tables-before-7.1/models/track.rb +0 -28
data/acceptance/cases/interleaved_associations/has_many_associations_using_interleaved_test.rb
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
# Copyright 2021 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
|
-
# ActiveRecord 7.1 introduced native support for composite primary keys.
|
|
10
|
-
# This deprecates the https://github.com/composite-primary-keys/composite_primary_keys gem that was previously used in
|
|
11
|
-
# this library to support composite primary keys, which again are needed for interleaved tables. These tests use the
|
|
12
|
-
# third-party composite primary key gem and are therefore not executed for Rails 7.1 and higher.
|
|
13
|
-
return if ActiveRecord::gem_version >= Gem::Version.create('7.1.0')
|
|
14
|
-
|
|
15
|
-
require "test_helper"
|
|
16
|
-
require "models/singer"
|
|
17
|
-
require "models/album"
|
|
18
|
-
require "models/track"
|
|
19
|
-
|
|
20
|
-
module ActiveRecord
|
|
21
|
-
module Associations
|
|
22
|
-
class HasManyUsingInterleavedTest < SpannerAdapter::TestCase
|
|
23
|
-
include SpannerAdapter::Associations::TestHelper
|
|
24
|
-
|
|
25
|
-
attr_accessor :singer, :album1, :album2
|
|
26
|
-
|
|
27
|
-
def setup
|
|
28
|
-
super
|
|
29
|
-
@original_verbosity = $VERBOSE
|
|
30
|
-
$VERBOSE = nil
|
|
31
|
-
|
|
32
|
-
@singer = Singer.create first_name: "FirstName1", last_name: "LastName1"
|
|
33
|
-
|
|
34
|
-
@album2 = Album.create title: "Title2", singer: singer
|
|
35
|
-
@album1 = Album.create title: "Title1", singer: singer
|
|
36
|
-
|
|
37
|
-
@track2_1 = Track.create title: "Title2_1", album: album2, duration: 3.6
|
|
38
|
-
@track2_2 = Track.create title: "Title2_2", album: album2, duration: 3.3
|
|
39
|
-
@track1_1 = Track.create title: "Title1_1", album: album1, duration: 4.5
|
|
40
|
-
@track1_2 = Track.create title: "Title1_2", album: album1
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def teardown
|
|
44
|
-
Album.destroy_all
|
|
45
|
-
Singer.destroy_all
|
|
46
|
-
|
|
47
|
-
$VERBOSE = @original_verbosity
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def test_has_many
|
|
51
|
-
assert_equal 2, singer.albums.count
|
|
52
|
-
assert_equal singer.albums.pluck(:title).sort, %w[Title1 Title2]
|
|
53
|
-
|
|
54
|
-
assert_equal 4, singer.tracks.count
|
|
55
|
-
assert_equal singer.tracks.pluck(:title).sort, %w[Title1_1 Title1_2 Title2_1 Title2_2]
|
|
56
|
-
|
|
57
|
-
assert_equal 2, album1.tracks.count
|
|
58
|
-
assert_equal album1.tracks.pluck(:title).sort, %w[Title1_1 Title1_2]
|
|
59
|
-
assert_equal 2, album2.tracks.count
|
|
60
|
-
assert_equal album2.tracks.pluck(:title).sort, %w[Title2_1 Title2_2]
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def test_finding_using_associated_fields
|
|
64
|
-
assert_equal Album.where(singerid: singer.id).to_a, singer.albums.to_a
|
|
65
|
-
assert_equal Track.where(singerid: singer.id).to_a, singer.tracks.to_a
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def test_successful_build_association
|
|
69
|
-
album = singer.albums.build title: "New Title"
|
|
70
|
-
assert album.save
|
|
71
|
-
|
|
72
|
-
singer.reload
|
|
73
|
-
assert_equal album, singer.albums.find(album.id)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def test_successful_build_nested_association
|
|
77
|
-
track = album1.tracks.build title: "New Title", duration: 4.45
|
|
78
|
-
assert track.save
|
|
79
|
-
|
|
80
|
-
album1.reload
|
|
81
|
-
assert_equal track, album1.tracks.find(track.id)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def test_create_and_destroy_associated_records
|
|
85
|
-
singer2 = Singer.new first_name: "First", last_name: "Last"
|
|
86
|
-
singer2.albums.build title: "New Title 1", albumid: Album.next_sequence_value
|
|
87
|
-
singer2.albums.build title: "New Title 2", albumid: Album.next_sequence_value
|
|
88
|
-
singer2.save!
|
|
89
|
-
|
|
90
|
-
singer2.reload
|
|
91
|
-
|
|
92
|
-
assert_equal 2, singer2.albums.count
|
|
93
|
-
assert_equal 4, Album.count
|
|
94
|
-
|
|
95
|
-
singer2.albums.destroy_all
|
|
96
|
-
singer2.reload
|
|
97
|
-
|
|
98
|
-
assert_equal 0, singer2.albums.count
|
|
99
|
-
assert_equal 2, Album.count
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def test_create_and_destroy_nested_associated_records
|
|
103
|
-
album3 = Album.new singer: singer, title: "Title 3"
|
|
104
|
-
album3.tracks.build title: "Title3_1", duration: 2.5, singer: singer, trackid: Track.next_sequence_value
|
|
105
|
-
album3.tracks.build title: "Title3_2", singer: singer, trackid: Track.next_sequence_value
|
|
106
|
-
album3.save!
|
|
107
|
-
|
|
108
|
-
album3.reload
|
|
109
|
-
|
|
110
|
-
assert_equal 2, album3.tracks.count
|
|
111
|
-
assert_equal 6, singer.tracks.count
|
|
112
|
-
assert_equal 6, Track.count
|
|
113
|
-
|
|
114
|
-
album3.tracks.destroy_all
|
|
115
|
-
album3.reload
|
|
116
|
-
|
|
117
|
-
assert_equal 0, album3.tracks.count
|
|
118
|
-
assert_equal 4, Track.count
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def test_create_and_delete_associated_records
|
|
122
|
-
singer2 = Singer.new first_name: "First", last_name: "Last"
|
|
123
|
-
singer2.albums.build title: "Album - 11", albumid: Album.next_sequence_value
|
|
124
|
-
singer2.albums.build title: "Album - 12", albumid: Album.next_sequence_value
|
|
125
|
-
singer2.save!
|
|
126
|
-
|
|
127
|
-
singer2.reload
|
|
128
|
-
|
|
129
|
-
assert_equal 2, singer2.albums.count
|
|
130
|
-
assert_equal 4, Album.count
|
|
131
|
-
|
|
132
|
-
assert_equal 2, singer2.albums.delete_all
|
|
133
|
-
singer2.reload
|
|
134
|
-
|
|
135
|
-
assert_equal 0, singer2.albums.count
|
|
136
|
-
assert_equal 2, Album.count
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def test_create_and_delete_nested_associated_records
|
|
140
|
-
album3 = Album.new title: "Album 3", singer: singer
|
|
141
|
-
album3.tracks.build title: "Track - 31", singer: singer, trackid: Track.next_sequence_value
|
|
142
|
-
album3.tracks.build title: "Track - 32", singer: singer, trackid: Track.next_sequence_value
|
|
143
|
-
album3.save!
|
|
144
|
-
|
|
145
|
-
album3.reload
|
|
146
|
-
|
|
147
|
-
assert_equal 2, album3.tracks.count
|
|
148
|
-
assert_equal 6, Track.count
|
|
149
|
-
|
|
150
|
-
assert_equal 2, album3.tracks.delete_all
|
|
151
|
-
album3.reload
|
|
152
|
-
|
|
153
|
-
assert_equal 0, album3.tracks.count
|
|
154
|
-
assert_equal 4, Track.count
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def test_update_associated_records
|
|
158
|
-
count = singer.albums.update_all title: "Title - Update"
|
|
159
|
-
assert_equal singer.albums.count, count
|
|
160
|
-
|
|
161
|
-
singer.reload
|
|
162
|
-
singer.albums.each do |album|
|
|
163
|
-
assert_equal "Title - Update", album.title
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def test_update_nested_associated_records
|
|
168
|
-
count = album1.tracks.update_all title: "Title - Update", duration: 6.626
|
|
169
|
-
assert_equal album1.tracks.count, count
|
|
170
|
-
|
|
171
|
-
album1.reload
|
|
172
|
-
album1.tracks.each do |track|
|
|
173
|
-
assert_equal "Title - Update", track.title
|
|
174
|
-
assert_equal 6.626, track.duration
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def test_fetch_associated_record_with_order
|
|
179
|
-
albums = singer.albums.order title: :desc
|
|
180
|
-
assert_equal %w[Title2 Title1], albums.pluck(:title)
|
|
181
|
-
|
|
182
|
-
albums = singer.albums.order title: :asc
|
|
183
|
-
assert_equal %w[Title1 Title2], albums.pluck(:title)
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def test_fetch_nested_associated_record_with_order
|
|
187
|
-
tracks = album1.tracks.order duration: :desc
|
|
188
|
-
assert_equal [4.5, nil], tracks.pluck(:duration)
|
|
189
|
-
|
|
190
|
-
tracks = album1.tracks.order duration: :asc
|
|
191
|
-
assert_equal [nil, 4.5], tracks.pluck(:duration)
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def test_set_counter_cache
|
|
195
|
-
singer.tracks.create! title: "New Title 1", album: album1
|
|
196
|
-
singer.tracks.create! title: "New Title 2", album: album2
|
|
197
|
-
|
|
198
|
-
singer.reload
|
|
199
|
-
assert_equal 6, singer.tracks_count
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
def test_cascade_destroy
|
|
203
|
-
assert_equal 4, singer.tracks.count
|
|
204
|
-
|
|
205
|
-
assert album1.destroy
|
|
206
|
-
|
|
207
|
-
singer.reload
|
|
208
|
-
assert_equal 2, singer.tracks.count
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def test_cascade_delete
|
|
212
|
-
assert_equal 4, singer.tracks.count
|
|
213
|
-
|
|
214
|
-
assert album1.delete
|
|
215
|
-
|
|
216
|
-
singer.reload
|
|
217
|
-
assert_equal 2, singer.tracks.count
|
|
218
|
-
end
|
|
219
|
-
end
|
|
220
|
-
end
|
|
221
|
-
end
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
# Copyright 2021 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
|
-
# ActiveRecord 7.1 introduced native support for composite primary keys.
|
|
10
|
-
# This deprecates the https://github.com/composite-primary-keys/composite_primary_keys gem that was previously used in
|
|
11
|
-
# this library to support composite primary keys, which again are needed for interleaved tables. These tests use the
|
|
12
|
-
# third-party composite primary key gem and are therefore not executed for Rails 7.1 and higher.
|
|
13
|
-
return if ActiveRecord::gem_version >= Gem::Version.create('7.1.0')
|
|
14
|
-
|
|
15
|
-
require "test_helper"
|
|
16
|
-
require "test_helpers/with_separate_database"
|
|
17
|
-
|
|
18
|
-
module ActiveRecord
|
|
19
|
-
module Model
|
|
20
|
-
class InterleaveTest
|
|
21
|
-
class WithPartialInsertsDisabledTest < SpannerAdapter::TestCase
|
|
22
|
-
include SpannerAdapter::Associations::TestHelper
|
|
23
|
-
|
|
24
|
-
class Singer < ActiveRecord::Base
|
|
25
|
-
has_many :albums, foreign_key: :singerid, dependent: :delete_all
|
|
26
|
-
has_many :tracks, foreign_key: :singerid
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
class Album < ActiveRecord::Base
|
|
30
|
-
self.primary_keys = :singerid, :albumid
|
|
31
|
-
|
|
32
|
-
belongs_to :singer, foreign_key: :singerid
|
|
33
|
-
|
|
34
|
-
if ActiveRecord::VERSION::MAJOR >= 7
|
|
35
|
-
self.partial_inserts = false
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
attr_accessor :singer
|
|
40
|
-
|
|
41
|
-
def setup
|
|
42
|
-
super
|
|
43
|
-
|
|
44
|
-
@singer = Singer.create first_name: "FirstName", last_name: "LastName"
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def teardown
|
|
48
|
-
Album.destroy_all
|
|
49
|
-
Singer.destroy_all
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def test_with_partial_inserts_disabled
|
|
53
|
-
Album.create! title: "Title3", singer: singer
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
class StringParentKeyTest < SpannerAdapter::TestCase
|
|
58
|
-
include TestHelpers::WithSeparateDatabase
|
|
59
|
-
|
|
60
|
-
class Singer < ActiveRecord::Base
|
|
61
|
-
has_many :albums, foreign_key: :singer_id
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
class Album < ActiveRecord::Base
|
|
65
|
-
self.primary_keys = [:singer_id, :album_id]
|
|
66
|
-
belongs_to :singer, foreign_key: :singer_id
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def setup
|
|
70
|
-
super
|
|
71
|
-
|
|
72
|
-
connection.ddl_batch do
|
|
73
|
-
connection.create_table :singers, id: false do |t|
|
|
74
|
-
t.string :singer_id, limit: 36, primary_key: true, null: false
|
|
75
|
-
t.string :name, null: false
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
connection.create_table :albums, id: false do |t|
|
|
79
|
-
t.interleave_in :singers
|
|
80
|
-
t.string :singer_id, limit: 36, parent_key: true, primary_key: true, null: false
|
|
81
|
-
t.integer :album_id, primary_key: true, null: false
|
|
82
|
-
t.string :title, null: false
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
@singer = Singer.create!(id: SecureRandom.uuid, name: "a singer")
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def test_create_album
|
|
90
|
-
Album.create!(singer: @singer, title: "an album")
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def test_update_album
|
|
94
|
-
album = Album.create!(singer: @singer, title: "an album")
|
|
95
|
-
album.update!(title: "an album 2")
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def test_get_album
|
|
99
|
-
album = Album.create!(singer: @singer, title: "an album")
|
|
100
|
-
got_album = Album.find([@singer.singer_id, album.album_id])
|
|
101
|
-
assert_equal(album, got_album)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def test_get_albums_as_children
|
|
105
|
-
album1 = Album.create!(singer: @singer, title: "album1")
|
|
106
|
-
album2 = Album.create!(singer: @singer, title: "album2")
|
|
107
|
-
albums = @singer.albums.order(:title)
|
|
108
|
-
assert_equal([album1, album2], albums)
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
end
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
# Copyright 2021 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
|
-
# ActiveRecord 7.1 introduced native support for composite primary keys.
|
|
10
|
-
# This deprecates the https://github.com/composite-primary-keys/composite_primary_keys gem that was previously used in
|
|
11
|
-
# this library to support composite primary keys, which again are needed for interleaved tables. These tests use the
|
|
12
|
-
# third-party composite primary key gem and are therefore not executed for Rails 7.1 and higher.
|
|
13
|
-
return if ActiveRecord::gem_version >= Gem::Version.create('7.1.0')
|
|
14
|
-
|
|
15
|
-
require "test_helper"
|
|
16
|
-
require "models/singer"
|
|
17
|
-
require "models/album"
|
|
18
|
-
require "models/track"
|
|
19
|
-
|
|
20
|
-
module ActiveRecord
|
|
21
|
-
module Transactions
|
|
22
|
-
class OptimisticLockingTest < SpannerAdapter::TestCase
|
|
23
|
-
include SpannerAdapter::Associations::TestHelper
|
|
24
|
-
|
|
25
|
-
def setup
|
|
26
|
-
super
|
|
27
|
-
|
|
28
|
-
@original_verbosity = $VERBOSE
|
|
29
|
-
$VERBOSE = nil
|
|
30
|
-
|
|
31
|
-
singer = Singer.create first_name: "Pete", last_name: "Allison"
|
|
32
|
-
album = Album.create title: "Musical Jeans", singer: singer
|
|
33
|
-
Track.create title: "Increased Headline", album: album, singer: singer
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def teardown
|
|
37
|
-
super
|
|
38
|
-
|
|
39
|
-
Track.delete_all
|
|
40
|
-
Album.delete_all
|
|
41
|
-
Singer.delete_all
|
|
42
|
-
|
|
43
|
-
$VERBOSE = @original_verbosity
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Runs the given block in a transaction with the given isolation level, or without a transaction if isolation is
|
|
47
|
-
# nil.
|
|
48
|
-
def run_in_transaction isolation
|
|
49
|
-
if isolation
|
|
50
|
-
Base.transaction isolation: isolation do
|
|
51
|
-
yield
|
|
52
|
-
end
|
|
53
|
-
else
|
|
54
|
-
yield
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def test_update_single_record_increases_version_number
|
|
59
|
-
[nil, :serializable, :buffered_mutations].each do |isolation|
|
|
60
|
-
singer = Singer.all.sample
|
|
61
|
-
original_version = singer.lock_version
|
|
62
|
-
|
|
63
|
-
run_in_transaction isolation do
|
|
64
|
-
singer.update last_name: "Peterson-#{singer.last_name}"
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
singer.reload
|
|
68
|
-
assert_equal original_version + 1, singer.lock_version
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def test_update_multiple_records_increases_version_numbers
|
|
73
|
-
singer = Singer.all.sample
|
|
74
|
-
album = Album.all.sample
|
|
75
|
-
track = Track.all.sample
|
|
76
|
-
[nil, :serializable, :buffered_mutations].each do |isolation|
|
|
77
|
-
original_singer_version = singer.reload.lock_version
|
|
78
|
-
original_album_version = album.reload.lock_version
|
|
79
|
-
original_track_version = track.reload.lock_version
|
|
80
|
-
|
|
81
|
-
run_in_transaction isolation do
|
|
82
|
-
singer.update last_name: "Peterson-#{singer.last_name}"
|
|
83
|
-
singer.albums.each { |album| album.update title: "Updated: #{album.title}" }
|
|
84
|
-
singer.tracks.each { |track| track.update title: "Updated: #{track.title}" }
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
singer.reload
|
|
88
|
-
assert_equal original_singer_version + 1, singer.lock_version
|
|
89
|
-
singer.albums.each { |album| assert_equal original_album_version + 1, album.lock_version }
|
|
90
|
-
singer.tracks.each { |track| assert_equal original_track_version + 1, track.lock_version }
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def test_concurrent_update_single_record_fails
|
|
95
|
-
[nil, :serializable, :buffered_mutations].each do |isolation|
|
|
96
|
-
singer = Singer.all.sample
|
|
97
|
-
original_version = singer.lock_version
|
|
98
|
-
|
|
99
|
-
# Update the singer in a separate thread to simulate a concurrent update.
|
|
100
|
-
t = Thread.new do
|
|
101
|
-
singer2 = Singer.find singer.id
|
|
102
|
-
singer2.update last_name: "Henderson-#{singer2.last_name}"
|
|
103
|
-
end
|
|
104
|
-
t.join
|
|
105
|
-
|
|
106
|
-
run_in_transaction isolation do
|
|
107
|
-
assert_raises ActiveRecord::StaleObjectError do
|
|
108
|
-
singer.update last_name: "Peterson-#{singer.last_name}"
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
singer.reload
|
|
113
|
-
assert_equal original_version + 1, singer.lock_version
|
|
114
|
-
assert singer.last_name.start_with?("Henderson-")
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def test_concurrent_update_multiple_records_fails
|
|
119
|
-
singer = Singer.all.sample
|
|
120
|
-
album = Album.all.sample
|
|
121
|
-
track = Track.all.sample
|
|
122
|
-
[nil, :serializable, :buffered_mutations].each do |isolation|
|
|
123
|
-
original_singer_version = singer.reload.lock_version
|
|
124
|
-
original_album_version = album.reload.lock_version
|
|
125
|
-
original_track_version = track.reload.lock_version
|
|
126
|
-
|
|
127
|
-
# Update the singer in a separate thread to simulate a concurrent update.
|
|
128
|
-
t = Thread.new do
|
|
129
|
-
singer2 = Singer.find singer.id
|
|
130
|
-
singer2.update last_name: "Henderson-#{singer2.last_name}"
|
|
131
|
-
end
|
|
132
|
-
t.join
|
|
133
|
-
|
|
134
|
-
run_in_transaction isolation do
|
|
135
|
-
assert_raises ActiveRecord::StaleObjectError do
|
|
136
|
-
singer.update last_name: "Peterson-#{singer.last_name}"
|
|
137
|
-
end
|
|
138
|
-
singer.albums.each { |album| album.update title: "Updated: #{album.title}" }
|
|
139
|
-
singer.tracks.each { |track| track.update title: "Updated: #{track.title}" }
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
singer.reload
|
|
143
|
-
# The singer should be updated, but only by the separate thread.
|
|
144
|
-
assert_equal original_singer_version + 1, singer.lock_version
|
|
145
|
-
assert singer.last_name.start_with? "Henderson-"
|
|
146
|
-
singer.albums.each { |album| assert_equal original_album_version + 1, album.lock_version }
|
|
147
|
-
singer.tracks.each { |track| assert_equal original_track_version + 1, track.lock_version }
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
end
|
data/acceptance/models/album.rb
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# Copyright 2021 Google LLC
|
|
2
|
-
#
|
|
3
|
-
# Use of this source code is governed by an MIT-style
|
|
4
|
-
# license that can be found in the LICENSE file or at
|
|
5
|
-
# https://opensource.org/licenses/MIT.
|
|
6
|
-
|
|
7
|
-
class Album < ActiveRecord::Base
|
|
8
|
-
# Register both primary key columns with composite_primary_keys
|
|
9
|
-
self.primary_keys = :singerid, :albumid
|
|
10
|
-
|
|
11
|
-
# The relationship with singer is not really a foreign key, but an INTERLEAVE IN relationship. We still need to
|
|
12
|
-
# use the `foreign_key` attribute to indicate which column to use for the relationship.
|
|
13
|
-
belongs_to :singer, foreign_key: :singerid
|
|
14
|
-
has_many :tracks, foreign_key: [:singerid, :albumid], dependent: :delete_all
|
|
15
|
-
end
|
data/acceptance/models/track.rb
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# Copyright 2021 Google LLC
|
|
2
|
-
#
|
|
3
|
-
# Use of this source code is governed by an MIT-style
|
|
4
|
-
# license that can be found in the LICENSE file or at
|
|
5
|
-
# https://opensource.org/licenses/MIT.
|
|
6
|
-
|
|
7
|
-
class Track < ActiveRecord::Base
|
|
8
|
-
# Register both primary key columns with composite_primary_keys
|
|
9
|
-
self.primary_keys = :singerid, :albumid, :trackid
|
|
10
|
-
|
|
11
|
-
belongs_to :album, foreign_key: [:singerid, :albumid]
|
|
12
|
-
belongs_to :singer, foreign_key: :singerid, counter_cache: true
|
|
13
|
-
|
|
14
|
-
def initialize attributes = nil
|
|
15
|
-
super
|
|
16
|
-
self.singer ||= self.album&.singer
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def album=value
|
|
20
|
-
super
|
|
21
|
-
self.singer = value&.singer
|
|
22
|
-
end
|
|
23
|
-
end
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
# Sample - Interleaved Tables - Before Rails 7.1.0
|
|
2
|
-
|
|
3
|
-
__NOTE__: This example uses the third-party gem `composite_primary_keys`. This is only supported with
|
|
4
|
-
Rails version < `7.1.0`.
|
|
5
|
-
|
|
6
|
-
This example shows how to use interleaved tables with the Spanner ActiveRecord adapter.
|
|
7
|
-
Interleaved tables use composite primary keys. This is not natively supported by ActiveRecord.
|
|
8
|
-
It is therefore necessary to use the `composite_primary_keys` (https://github.com/composite-primary-keys/composite_primary_keys)
|
|
9
|
-
gem to enable the use of interleaved tables.
|
|
10
|
-
|
|
11
|
-
See https://cloud.google.com/spanner/docs/schema-and-data-model#creating-interleaved-tables for more information
|
|
12
|
-
on interleaved tables if you are not familiar with this concept.
|
|
13
|
-
|
|
14
|
-
## Creating Interleaved Tables in ActiveRecord
|
|
15
|
-
You can create interleaved tables using migrations in ActiveRecord by using the following Spanner ActiveRecord specific
|
|
16
|
-
methods that are defined on `TableDefinition`:
|
|
17
|
-
* `interleave_in`: Specifies which parent table a child table should be interleaved in and optionally whether
|
|
18
|
-
deletes of a parent record should automatically cascade delete all child records.
|
|
19
|
-
* `parent_key`: Creates a column that is a reference to (a part of) the primary key of the parent table. Each child
|
|
20
|
-
table must include all the primary key columns of the parent table as a `parent_key`.
|
|
21
|
-
|
|
22
|
-
Cloud Spanner requires a child table to include the exact same primary key columns as the parent table in addition to
|
|
23
|
-
the primary key column(s) of the child table. This means that the default `id` primary key column of ActiveRecord is
|
|
24
|
-
not usable in combination with interleaved tables. Instead each primary key column should be prefixed with the table
|
|
25
|
-
name of the table that it references, or use some other unique name.
|
|
26
|
-
|
|
27
|
-
## Example Data Model
|
|
28
|
-
This example uses the following table schema:
|
|
29
|
-
|
|
30
|
-
```sql
|
|
31
|
-
CREATE TABLE singers (
|
|
32
|
-
singerid INT64 NOT NULL,
|
|
33
|
-
first_name STRING(MAX),
|
|
34
|
-
last_name STRING(MAX)
|
|
35
|
-
) PRIMARY KEY (singerid);
|
|
36
|
-
|
|
37
|
-
CREATE TABLE albums (
|
|
38
|
-
albumid INT64 NOT NULL,
|
|
39
|
-
singerid INT64 NOT NULL,
|
|
40
|
-
title STRING(MAX)
|
|
41
|
-
) PRIMARY KEY (singerid, albumid), INTERLEAVE IN PARENT singers;
|
|
42
|
-
|
|
43
|
-
CREATE TABLE tracks (
|
|
44
|
-
trackid INT64 NOT NULL,
|
|
45
|
-
singerid INT64 NOT NULL,
|
|
46
|
-
albumid INT64 NOT NULL,
|
|
47
|
-
title STRING(MAX),
|
|
48
|
-
duration NUMERIC
|
|
49
|
-
) PRIMARY KEY (singerid, albumid, trackid), INTERLEAVE IN PARENT albums ON DELETE CASCADE;
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
This schema can be created in ActiveRecord as follows:
|
|
53
|
-
|
|
54
|
-
```ruby
|
|
55
|
-
create_table :singers, id: false do |t|
|
|
56
|
-
# Explicitly define the primary key with a custom name to prevent all primary key columns from being named `id`.
|
|
57
|
-
t.primary_key :singerid
|
|
58
|
-
t.string :first_name
|
|
59
|
-
t.string :last_name
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
create_table :albums, id: false do |t|
|
|
63
|
-
# Interleave the `albums` table in the parent table `singers`.
|
|
64
|
-
t.interleave_in :singers
|
|
65
|
-
t.primary_key :albumid
|
|
66
|
-
# `singerid` is defined as a `parent_key` which makes it a part of the primary key in the table definition, but
|
|
67
|
-
# it is not presented to ActiveRecord as part of the primary key, to prevent ActiveRecord from considering this
|
|
68
|
-
# to be an entity with a composite primary key (which is not supported by ActiveRecord).
|
|
69
|
-
t.parent_key :singerid
|
|
70
|
-
t.string :title
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
create_table :tracks, id: false do |t|
|
|
74
|
-
# Interleave the `tracks` table in the parent table `albums` and cascade delete all tracks that belong to an
|
|
75
|
-
# album when an album is deleted.
|
|
76
|
-
t.interleave_in :albums, :cascade
|
|
77
|
-
# Add `trackid` as the primary key in the table definition. Add the other key parts as
|
|
78
|
-
# a `parent_key`.
|
|
79
|
-
t.primary_key :trackid
|
|
80
|
-
# `singerid` and `albumid` form the parent key of `tracks`. These are part of the primary key definition in the
|
|
81
|
-
# database, but are presented as parent keys to ActiveRecord.
|
|
82
|
-
t.parent_key :singerid
|
|
83
|
-
t.parent_key :albumid
|
|
84
|
-
t.string :title
|
|
85
|
-
t.numeric :duration
|
|
86
|
-
end
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## Models for Interleaved Tables
|
|
90
|
-
The model definition for an interleaved table (a child table) must use the `primary_keys=col1, col2, ...`
|
|
91
|
-
function from the `composite_primary_keys` gem.
|
|
92
|
-
|
|
93
|
-
An interleaved table parent/child relationship must be modelled as a `belongs_to`/`has_many` association in
|
|
94
|
-
ActiveRecord. As the columns that are used to reference a parent record use a custom column name, it is required to also
|
|
95
|
-
include the custom column name(s) in the `belongs_to` and `has_many` definitions.
|
|
96
|
-
|
|
97
|
-
Instances of these models can be used in the same way as any other association in ActiveRecord, but with a couple of
|
|
98
|
-
inherent limitations:
|
|
99
|
-
* It is not possible to change the parent record of a child record. For instance, changing the singer of an album in the
|
|
100
|
-
above example is impossible, as Cloud Spanner does not allow such an update.
|
|
101
|
-
* It is not possible to de-reference a parent record by setting it to null.
|
|
102
|
-
* It is only possible to delete a parent record with existing child records, if the child records are also deleted. This
|
|
103
|
-
can be done by enabling ON DELETE CASCADE in Cloud Spanner, or by deleting the child records using ActiveRecord.
|
|
104
|
-
|
|
105
|
-
### Example Models
|
|
106
|
-
|
|
107
|
-
```ruby
|
|
108
|
-
class Singer < ActiveRecord::Base
|
|
109
|
-
# `albums` is defined as INTERLEAVE IN PARENT `singers`.
|
|
110
|
-
# The primary key of `albums` is (`singerid`, `albumid`).
|
|
111
|
-
has_many :albums, foreign_key: :singerid
|
|
112
|
-
|
|
113
|
-
# `tracks` is defined as INTERLEAVE IN PARENT `albums`.
|
|
114
|
-
# The primary key of `tracks` is (`singerid`, `albumid`, `trackid`).
|
|
115
|
-
# The `singerid` column can be used to associate tracks with a singer without the need to go through albums.
|
|
116
|
-
# Note also that the inclusion of `singerid` as a column in `tracks` is required in order to make `tracks` a child
|
|
117
|
-
# table of `albums` which has primary key (`singerid`, `albumid`).
|
|
118
|
-
has_many :tracks, foreign_key: :singerid
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
class Album < ActiveRecord::Base
|
|
122
|
-
# Use the `composite_primary_key` gem to create a composite primary key definition for the model.
|
|
123
|
-
self.primary_keys = :singerid, :albumid
|
|
124
|
-
|
|
125
|
-
# `albums` is defined as INTERLEAVE IN PARENT `singers`.
|
|
126
|
-
# The primary key of `singers` is `singerid`.
|
|
127
|
-
belongs_to :singer, foreign_key: :singerid
|
|
128
|
-
|
|
129
|
-
# `tracks` is defined as INTERLEAVE IN PARENT `albums`.
|
|
130
|
-
# The primary key of `albums` is (`singerid`, `albumid`).
|
|
131
|
-
has_many :tracks, foreign_key: [:singerid, :albumid]
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
class Track < ActiveRecord::Base
|
|
135
|
-
# Use the `composite_primary_key` gem to create a composite primary key definition for the model.
|
|
136
|
-
self.primary_keys = :singerid, :albumid, :trackid
|
|
137
|
-
|
|
138
|
-
# `tracks` is defined as INTERLEAVE IN PARENT `albums`. The primary key of `albums` is (`singerid`, `albumid`).
|
|
139
|
-
belongs_to :album, foreign_key: [:singerid, :albumid]
|
|
140
|
-
|
|
141
|
-
# `tracks` also has a `singerid` column that can be used to associate a Track with a Singer.
|
|
142
|
-
belongs_to :singer, foreign_key: :singerid
|
|
143
|
-
|
|
144
|
-
# Override the default initialize method to automatically set the singer attribute when an album is given.
|
|
145
|
-
def initialize attributes = nil
|
|
146
|
-
super
|
|
147
|
-
self.singer ||= album&.singer
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def album=value
|
|
151
|
-
super
|
|
152
|
-
# Ensure the singer of this track is equal to the singer of the album that is set.
|
|
153
|
-
self.singer = value&.singer
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
## Running the Sample
|
|
159
|
-
|
|
160
|
-
The sample will automatically start a Spanner Emulator in a docker container and execute the sample
|
|
161
|
-
against that emulator. The emulator will automatically be stopped when the application finishes.
|
|
162
|
-
|
|
163
|
-
Run the application with the command
|
|
164
|
-
|
|
165
|
-
```bash
|
|
166
|
-
bundle exec rake run
|
|
167
|
-
```
|