activerecord-spanner-adapter 1.0.1 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -1
  3. data/.github/sync-repo-settings.yaml +2 -2
  4. data/.github/workflows/acceptance-tests-on-emulator.yaml +10 -6
  5. data/.github/workflows/acceptance-tests-on-production.yaml +1 -1
  6. data/.github/workflows/ci.yaml +10 -8
  7. data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +14 -5
  8. data/.github/workflows/nightly-acceptance-tests-on-production.yaml +2 -2
  9. data/.github/workflows/nightly-unit-tests.yaml +14 -5
  10. data/.github/workflows/release-please.yml +2 -2
  11. data/.github/workflows/rubocop.yaml +3 -3
  12. data/.kokoro/release.sh +1 -3
  13. data/.release-please-manifest.json +1 -1
  14. data/.toys/release.rb +8 -2
  15. data/CHANGELOG.md +18 -0
  16. data/Gemfile +5 -1
  17. data/acceptance/cases/interleaved_associations/has_many_associations_using_interleaved_test.rb +12 -8
  18. data/acceptance/cases/models/insert_all_test.rb +150 -0
  19. data/acceptance/cases/transactions/optimistic_locking_test.rb +5 -0
  20. data/acceptance/cases/type/all_types_test.rb +10 -13
  21. data/acceptance/cases/type/json_test.rb +0 -2
  22. data/acceptance/models/album.rb +7 -2
  23. data/acceptance/models/singer.rb +2 -2
  24. data/acceptance/models/track.rb +5 -2
  25. data/acceptance/schema/schema.rb +2 -4
  26. data/acceptance/test_helper.rb +1 -1
  27. data/activerecord-spanner-adapter.gemspec +1 -1
  28. data/examples/snippets/interleaved-tables/README.md +164 -0
  29. data/examples/snippets/interleaved-tables/Rakefile +13 -0
  30. data/examples/snippets/interleaved-tables/application.rb +126 -0
  31. data/examples/snippets/interleaved-tables/config/database.yml +8 -0
  32. data/examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb +44 -0
  33. data/examples/snippets/interleaved-tables/db/schema.rb +32 -0
  34. data/examples/snippets/interleaved-tables/db/seeds.rb +40 -0
  35. data/examples/snippets/interleaved-tables/models/album.rb +20 -0
  36. data/examples/snippets/interleaved-tables/models/singer.rb +18 -0
  37. data/examples/snippets/interleaved-tables/models/track.rb +28 -0
  38. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +10 -4
  39. data/lib/active_record/connection_adapters/spanner_adapter.rb +64 -31
  40. data/lib/activerecord_spanner_adapter/base.rb +150 -17
  41. data/lib/activerecord_spanner_adapter/connection.rb +1 -1
  42. data/lib/activerecord_spanner_adapter/relation.rb +21 -0
  43. data/lib/activerecord_spanner_adapter/transaction.rb +4 -4
  44. data/lib/activerecord_spanner_adapter/version.rb +1 -1
  45. data/lib/arel/visitors/spanner.rb +10 -0
  46. metadata +25 -7
@@ -4,9 +4,14 @@
4
4
  # license that can be found in the LICENSE file or at
5
5
  # https://opensource.org/licenses/MIT.
6
6
 
7
+ require "composite_primary_keys"
8
+
7
9
  class Album < ActiveRecord::Base
10
+ # Register both primary key columns with composite_primary_keys
11
+ self.primary_keys = :singerid, :albumid
12
+
8
13
  # The relationship with singer is not really a foreign key, but an INTERLEAVE IN relationship. We still need to
9
14
  # use the `foreign_key` attribute to indicate which column to use for the relationship.
10
- belongs_to :singer, foreign_key: "singerid"
11
- has_many :tracks, foreign_key: "albumid", dependent: :delete_all
15
+ belongs_to :singer, foreign_key: :singerid
16
+ has_many :tracks, foreign_key: [:singerid, :albumid], dependent: :delete_all
12
17
  end
@@ -5,6 +5,6 @@
5
5
  # https://opensource.org/licenses/MIT.
6
6
 
7
7
  class Singer < ActiveRecord::Base
8
- has_many :albums, foreign_key: "singerid", dependent: :delete_all
9
- has_many :tracks, foreign_key: "singerid"
8
+ has_many :albums, foreign_key: :singerid, dependent: :delete_all
9
+ has_many :tracks, foreign_key: :singerid
10
10
  end
@@ -5,8 +5,11 @@
5
5
  # https://opensource.org/licenses/MIT.
6
6
 
7
7
  class Track < ActiveRecord::Base
8
- belongs_to :album, foreign_key: "albumid"
9
- belongs_to :singer, foreign_key: "singerid", counter_cache: true
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
10
13
 
11
14
  def initialize attributes = nil
12
15
  super
@@ -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"
@@ -0,0 +1,164 @@
1
+ # Sample - Interleaved Tables
2
+
3
+ This example shows how to use interleaved tables with the Spanner ActiveRecord adapter.
4
+ Interleaved tables use composite primary keys. This is not natively supported by ActiveRecord.
5
+ It is therefore necessary to use the `composite_primary_keys` (https://github.com/composite-primary-keys/composite_primary_keys)
6
+ gem to enable the use of interleaved tables.
7
+
8
+ See https://cloud.google.com/spanner/docs/schema-and-data-model#creating-interleaved-tables for more information
9
+ on interleaved tables if you are not familiar with this concept.
10
+
11
+ ## Creating Interleaved Tables in ActiveRecord
12
+ You can create interleaved tables using migrations in ActiveRecord by using the following Spanner ActiveRecord specific
13
+ methods that are defined on `TableDefinition`:
14
+ * `interleave_in`: Specifies which parent table a child table should be interleaved in and optionally whether
15
+ deletes of a parent record should automatically cascade delete all child records.
16
+ * `parent_key`: Creates a column that is a reference to (a part of) the primary key of the parent table. Each child
17
+ table must include all the primary key columns of the parent table as a `parent_key`.
18
+
19
+ Cloud Spanner requires a child table to include the exact same primary key columns as the parent table in addition to
20
+ the primary key column(s) of the child table. This means that the default `id` primary key column of ActiveRecord is
21
+ not usable in combination with interleaved tables. Instead each primary key column should be prefixed with the table
22
+ name of the table that it references, or use some other unique name.
23
+
24
+ ## Example Data Model
25
+ This example uses the following table schema:
26
+
27
+ ```sql
28
+ CREATE TABLE singers (
29
+ singerid INT64 NOT NULL,
30
+ first_name STRING(MAX),
31
+ last_name STRING(MAX)
32
+ ) PRIMARY KEY (singerid);
33
+
34
+ CREATE TABLE albums (
35
+ albumid INT64 NOT NULL,
36
+ singerid INT64 NOT NULL,
37
+ title STRING(MAX)
38
+ ) PRIMARY KEY (singerid, albumid), INTERLEAVE IN PARENT singers;
39
+
40
+ CREATE TABLE tracks (
41
+ trackid INT64 NOT NULL,
42
+ singerid INT64 NOT NULL,
43
+ albumid INT64 NOT NULL,
44
+ title STRING(MAX),
45
+ duration NUMERIC
46
+ ) PRIMARY KEY (singerid, albumid, trackid), INTERLEAVE IN PARENT albums ON DELETE CASCADE;
47
+ ```
48
+
49
+ This schema can be created in ActiveRecord as follows:
50
+
51
+ ```ruby
52
+ create_table :singers, id: false do |t|
53
+ # Explicitly define the primary key with a custom name to prevent all primary key columns from being named `id`.
54
+ t.primary_key :singerid
55
+ t.string :first_name
56
+ t.string :last_name
57
+ end
58
+
59
+ create_table :albums, id: false do |t|
60
+ # Interleave the `albums` table in the parent table `singers`.
61
+ t.interleave_in :singers
62
+ t.primary_key :albumid
63
+ # `singerid` is defined as a `parent_key` which makes it a part of the primary key in the table definition, but
64
+ # it is not presented to ActiveRecord as part of the primary key, to prevent ActiveRecord from considering this
65
+ # to be an entity with a composite primary key (which is not supported by ActiveRecord).
66
+ t.parent_key :singerid
67
+ t.string :title
68
+ end
69
+
70
+ create_table :tracks, id: false do |t|
71
+ # Interleave the `tracks` table in the parent table `albums` and cascade delete all tracks that belong to an
72
+ # album when an album is deleted.
73
+ t.interleave_in :albums, :cascade
74
+ # Add `trackid` as the primary key in the table definition. Add the other key parts as
75
+ # a `parent_key`.
76
+ t.primary_key :trackid
77
+ # `singerid` and `albumid` form the parent key of `tracks`. These are part of the primary key definition in the
78
+ # database, but are presented as parent keys to ActiveRecord.
79
+ t.parent_key :singerid
80
+ t.parent_key :albumid
81
+ t.string :title
82
+ t.numeric :duration
83
+ end
84
+ ```
85
+
86
+ ## Models for Interleaved Tables
87
+ The model definition for an interleaved table (a child table) must use the `primary_keys=col1, col2, ...`
88
+ function from the `composite_primary_key` gem.
89
+
90
+ An interleaved table parent/child relationship must be modelled as a `belongs_to`/`has_many` association in
91
+ ActiveRecord. As the columns that are used to reference a parent record use a custom column name, it is required to also
92
+ include the custom column name(s) in the `belongs_to` and `has_many` definitions.
93
+
94
+ Instances of these models can be used in the same way as any other association in ActiveRecord, but with a couple of
95
+ inherent limitations:
96
+ * It is not possible to change the parent record of a child record. For instance, changing the singer of an album in the
97
+ above example is impossible, as Cloud Spanner does not allow such an update.
98
+ * It is not possible to de-reference a parent record by setting it to null.
99
+ * It is only possible to delete a parent record with existing child records, if the child records are also deleted. This
100
+ can be done by enabling ON DELETE CASCADE in Cloud Spanner, or by deleting the child records using ActiveRecord.
101
+
102
+ ### Example Models
103
+
104
+ ```ruby
105
+ class Singer < ActiveRecord::Base
106
+ # `albums` is defined as INTERLEAVE IN PARENT `singers`.
107
+ # The primary key of `albums` is (`singerid`, `albumid`).
108
+ has_many :albums, foreign_key: :singerid
109
+
110
+ # `tracks` is defined as INTERLEAVE IN PARENT `albums`.
111
+ # The primary key of `tracks` is (`singerid`, `albumid`, `trackid`).
112
+ # The `singerid` column can be used to associate tracks with a singer without the need to go through albums.
113
+ # Note also that the inclusion of `singerid` as a column in `tracks` is required in order to make `tracks` a child
114
+ # table of `albums` which has primary key (`singerid`, `albumid`).
115
+ has_many :tracks, foreign_key: :singerid
116
+ end
117
+
118
+ class Album < ActiveRecord::Base
119
+ # Use the `composite_primary_key` gem to create a composite primary key definition for the model.
120
+ self.primary_keys = :singerid, :albumid
121
+
122
+ # `albums` is defined as INTERLEAVE IN PARENT `singers`.
123
+ # The primary key of `singers` is `singerid`.
124
+ belongs_to :singer, foreign_key: :singerid
125
+
126
+ # `tracks` is defined as INTERLEAVE IN PARENT `albums`.
127
+ # The primary key of `albums` is (`singerid`, `albumid`).
128
+ has_many :tracks, foreign_key: [:singerid, :albumid]
129
+ end
130
+
131
+ class Track < ActiveRecord::Base
132
+ # Use the `composite_primary_key` gem to create a composite primary key definition for the model.
133
+ self.primary_keys = :singerid, :albumid, :trackid
134
+
135
+ # `tracks` is defined as INTERLEAVE IN PARENT `albums`. The primary key of `albums` is (`singerid`, `albumid`).
136
+ belongs_to :album, foreign_key: [:singerid, :albumid]
137
+
138
+ # `tracks` also has a `singerid` column that can be used to associate a Track with a Singer.
139
+ belongs_to :singer, foreign_key: :singerid
140
+
141
+ # Override the default initialize method to automatically set the singer attribute when an album is given.
142
+ def initialize attributes = nil
143
+ super
144
+ self.singer ||= album&.singer
145
+ end
146
+
147
+ def album=value
148
+ super
149
+ # Ensure the singer of this track is equal to the singer of the album that is set.
150
+ self.singer = value&.singer
151
+ end
152
+ end
153
+ ```
154
+
155
+ ## Running the Sample
156
+
157
+ The sample will automatically start a Spanner Emulator in a docker container and execute the sample
158
+ against that emulator. The emulator will automatically be stopped when the application finishes.
159
+
160
+ Run the application with the command
161
+
162
+ ```bash
163
+ bundle exec rake run
164
+ ```
@@ -0,0 +1,13 @@
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
+ require_relative "../config/environment"
8
+ require "sinatra/activerecord/rake"
9
+
10
+ desc "Sample showing how to work with interleaved tables in ActiveRecord."
11
+ task :run do
12
+ Dir.chdir("..") { sh "bundle exec rake run[interleaved-tables]" }
13
+ end
@@ -0,0 +1,126 @@
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
+ require "io/console"
8
+ require_relative "../config/environment"
9
+ require_relative "models/singer"
10
+ require_relative "models/album"
11
+ require_relative "models/track"
12
+
13
+ class Application
14
+ def self.run
15
+ # List all singers, albums and tracks.
16
+ list_singers_albums_tracks
17
+
18
+ # Create a new album with some tracks.
19
+ create_new_album
20
+
21
+ # Try to update the singer of an album. This is not possible as albums are interleaved in singers.
22
+ update_singer_of_album
23
+
24
+ # Try to delete a singer that has at least one album. This is NOT possible as albums is NOT marked with
25
+ # ON DELETE CASCADE.
26
+ delete_singer_with_albums
27
+
28
+ # Try to delete an album that has at least one track. This IS possible as tracks IS marked with
29
+ # ON DELETE CASCADE.
30
+ delete_album_with_tracks
31
+
32
+ puts ""
33
+ puts "Press any key to end the application"
34
+ STDIN.getch
35
+ end
36
+
37
+ def self.find_singer
38
+ singerid = Singer.all.sample.singerid
39
+
40
+ singer = Singer.find singerid
41
+ puts "Found singer: #{singer.first_name} #{singer.last_name}"
42
+ end
43
+
44
+ def self.find_album
45
+ singer = Singer.all.sample
46
+ albumid = singer.albums.sample.albumid
47
+
48
+ album = Album.find [singer.singerid, albumid]
49
+ puts "Found album: #{album.title}"
50
+ end
51
+
52
+ def self.list_singers_albums_tracks
53
+ puts ""
54
+ puts "Listing all singers with corresponding albums and tracks"
55
+ Singer.all.order("last_name, first_name").each do |singer|
56
+ puts "#{singer.first_name} #{singer.last_name} has #{singer.albums.count} albums:"
57
+ singer.albums.order("title").each do |album|
58
+ puts " #{album.title} has #{album.tracks.count} tracks:"
59
+ album.tracks.each do |track|
60
+ puts " #{track.title}"
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ def self.create_new_album
67
+ # Create a new album with some tracks.
68
+ puts ""
69
+ singer = Singer.all.sample
70
+ puts "Creating a new album for #{singer.first_name} #{singer.last_name}"
71
+ album = singer.albums.build title: "New Title"
72
+ # NOTE: When adding multiple elements to a collection, you *MUST* set the primary key value (i.e. trackid).
73
+ # Otherwise, ActiveRecord thinks that you are adding the same record multiple times and will only add one.
74
+ album.tracks.build title: "Track 1", duration: 3.5, singer: singer, trackid: Track.next_sequence_value
75
+ album.tracks.build title: "Track 2", duration: 3.6, singer: singer, trackid: Track.next_sequence_value
76
+ # This will save the album and corresponding tracks in one transaction.
77
+ album.save!
78
+
79
+ album.reload
80
+ puts "Album #{album.title} has #{album.tracks.count} tracks:"
81
+ album.tracks.order("title").each do |track|
82
+ puts " #{track.title} with duration #{track.duration}"
83
+ end
84
+ end
85
+
86
+ def self.update_singer_of_album
87
+ # It is not possible to change the singer of an album or the album of a track. This is because the associations
88
+ # between these are not traditional foreign keys, but an immutable parent-child relationship.
89
+ album = Album.all.sample
90
+ new_singer = Singer.all.where.not(singerid: album.singer).sample
91
+ # This will fail as we cannot assign a new singer to an album as it is an INTERLEAVE IN PARENT relationship.
92
+ begin
93
+ album.update! singer: new_singer
94
+ raise StandardError, "Unexpected error: Updating the singer of an album should not be possible."
95
+ rescue ActiveRecord::StatementInvalid
96
+ puts ""
97
+ puts "Failed to update the singer of an album. This is expected."
98
+ end
99
+ end
100
+
101
+ def self.delete_singer_with_albums
102
+ # Deleting a singer that has albums is not possible, as the INTERLEAVE IN PARENT of albums is not marked with
103
+ # ON DELETE CASCADE.
104
+ singer = Album.all.sample.singer
105
+ begin
106
+ singer.delete
107
+ raise StandardError, "Unexpected error: Updating the singer of an album should not be possible."
108
+ rescue ActiveRecord::StatementInvalid
109
+ puts ""
110
+ puts "Failed to delete a singer that has #{singer.albums.count} albums. This is expected."
111
+ end
112
+ end
113
+
114
+ def self.delete_album_with_tracks
115
+ # Deleting an album with tracks is supported, as the INTERLEAVE IN PARENT relationship between tracks and albums is
116
+ # marked with ON DELETE CASCADE.
117
+ puts ""
118
+ puts "Total track count: #{Track.count}"
119
+ album = Track.all.sample.album
120
+ puts "Deleting album #{album.title} with #{album.tracks.count} tracks"
121
+ album.delete
122
+ puts "Total track count after deletion: #{Track.count}"
123
+ end
124
+ end
125
+
126
+ Application.run
@@ -0,0 +1,8 @@
1
+ development:
2
+ adapter: spanner
3
+ emulator_host: localhost:9010
4
+ project: test-project
5
+ instance: test-instance
6
+ database: testdb
7
+ pool: 5
8
+ timeout: 5000
@@ -0,0 +1,44 @@
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
+ class CreateTables < ActiveRecord::Migration[6.0]
8
+ def change
9
+ # Execute the entire migration as one DDL batch.
10
+ connection.ddl_batch do
11
+ create_table :singers, id: false do |t|
12
+ # Explicitly define the primary key with a custom name to prevent all primary key columns from being named `id`.
13
+ t.primary_key :singerid
14
+ t.string :first_name
15
+ t.string :last_name
16
+ end
17
+
18
+ create_table :albums, id: false do |t|
19
+ # Interleave the `albums` table in the parent table `singers`.
20
+ t.interleave_in :singers
21
+ t.primary_key :albumid
22
+ # `singerid` is defined as a `parent_key` which makes it a part of the primary key in the table definition, but
23
+ # it is not presented to ActiveRecord as part of the primary key, to prevent ActiveRecord from considering this
24
+ # to be an entity with a composite primary key (which is not supported by ActiveRecord).
25
+ t.parent_key :singerid
26
+ t.string :title
27
+ end
28
+
29
+ create_table :tracks, id: false do |t|
30
+ # Interleave the `tracks` table in the parent table `albums` and cascade delete all tracks that belong to an
31
+ # album when an album is deleted.
32
+ t.interleave_in :albums, :cascade
33
+ # `trackid` is considered the only primary key column by ActiveRecord.
34
+ t.primary_key :trackid
35
+ # `singerid` and `albumid` form the parent key of `tracks`. These are part of the primary key definition in the
36
+ # database, but are presented as parent keys to ActiveRecord.
37
+ t.parent_key :singerid
38
+ t.parent_key :albumid
39
+ t.string :title
40
+ t.numeric :duration
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,32 @@
1
+ # This file is auto-generated from the current state of the database. Instead
2
+ # of editing this file, please use the migrations feature of Active Record to
3
+ # incrementally modify your database, and then regenerate this schema definition.
4
+ #
5
+ # This file is the source Rails uses to define your schema when running `bin/rails
6
+ # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7
+ # be faster and is potentially less error prone than running all of your
8
+ # migrations from scratch. Old migrations may fail to apply correctly if those
9
+ # migrations use external dependencies or application code.
10
+ #
11
+ # It's strongly recommended that you check this file into your version control system.
12
+
13
+ ActiveRecord::Schema.define(version: 1) do
14
+
15
+ create_table "albums", primary_key: "albumid", id: { limit: 8 }, force: :cascade do |t|
16
+ t.integer "singerid", limit: 8, null: false
17
+ t.string "title"
18
+ end
19
+
20
+ create_table "singers", primary_key: "singerid", id: { limit: 8 }, force: :cascade do |t|
21
+ t.string "first_name"
22
+ t.string "last_name"
23
+ end
24
+
25
+ create_table "tracks", primary_key: "trackid", id: { limit: 8 }, force: :cascade do |t|
26
+ t.integer "singerid", limit: 8, null: false
27
+ t.integer "albumid", limit: 8, null: false
28
+ t.string "title"
29
+ t.decimal "duration"
30
+ end
31
+
32
+ end
@@ -0,0 +1,40 @@
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
+ require_relative "../../config/environment.rb"
8
+ require_relative "../models/singer"
9
+ require_relative "../models/album"
10
+ require_relative "../models/track"
11
+
12
+ first_names = %w[Pete Alice John Ethel Trudy Naomi Wendy Ruben Thomas Elly]
13
+ last_names = %w[Wendelson Allison Peterson Johnson Henderson Ericsson Aronson Tennet Courtou]
14
+
15
+ adjectives = %w[daily happy blue generous cooked bad open]
16
+ nouns = %w[windows potatoes bank street tree glass bottle]
17
+
18
+ verbs = %w[operate waste package chew yield express polish stress slip want cough campaign cultivate report park refer]
19
+ adverbs = %w[directly right hopefully personally economically privately supposedly consequently fully later urgently]
20
+
21
+ durations = [3.14, 5.4, 3.3, 4.1, 5.0, 3.2, 3.0, 3.5, 4.0, 4.5, 5.5, 6.0]
22
+
23
+ # This ensures all the records are inserted using one read/write transaction that will use mutations instead of DML.
24
+ ActiveRecord::Base.transaction isolation: :buffered_mutations do
25
+ singers = []
26
+ 5.times do
27
+ singers << Singer.create(first_name: first_names.sample, last_name: last_names.sample)
28
+ end
29
+
30
+ albums = []
31
+ 20.times do
32
+ singer = singers.sample
33
+ albums << Album.create(title: "#{adjectives.sample} #{nouns.sample}", singer: singer)
34
+ end
35
+
36
+ 200.times do
37
+ album = albums.sample
38
+ Track.create title: "#{verbs.sample} #{adverbs.sample}", duration: durations.sample, album: album
39
+ end
40
+ end
@@ -0,0 +1,20 @@
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
+ require "composite_primary_keys"
8
+
9
+ class Album < ActiveRecord::Base
10
+ # Use the `composite_primary_key` gem to create a composite primary key definition for the model.
11
+ self.primary_keys = :singerid, :albumid
12
+
13
+ # `albums` is defined as INTERLEAVE IN PARENT `singers`.
14
+ # The primary key of `singers` is `singerid`.
15
+ belongs_to :singer, foreign_key: :singerid
16
+
17
+ # `tracks` is defined as INTERLEAVE IN PARENT `albums`.
18
+ # The primary key of `albums` is (`singerid`, `albumid`).
19
+ has_many :tracks, foreign_key: [:singerid, :albumid]
20
+ end
@@ -0,0 +1,18 @@
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
+ class Singer < ActiveRecord::Base
8
+ # `albums` is defined as INTERLEAVE IN PARENT `singers`.
9
+ # The primary key of `albums` is (`singerid`, `albumid`).
10
+ has_many :albums, foreign_key: :singerid
11
+
12
+ # `tracks` is defined as INTERLEAVE IN PARENT `albums`.
13
+ # The primary key of `tracks` is [`singerid`, `albumid`, `trackid`].
14
+ # The `singerid` column can be used to associate tracks with a singer without the need to go through albums.
15
+ # Note also that the inclusion of `singerid` as a column in `tracks` is required in order to make `tracks` a child
16
+ # table of `albums` which has primary key (`singerid`, `albumid`).
17
+ has_many :tracks, foreign_key: :singerid
18
+ end
@@ -0,0 +1,28 @@
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
+ class Track < ActiveRecord::Base
8
+ # Use the `composite_primary_key` gem to create a composite primary key definition for the model.
9
+ self.primary_keys = :singerid, :albumid, :trackid
10
+
11
+ # `tracks` is defined as INTERLEAVE IN PARENT `albums`. The primary key of `albums` is ()`singerid`, `albumid`).
12
+ belongs_to :album, foreign_key: [:singerid, :albumid]
13
+
14
+ # `tracks` also has a `singerid` column should be used to associate a Track with a Singer.
15
+ belongs_to :singer, foreign_key: :singerid
16
+
17
+ # Override the default initialize method to automatically set the singer attribute when an album is given.
18
+ def initialize attributes = nil
19
+ super
20
+ self.singer ||= album&.singer
21
+ end
22
+
23
+ def album=value
24
+ super
25
+ # Ensure the singer of this track is equal to the singer of the album that is set.
26
+ self.singer = value&.singer
27
+ end
28
+ end
@@ -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