activerecord-spanner-adapter 1.5.1 → 1.6.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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/acceptance-tests-on-emulator.yaml +1 -1
  3. data/.github/workflows/acceptance-tests-on-production.yaml +5 -3
  4. data/.github/workflows/ci.yaml +1 -1
  5. data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +1 -1
  6. data/.github/workflows/nightly-acceptance-tests-on-production.yaml +5 -3
  7. data/.github/workflows/nightly-unit-tests.yaml +1 -1
  8. data/.github/workflows/release-please-label.yml +1 -1
  9. data/.release-please-manifest.json +1 -1
  10. data/CHANGELOG.md +8 -0
  11. data/Gemfile +5 -2
  12. data/README.md +10 -10
  13. data/acceptance/cases/interleaved_associations/has_many_associations_using_interleaved_test.rb +6 -0
  14. data/acceptance/cases/migration/change_schema_test.rb +19 -3
  15. data/acceptance/cases/migration/schema_dumper_test.rb +10 -1
  16. data/acceptance/cases/models/interleave_test.rb +6 -0
  17. data/acceptance/cases/tasks/database_tasks_test.rb +340 -2
  18. data/acceptance/cases/transactions/optimistic_locking_test.rb +6 -0
  19. data/acceptance/cases/transactions/read_write_transactions_test.rb +24 -0
  20. data/acceptance/models/table_with_sequence.rb +10 -0
  21. data/acceptance/schema/schema.rb +65 -19
  22. data/acceptance/test_helper.rb +1 -1
  23. data/activerecord-spanner-adapter.gemspec +1 -1
  24. data/examples/snippets/bit-reversed-sequence/README.md +103 -0
  25. data/examples/snippets/bit-reversed-sequence/Rakefile +13 -0
  26. data/examples/snippets/bit-reversed-sequence/application.rb +68 -0
  27. data/examples/snippets/bit-reversed-sequence/config/database.yml +8 -0
  28. data/examples/snippets/bit-reversed-sequence/db/migrate/01_create_tables.rb +33 -0
  29. data/examples/snippets/bit-reversed-sequence/db/schema.rb +31 -0
  30. data/examples/snippets/bit-reversed-sequence/db/seeds.rb +31 -0
  31. data/examples/snippets/bit-reversed-sequence/models/album.rb +11 -0
  32. data/examples/snippets/bit-reversed-sequence/models/singer.rb +15 -0
  33. data/examples/snippets/interleaved-tables/README.md +44 -53
  34. data/examples/snippets/interleaved-tables/Rakefile +2 -2
  35. data/examples/snippets/interleaved-tables/application.rb +2 -2
  36. data/examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb +12 -18
  37. data/examples/snippets/interleaved-tables/db/schema.rb +9 -7
  38. data/examples/snippets/interleaved-tables/db/seeds.rb +1 -1
  39. data/examples/snippets/interleaved-tables/models/album.rb +3 -7
  40. data/examples/snippets/interleaved-tables/models/singer.rb +1 -1
  41. data/examples/snippets/interleaved-tables/models/track.rb +6 -7
  42. data/examples/snippets/interleaved-tables-before-7.1/README.md +167 -0
  43. data/examples/snippets/interleaved-tables-before-7.1/Rakefile +13 -0
  44. data/examples/snippets/interleaved-tables-before-7.1/application.rb +126 -0
  45. data/examples/snippets/interleaved-tables-before-7.1/config/database.yml +8 -0
  46. data/examples/snippets/interleaved-tables-before-7.1/db/migrate/01_create_tables.rb +44 -0
  47. data/examples/snippets/interleaved-tables-before-7.1/db/schema.rb +37 -0
  48. data/examples/snippets/interleaved-tables-before-7.1/db/seeds.rb +40 -0
  49. data/examples/snippets/interleaved-tables-before-7.1/models/album.rb +20 -0
  50. data/examples/snippets/interleaved-tables-before-7.1/models/singer.rb +18 -0
  51. data/examples/snippets/interleaved-tables-before-7.1/models/track.rb +28 -0
  52. data/examples/snippets/query-logs/README.md +43 -0
  53. data/examples/snippets/query-logs/Rakefile +13 -0
  54. data/examples/snippets/query-logs/application.rb +63 -0
  55. data/examples/snippets/query-logs/config/database.yml +8 -0
  56. data/examples/snippets/query-logs/db/migrate/01_create_tables.rb +21 -0
  57. data/examples/snippets/query-logs/db/schema.rb +31 -0
  58. data/examples/snippets/query-logs/db/seeds.rb +24 -0
  59. data/examples/snippets/query-logs/models/album.rb +9 -0
  60. data/examples/snippets/query-logs/models/singer.rb +9 -0
  61. data/lib/active_record/connection_adapters/spanner/column.rb +13 -0
  62. data/lib/active_record/connection_adapters/spanner/database_statements.rb +144 -35
  63. data/lib/active_record/connection_adapters/spanner/schema_cache.rb +3 -21
  64. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +11 -0
  65. data/lib/active_record/connection_adapters/spanner/schema_definitions.rb +4 -0
  66. data/lib/active_record/connection_adapters/spanner/schema_statements.rb +3 -2
  67. data/lib/active_record/connection_adapters/spanner_adapter.rb +28 -9
  68. data/lib/activerecord_spanner_adapter/base.rb +56 -19
  69. data/lib/activerecord_spanner_adapter/information_schema.rb +33 -24
  70. data/lib/activerecord_spanner_adapter/primary_key.rb +1 -1
  71. data/lib/activerecord_spanner_adapter/table/column.rb +4 -9
  72. data/lib/activerecord_spanner_adapter/version.rb +1 -1
  73. data/lib/arel/visitors/spanner.rb +3 -1
  74. metadata +33 -4
@@ -1,4 +1,4 @@
1
- # Copyright 2022 Google LLC
1
+ # Copyright 2023 Google LLC
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -7,7 +7,7 @@
7
7
  require_relative "../config/environment"
8
8
  require "sinatra/activerecord/rake"
9
9
 
10
- desc "Sample showing how to work with interleaved tables in ActiveRecord."
10
+ desc "Sample showing how to work with interleaved tables in ActiveRecord 7.1 and later."
11
11
  task :run do
12
12
  Dir.chdir("..") { sh "bundle exec rake run[interleaved-tables]" }
13
13
  end
@@ -1,4 +1,4 @@
1
- # Copyright 2022 Google LLC
1
+ # Copyright 2023 Google LLC
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -49,7 +49,7 @@ class Application
49
49
  puts "Found album: #{album.title}"
50
50
  end
51
51
 
52
- def self.list_singers_albums_tracks
52
+ def self.list_singers_albums
53
53
  puts ""
54
54
  puts "Listing all singers with corresponding albums and tracks"
55
55
  Singer.all.order("last_name, first_name").each do |singer|
@@ -1,41 +1,35 @@
1
- # Copyright 2022 Google LLC
1
+ # Copyright 2023 Google LLC
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
5
5
  # https://opensource.org/licenses/MIT.
6
6
 
7
- class CreateTables < ActiveRecord::Migration[6.0]
7
+ class CreateTables < ActiveRecord::Migration[7.1]
8
8
  def change
9
9
  # Execute the entire migration as one DDL batch.
10
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
11
+ # Explicitly define the primary key.
12
+ create_table :singers, id: false, primary_key: :singerid do |t|
13
+ t.integer :singerid
14
14
  t.string :first_name
15
15
  t.string :last_name
16
16
  end
17
17
 
18
- create_table :albums, id: false do |t|
18
+ create_table :albums, primary_key: [:singerid, :albumid], id: false do |t|
19
19
  # Interleave the `albums` table in the parent table `singers`.
20
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
21
+ t.integer :singerid
22
+ t.integer :albumid
26
23
  t.string :title
27
24
  end
28
25
 
29
- create_table :tracks, id: false do |t|
26
+ create_table :tracks, primary_key: [:singerid, :albumid, :trackid], id: false do |t|
30
27
  # Interleave the `tracks` table in the parent table `albums` and cascade delete all tracks that belong to an
31
28
  # album when an album is deleted.
32
29
  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
30
+ t.integer :singerid
31
+ t.integer :albumid
32
+ t.integer :trackid
39
33
  t.string :title
40
34
  t.numeric :duration
41
35
  end
@@ -10,22 +10,24 @@
10
10
  #
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
- ActiveRecord::Schema.define(version: 1) do
13
+ ActiveRecord::Schema[7.1].define(version: 1) do
14
14
  connection.start_batch_ddl
15
15
 
16
- create_table "albums", primary_key: "albumid", id: { limit: 8 }, force: :cascade do |t|
17
- t.integer "singerid", limit: 8, null: false
16
+ create_table "albums", primary_key: ["singerid", "albumid"], force: :cascade do |t|
17
+ t.integer "singerid", limit: 8
18
+ t.integer "albumid", limit: 8
18
19
  t.string "title"
19
20
  end
20
21
 
21
- create_table "singers", primary_key: "singerid", id: { limit: 8 }, force: :cascade do |t|
22
+ create_table "singers", primary_key: "singerid", force: :cascade do |t|
22
23
  t.string "first_name"
23
24
  t.string "last_name"
24
25
  end
25
26
 
26
- create_table "tracks", primary_key: "trackid", id: { limit: 8 }, force: :cascade do |t|
27
- t.integer "singerid", limit: 8, null: false
28
- t.integer "albumid", limit: 8, null: false
27
+ create_table "tracks", primary_key: ["singerid", "albumid", "trackid"], force: :cascade do |t|
28
+ t.integer "singerid", limit: 8
29
+ t.integer "albumid", limit: 8
30
+ t.integer "trackid", limit: 8
29
31
  t.string "title"
30
32
  t.decimal "duration"
31
33
  end
@@ -1,4 +1,4 @@
1
- # Copyright 2022 Google LLC
1
+ # Copyright 2023 Google LLC
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,20 +1,16 @@
1
- # Copyright 2022 Google LLC
1
+ # Copyright 2023 Google LLC
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
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
-
9
7
  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
8
  # `albums` is defined as INTERLEAVE IN PARENT `singers`.
14
9
  # The primary key of `singers` is `singerid`.
15
10
  belongs_to :singer, foreign_key: :singerid
16
11
 
17
12
  # `tracks` is defined as INTERLEAVE IN PARENT `albums`.
18
13
  # The primary key of `albums` is (`singerid`, `albumid`).
19
- has_many :tracks, foreign_key: [:singerid, :albumid]
14
+ # Rails 7.1 requires using query_constraints to define a composite foreign key.
15
+ has_many :tracks, query_constraints: [:singerid, :albumid]
20
16
  end
@@ -1,4 +1,4 @@
1
- # Copyright 2022 Google LLC
1
+ # Copyright 2023 Google LLC
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -1,17 +1,16 @@
1
- # Copyright 2022 Google LLC
1
+ # Copyright 2023 Google LLC
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
5
5
  # https://opensource.org/licenses/MIT.
6
6
 
7
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
8
+ # `tracks` is defined as INTERLEAVE IN PARENT `albums`.
9
+ # The primary key of `albums` is (`singerid`, `albumid`).
10
+ # Rails 7.1 requires a composite primary key in a belongs_to relationship to be specified as query_constraints.
11
+ belongs_to :album, query_constraints: [:singerid, :albumid]
10
12
 
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.
13
+ # `tracks` also has a `singerid` column that can be used to associate a Track with a Singer.
15
14
  belongs_to :singer, foreign_key: :singerid
16
15
 
17
16
  # Override the default initialize method to automatically set the singer attribute when an album is given.
@@ -0,0 +1,167 @@
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
+ ```
@@ -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-before-7.1]" }
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
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,37 @@
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
+ connection.start_batch_ddl
15
+
16
+ create_table "albums", primary_key: "albumid", force: :cascade do |t|
17
+ t.integer "singerid", limit: 8, null: false
18
+ t.string "title"
19
+ end
20
+
21
+ create_table "singers", primary_key: "singerid", force: :cascade do |t|
22
+ t.string "first_name"
23
+ t.string "last_name"
24
+ end
25
+
26
+ create_table "tracks", primary_key: "trackid", force: :cascade do |t|
27
+ t.integer "singerid", limit: 8, null: false
28
+ t.integer "albumid", limit: 8, null: false
29
+ t.string "title"
30
+ t.decimal "duration"
31
+ end
32
+
33
+ connection.run_batch
34
+ rescue
35
+ abort_batch
36
+ raise
37
+ 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