activerecord-spanner-adapter 2.5.1 → 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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/acceptance-tests-on-emulator.yaml +19 -15
  3. data/.github/workflows/acceptance-tests-on-production.yaml +13 -6
  4. data/.github/workflows/ci.yaml +18 -14
  5. data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +19 -15
  6. data/.github/workflows/nightly-acceptance-tests-on-production.yaml +13 -6
  7. data/.github/workflows/nightly-unit-tests.yaml +18 -14
  8. data/.github/workflows/rubocop.yaml +9 -4
  9. data/.github/workflows/samples.yaml +18 -12
  10. data/.gitignore +0 -1
  11. data/.release-please-manifest.json +1 -1
  12. data/.rubocop.yml +2 -0
  13. data/Appraisals +15 -0
  14. data/CHANGELOG.md +19 -0
  15. data/Gemfile +12 -20
  16. data/Gemfile.lock +479 -0
  17. data/README.md +3 -3
  18. data/acceptance/cases/migration/change_table_test.rb +4 -14
  19. data/acceptance/cases/models/generated_column_test.rb +8 -26
  20. data/acceptance/test_helper.rb +10 -22
  21. data/activerecord-spanner-adapter.gemspec +4 -3
  22. data/benchmarks/db/migrate/01_create_tables.rb +1 -1
  23. data/examples/rails/README.md +1 -1
  24. data/examples/snippets/Rakefile +0 -7
  25. data/examples/snippets/array-data-type/db/migrate/01_create_tables.rb +1 -1
  26. data/examples/snippets/batch-dml/db/migrate/01_create_tables.rb +1 -1
  27. data/examples/snippets/bulk-insert/db/migrate/01_create_tables.rb +1 -1
  28. data/examples/snippets/commit-timestamp/db/migrate/01_create_tables.rb +1 -1
  29. data/examples/snippets/create-records/db/migrate/01_create_tables.rb +1 -1
  30. data/examples/snippets/date-data-type/db/migrate/01_create_tables.rb +1 -1
  31. data/examples/snippets/generated-column/db/migrate/01_create_tables.rb +1 -1
  32. data/examples/snippets/hints/db/migrate/01_create_tables.rb +1 -1
  33. data/examples/snippets/interleaved-tables/README.md +2 -5
  34. data/examples/snippets/isolation-level/db/migrate/01_create_tables.rb +1 -1
  35. data/examples/snippets/migrations/db/migrate/01_create_tables.rb +1 -1
  36. data/examples/snippets/mutations/db/migrate/01_create_tables.rb +1 -1
  37. data/examples/snippets/optimistic-locking/db/migrate/01_create_tables.rb +1 -1
  38. data/examples/snippets/partitioned-dml/db/migrate/01_create_tables.rb +1 -1
  39. data/examples/snippets/query-logs/application.rb +3 -3
  40. data/examples/snippets/query-logs/db/migrate/01_create_tables.rb +1 -1
  41. data/examples/snippets/quickstart/db/migrate/01_create_tables.rb +1 -1
  42. data/examples/snippets/read-only-transactions/db/migrate/01_create_tables.rb +1 -1
  43. data/examples/snippets/read-write-transactions/db/migrate/01_create_tables.rb +1 -1
  44. data/examples/snippets/stale-reads/db/migrate/01_create_tables.rb +1 -1
  45. data/examples/snippets/tags/db/migrate/01_create_tables.rb +1 -1
  46. data/examples/snippets/timestamp-data-type/db/migrate/01_create_tables.rb +1 -1
  47. data/gemfiles/7.1.gemfile +18 -0
  48. data/gemfiles/7.1.gemfile.lock +389 -0
  49. data/gemfiles/7.2.gemfile +18 -0
  50. data/gemfiles/7.2.gemfile.lock +479 -0
  51. data/gemfiles/8.0.gemfile +18 -0
  52. data/gemfiles/8.0.gemfile.lock +480 -0
  53. data/gemfiles/8.1.gemfile +18 -0
  54. data/gemfiles/8.1.gemfile.lock +478 -0
  55. data/lib/active_record/connection_adapters/spanner/database_statements.rb +62 -95
  56. data/lib/active_record/connection_adapters/spanner/quoting.rb +0 -22
  57. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +3 -15
  58. data/lib/active_record/connection_adapters/spanner/schema_statements.rb +0 -4
  59. data/lib/active_record/connection_adapters/spanner_adapter.rb +12 -14
  60. data/lib/active_record/type/spanner/spanner_active_record_converter.rb +33 -2
  61. data/lib/activerecord_spanner_adapter/base.rb +5 -27
  62. data/lib/activerecord_spanner_adapter/connection.rb +0 -20
  63. data/lib/activerecord_spanner_adapter/information_schema.rb +1 -4
  64. data/lib/activerecord_spanner_adapter/table.rb +0 -22
  65. data/lib/activerecord_spanner_adapter/version.rb +1 -1
  66. data/lib/arel/visitors/spanner.rb +12 -15
  67. data/lib/spanner_client_ext.rb +6 -2
  68. metadata +30 -20
  69. data/acceptance/cases/interleaved_associations/has_many_associations_using_interleaved_test.rb +0 -221
  70. data/acceptance/cases/models/interleave_test.rb +0 -113
  71. data/acceptance/cases/transactions/optimistic_locking_test.rb +0 -152
  72. data/acceptance/models/album.rb +0 -15
  73. data/acceptance/models/track.rb +0 -23
  74. data/examples/snippets/interleaved-tables-before-7.1/README.md +0 -167
  75. data/examples/snippets/interleaved-tables-before-7.1/Rakefile +0 -13
  76. data/examples/snippets/interleaved-tables-before-7.1/application.rb +0 -122
  77. data/examples/snippets/interleaved-tables-before-7.1/config/database.yml +0 -9
  78. data/examples/snippets/interleaved-tables-before-7.1/db/migrate/01_create_tables.rb +0 -44
  79. data/examples/snippets/interleaved-tables-before-7.1/db/seeds.rb +0 -40
  80. data/examples/snippets/interleaved-tables-before-7.1/models/album.rb +0 -20
  81. data/examples/snippets/interleaved-tables-before-7.1/models/singer.rb +0 -18
  82. data/examples/snippets/interleaved-tables-before-7.1/models/track.rb +0 -28
@@ -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
@@ -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
@@ -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
- ```