activerecord-spanner-adapter 1.5.1 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -11,6 +11,7 @@ require "models/author"
11
11
  require "models/post"
12
12
  require "models/comment"
13
13
  require "models/organization"
14
+ require "models/table_with_sequence"
14
15
 
15
16
  module ActiveRecord
16
17
  module Transactions
@@ -243,6 +244,29 @@ module ActiveRecord
243
244
 
244
245
  assert_equal 0, Comment.count
245
246
  end
247
+
248
+ def test_create_record_with_sequence
249
+ record = TableWithSequence.create name: "Some name", age: 40
250
+ assert record.id, "ID should be generated and returned by the database"
251
+ assert record.id > 0, "ID should be positive" unless ENV["SPANNER_EMULATOR_HOST"]
252
+ end
253
+
254
+ def test_create_record_with_sequence_in_transaction
255
+ record = TableWithSequence.transaction do
256
+ TableWithSequence.create name: "Some name", age: 40
257
+ end
258
+ assert record.id, "ID should be generated and returned by the database"
259
+ assert record.id > 0, "ID should be positive" unless ENV["SPANNER_EMULATOR_HOST"]
260
+ end
261
+
262
+ def test_create_record_with_sequence_using_mutations
263
+ err = assert_raises ActiveRecord::StatementInvalid do
264
+ TableWithSequence.transaction isolation: :buffered_mutations do
265
+ TableWithSequence.create name: "Foo", age: 50
266
+ end
267
+ end
268
+ assert_equal "Mutations cannot be used to create records that use a sequence to generate the primary key. TableWithSequence uses test_sequence.", err.message
269
+ end
246
270
  end
247
271
  end
248
272
  end
@@ -0,0 +1,10 @@
1
+ # Copyright 2023 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 TableWithSequence < ActiveRecord::Base
8
+ self.table_name = :table_with_sequence
9
+ self.sequence_name = :test_sequence
10
+ end
@@ -6,6 +6,11 @@
6
6
 
7
7
  # frozen_string_literal: true
8
8
 
9
+
10
+ def is_7_1_or_higher?
11
+ ActiveRecord::gem_version >= Gem::Version.create('7.1.0')
12
+ end
13
+
9
14
  def create_tables_in_test_schema
10
15
  ActiveRecord::Schema.define(version: 1) do
11
16
  ActiveRecord::Base.connection.ddl_batch do
@@ -122,29 +127,70 @@ def create_tables_in_test_schema
122
127
  t.virtual :full_name, type: :string, as: "COALESCE(first_name || ' ', '') || last_name", stored: true
123
128
  end
124
129
 
125
- create_table :albums, id: false do |t|
126
- t.interleave_in :singers
127
- t.primary_key :albumid
128
- # `singerid` is part of the primary key in the table definition, but it is not visible to ActiveRecord as part of
129
- # the primary key, to prevent ActiveRecord from considering this to be an entity with a composite primary key.
130
- t.parent_key :singerid
131
- t.string :title
132
- t.integer :lock_version
133
- end
134
-
135
- create_table :tracks, id: false do |t|
136
- # `:cascade` causes all tracks that belong to an album to automatically be deleted when an album is deleted.
137
- t.interleave_in :albums, :cascade
138
- t.primary_key :trackid
139
- t.parent_key :singerid
140
- t.parent_key :albumid
141
- t.string :title
142
- t.numeric :duration
143
- t.integer :lock_version
130
+ if is_7_1_or_higher?
131
+ create_table :albums, primary_key: [:singerid, :albumid] do |t|
132
+ t.interleave_in :singers
133
+ t.integer :singerid, null: false
134
+ t.integer :albumid, null: false
135
+ t.string :title
136
+ t.integer :lock_version
137
+ end
138
+ else
139
+ create_table :albums, id: false do |t|
140
+ t.interleave_in :singers
141
+ t.primary_key :albumid
142
+ # `singerid` is part of the primary key in the table definition, but it is not visible to ActiveRecord as part of
143
+ # the primary key, to prevent ActiveRecord from considering this to be an entity with a composite primary key.
144
+ t.parent_key :singerid
145
+ t.string :title
146
+ t.integer :lock_version
147
+ end
148
+ end
149
+
150
+ if is_7_1_or_higher?
151
+ create_table :tracks, primary_key: [:singerid, :albumid, :trackid] do |t|
152
+ # `:cascade` causes all tracks that belong to an album to automatically be deleted when an album is deleted.
153
+ t.interleave_in :albums, :cascade
154
+ t.integer :singerid, null: false
155
+ t.integer :albumid, null: false
156
+ t.integer :trackid, null: false
157
+ t.string :title
158
+ t.numeric :duration
159
+ t.integer :lock_version
160
+ end
161
+ else
162
+ create_table :tracks, id: false do |t|
163
+ # `:cascade` causes all tracks that belong to an album to automatically be deleted when an album is deleted.
164
+ t.interleave_in :albums, :cascade
165
+ t.primary_key :trackid
166
+ t.parent_key :singerid
167
+ t.parent_key :albumid
168
+ t.string :title
169
+ t.numeric :duration
170
+ t.integer :lock_version
171
+ end
144
172
  end
145
173
 
146
174
  add_index :tracks, [:singerid, :albumid, :title], interleave_in: :albums, null_filtered: true, unique: false
147
175
 
176
+ if ENV["SPANNER_EMULATOR_HOST"]
177
+ create_table :table_with_sequence, id: false do |t|
178
+ # The emulator does not yet support bit-reversed sequences, so we emulate a sequence value
179
+ # by hashing a UUID instead.
180
+ t.integer :id, primary_key: true, null: false, default: -> { "FARM_FINGERPRINT(GENERATE_UUID())" }
181
+ t.string :name, null: false
182
+ t.integer :age, null: false
183
+ end
184
+ else
185
+ connection.execute "create sequence test_sequence OPTIONS (sequence_kind = 'bit_reversed_positive')"
186
+
187
+ create_table :table_with_sequence, id: false do |t|
188
+ t.integer :id, primary_key: true, null: false, default: -> { "GET_NEXT_SEQUENCE_VALUE(SEQUENCE test_sequence)" }
189
+ t.string :name, null: false
190
+ t.integer :age, null: false
191
+ end
192
+ end
193
+
148
194
  end
149
195
  end
150
196
  end
@@ -15,7 +15,7 @@ require "active_support/testing/stream"
15
15
  require "activerecord-spanner-adapter"
16
16
  require "active_record/connection_adapters/spanner_adapter"
17
17
  require "securerandom"
18
- require "composite_primary_keys"
18
+ require "composite_primary_keys" if ActiveRecord::gem_version < Gem::Version.create('7.1.0')
19
19
 
20
20
  # rubocop:disable Style/GlobalVars
21
21
 
@@ -25,7 +25,7 @@ Gem::Specification.new do |spec|
25
25
  spec.required_ruby_version = ">= 2.7"
26
26
 
27
27
  spec.add_dependency "google-cloud-spanner", "~> 2.18"
28
- spec.add_runtime_dependency "activerecord", [">= 6.0.0", "< 7.1"]
28
+ spec.add_runtime_dependency "activerecord", [">= 6.0.0", "< 7.2"]
29
29
 
30
30
  spec.add_development_dependency "autotest-suffix", "~> 1.1"
31
31
  spec.add_development_dependency "bundler", "~> 2.0"
@@ -0,0 +1,103 @@
1
+ # Sample - Bit-reversed Sequence
2
+
3
+ This example shows how to use a bit-reversed sequence to generate the primary key of a model.
4
+
5
+ See https://cloud.google.com/spanner/docs/primary-key-default-value#bit-reversed-sequence for more information
6
+ about bit-reversed sequences in Cloud Spanner.
7
+
8
+ ## Creating Tables with Bit-Reversed Sequences in ActiveRecord
9
+ You can create bit-reversed sequences using migrations in ActiveRecord by executing a SQL statement using the underlying
10
+ connection.
11
+
12
+ ```ruby
13
+ connection.execute "create sequence singer_sequence OPTIONS (sequence_kind = 'bit_reversed_positive')"
14
+ ```
15
+
16
+ The sequence can be used to generate a default value for the primary key column of a table:
17
+
18
+ ```ruby
19
+ create_table :singers, id: false do |t|
20
+ t.integer :singerid, primary_key: true, null: false, default: -> { "GET_NEXT_SEQUENCE_VALUE(SEQUENCE singer_sequence)" }
21
+ t.string :first_name
22
+ t.string :last_name
23
+ end
24
+ ```
25
+
26
+ ## Example Data Model
27
+ This example uses the following table schema:
28
+
29
+ ```sql
30
+ CREATE SEQUENCE singer_sequence (OPTIONS sequence_kind="bit_reversed_positive")
31
+
32
+ CREATE TABLE singers (
33
+ singerid INT64 NOT NULL DEFAULT GET_NEXT_SEQUENCE_VALUE(SEQUENCE singer_sequence),
34
+ first_name STRING(MAX),
35
+ last_name STRING(MAX)
36
+ ) PRIMARY KEY (singerid);
37
+
38
+ CREATE TABLE albums (
39
+ singerid INT64 NOT NULL,
40
+ albumid INT64 NOT NULL,
41
+ title STRING(MAX)
42
+ ) PRIMARY KEY (singerid, albumid), INTERLEAVE IN PARENT singers;
43
+ ```
44
+
45
+ This schema can be created in ActiveRecord 7.1 and later as follows:
46
+
47
+ ```ruby
48
+ # Execute the entire migration as one DDL batch.
49
+ connection.ddl_batch do
50
+ connection.execute "create sequence singer_sequence OPTIONS (sequence_kind = 'bit_reversed_positive')"
51
+
52
+ # Explicitly define the primary key.
53
+ create_table :singers, id: false, primary_key: :singerid do |t|
54
+ t.integer :singerid, primary_key: true, null: false, default: -> { "GET_NEXT_SEQUENCE_VALUE(SEQUENCE singer_sequence)" }
55
+ t.string :first_name
56
+ t.string :last_name
57
+ end
58
+
59
+ create_table :albums, primary_key: [:singerid, :albumid], id: false do |t|
60
+ # Interleave the `albums` table in the parent table `singers`.
61
+ t.interleave_in :singers
62
+ t.integer :singerid
63
+ t.integer :albumid
64
+ t.string :title
65
+ end
66
+ end
67
+ ```
68
+
69
+ ## Models for Tables with a Sequence
70
+ The models for tables that use a sequence to generate the primary key must include the sequence name. This instructs
71
+ the Cloud Spanner ActiveRecord provider to let the database generate the primary key value, instead of generating one
72
+ in memory.
73
+
74
+ ### Example Models
75
+
76
+ ```ruby
77
+ class Singer < ActiveRecord::Base
78
+ self.sequence_name = :singer_sequence
79
+
80
+ # `albums` is defined as INTERLEAVE IN PARENT `singers`.
81
+ # The primary key of `albums` is (`singerid`, `albumid`).
82
+ has_many :albums, foreign_key: :singerid
83
+ end
84
+
85
+ class Album < ActiveRecord::Base
86
+ # `albums` is defined as INTERLEAVE IN PARENT `singers`.
87
+ # The primary key of `singers` is `singerid`.
88
+ belongs_to :singer, foreign_key: :singerid
89
+ end
90
+ ```
91
+
92
+ ## Running the Sample
93
+
94
+ The sample will automatically start a Spanner Emulator in a docker container and execute the sample
95
+ against that emulator. The emulator will automatically be stopped when the application finishes.
96
+
97
+ Run the application with the following commands:
98
+
99
+ ```bash
100
+ export AR_VERSION="~> 7.1.2"
101
+ bundle install
102
+ bundle exec rake run
103
+ ```
@@ -0,0 +1,13 @@
1
+ # Copyright 2023 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 bit-reversed sequences."
11
+ task :run do
12
+ Dir.chdir("..") { sh "bundle exec rake run[bit-reversed-sequence]" }
13
+ end
@@ -0,0 +1,68 @@
1
+ # Copyright 2023 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
+
12
+ class Application
13
+ def self.run
14
+ # Create a new singer.
15
+ singer = create_new_singer
16
+
17
+ # Create a new album.
18
+ album = create_new_album singer
19
+
20
+ # Verify that the album exists.
21
+ find_album singer.singerid, album.albumid
22
+
23
+ # List all singers, albums and tracks.
24
+ list_singers_albums
25
+
26
+ puts ""
27
+ puts "Press any key to end the application"
28
+ STDIN.getch
29
+ end
30
+
31
+ def self.find_album singerid, albumid
32
+ album = Album.find [singerid, albumid]
33
+ puts "Found album: #{album.title}"
34
+ end
35
+
36
+ def self.list_singers_albums
37
+ puts ""
38
+ puts "Listing all singers with corresponding albums"
39
+ Singer.all.order("last_name, first_name").each do |singer|
40
+ puts "#{singer.first_name} #{singer.last_name} has #{singer.albums.count} albums:"
41
+ singer.albums.order("title").each do |album|
42
+ puts " #{album.title}"
43
+ end
44
+ end
45
+ end
46
+
47
+ def self.create_new_singer
48
+ # Create a new singer. The singerid is generated by the bit-reversed sequence in the database and returned.
49
+ puts ""
50
+ singer = Singer.create first_name: "Melissa", last_name: "Garcia"
51
+ puts "Created a new singer '#{singer.first_name} #{singer.last_name}' with id #{singer.singerid}"
52
+
53
+ singer
54
+ end
55
+
56
+ def self.create_new_album singer
57
+ # Create a new album.
58
+ puts ""
59
+ puts "Creating a new album for #{singer.first_name} #{singer.last_name}"
60
+ # The albumid is not generated by a sequence in the database.
61
+ album = singer.albums.build albumid: 1, title: "New Title"
62
+ album.save!
63
+
64
+ album
65
+ end
66
+ end
67
+
68
+ 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,33 @@
1
+ # Copyright 2023 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[7.1]
8
+ def change
9
+ # Execute the entire migration as one DDL batch.
10
+ connection.ddl_batch do
11
+ # TODO: Uncomment when bit-reversed sequences are supported in the emulator.
12
+ # connection.execute "create sequence singer_sequence OPTIONS (sequence_kind = 'bit_reversed_positive')"
13
+
14
+ # Explicitly define the primary key.
15
+ create_table :singers, id: false, primary_key: :singerid do |t|
16
+ # TODO: Uncomment when bit-reversed sequences are supported in the emulator.
17
+ # t.integer :singerid, primary_key: true, null: false,
18
+ # default: -> { "GET_NEXT_SEQUENCE_VALUE(SEQUENCE singer_sequence)" }
19
+ t.integer :singerid, primary_key: true, null: false, default: -> { "FARM_FINGERPRINT(GENERATE_UUID())" }
20
+ t.string :first_name
21
+ t.string :last_name
22
+ end
23
+
24
+ create_table :albums, primary_key: [:singerid, :albumid], id: false do |t|
25
+ # Interleave the `albums` table in the parent table `singers`.
26
+ t.interleave_in :singers
27
+ t.integer :singerid, null: false
28
+ t.integer :albumid, null: false
29
+ t.string :title
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,31 @@
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[7.1].define(version: 1) do
14
+ connection.start_batch_ddl
15
+
16
+ create_table "albums", primary_key: ["singerid", "albumid"], force: :cascade do |t|
17
+ t.integer "singerid", limit: 8, null: false
18
+ t.integer "albumid", limit: 8, null: false
19
+ t.string "title"
20
+ end
21
+
22
+ create_table "singers", primary_key: "singerid", default: -> { "FARM_FINGERPRINT(GENERATE_UUID())" }, force: :cascade do |t|
23
+ t.string "first_name"
24
+ t.string "last_name"
25
+ end
26
+
27
+ connection.run_batch
28
+ rescue
29
+ abort_batch
30
+ raise
31
+ end
@@ -0,0 +1,31 @@
1
+ # Copyright 2023 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
+
11
+ first_names = %w[Pete Alice John Ethel Trudy Naomi Wendy Ruben Thomas Elly]
12
+ last_names = %w[Wendelson Allison Peterson Johnson Henderson Ericsson Aronson Tennet Courtou]
13
+
14
+ adjectives = %w[daily happy blue generous cooked bad open]
15
+ nouns = %w[windows potatoes bank street tree glass bottle]
16
+
17
+ # Note: We do not use mutations to insert these rows, because letting the database generate the primary key means that
18
+ # we rely on a `THEN RETURN id` clause in the insert statement. This is only supported for DML statements, and not for
19
+ # mutations.
20
+ ActiveRecord::Base.transaction do
21
+ singers = []
22
+ 5.times do
23
+ singers << Singer.create(first_name: first_names.sample, last_name: last_names.sample)
24
+ end
25
+
26
+ albums = []
27
+ 20.times do
28
+ singer = singers.sample
29
+ albums << Album.create(title: "#{adjectives.sample} #{nouns.sample}", singer: singer)
30
+ end
31
+ end
@@ -0,0 +1,11 @@
1
+ # Copyright 2023 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
+ # `albums` is defined as INTERLEAVE IN PARENT `singers`.
9
+ # The primary key of `singers` is `singerid`.
10
+ belongs_to :singer, foreign_key: :singerid
11
+ end
@@ -0,0 +1,15 @@
1
+ # Copyright 2023 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
+ # Set the sequence name so the ActiveRecord provider knows that it should let the database generate the primary key
9
+ # value and return it using a `THEN RETURN id` clause.
10
+ self.sequence_name = :singer_sequence
11
+
12
+ # `albums` is defined as INTERLEAVE IN PARENT `singers`.
13
+ # The primary key of `albums` is (`singerid`, `albumid`).
14
+ has_many :albums, foreign_key: :singerid
15
+ end
@@ -1,9 +1,10 @@
1
1
  # Sample - Interleaved Tables
2
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.
3
+ __NOTE__: This example requires Rails 7.1 or later.
4
+
5
+ This example shows how to use interleaved tables with the Spanner ActiveRecord adapter in Rails 7.1 and later.
6
+ Interleaved tables use composite primary keys. This is only supported by Rails 7.1 and later. For older versions,
7
+ you need to use the third-party gem `composite_primary_key` (https://github.com/composite-primary-keys/composite_primary_keys).
7
8
 
8
9
  See https://cloud.google.com/spanner/docs/schema-and-data-model#creating-interleaved-tables for more information
9
10
  on interleaved tables if you are not familiar with this concept.
@@ -13,8 +14,6 @@ You can create interleaved tables using migrations in ActiveRecord by using the
13
14
  methods that are defined on `TableDefinition`:
14
15
  * `interleave_in`: Specifies which parent table a child table should be interleaved in and optionally whether
15
16
  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
17
 
19
18
  Cloud Spanner requires a child table to include the exact same primary key columns as the parent table in addition to
20
19
  the primary key column(s) of the child table. This means that the default `id` primary key column of ActiveRecord is
@@ -32,62 +31,55 @@ CREATE TABLE singers (
32
31
  ) PRIMARY KEY (singerid);
33
32
 
34
33
  CREATE TABLE albums (
35
- albumid INT64 NOT NULL,
36
34
  singerid INT64 NOT NULL,
35
+ albumid INT64 NOT NULL,
37
36
  title STRING(MAX)
38
37
  ) PRIMARY KEY (singerid, albumid), INTERLEAVE IN PARENT singers;
39
38
 
40
39
  CREATE TABLE tracks (
41
- trackid INT64 NOT NULL,
42
40
  singerid INT64 NOT NULL,
43
41
  albumid INT64 NOT NULL,
42
+ trackid INT64 NOT NULL,
44
43
  title STRING(MAX),
45
44
  duration NUMERIC
46
45
  ) PRIMARY KEY (singerid, albumid, trackid), INTERLEAVE IN PARENT albums ON DELETE CASCADE;
47
46
  ```
48
47
 
49
- This schema can be created in ActiveRecord as follows:
48
+ This schema can be created in ActiveRecord 7.1 and later as follows:
50
49
 
51
50
  ```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
51
+ # Execute the entire migration as one DDL batch.
52
+ connection.ddl_batch do
53
+ # Explicitly define the primary key.
54
+ create_table :singers, id: false, primary_key: :singerid do |t|
55
+ t.integer :singerid
56
+ t.string :first_name
57
+ t.string :last_name
58
+ end
58
59
 
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
60
+ create_table :albums, primary_key: [:singerid, :albumid], id: false do |t|
61
+ # Interleave the `albums` table in the parent table `singers`.
62
+ t.interleave_in :singers
63
+ t.integer :singerid
64
+ t.integer :albumid
65
+ t.string :title
66
+ end
69
67
 
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
68
+ create_table :tracks, primary_key: [:singerid, :albumid, :trackid], id: false do |t|
69
+ # Interleave the `tracks` table in the parent table `albums` and cascade delete all tracks that belong to an
70
+ # album when an album is deleted.
71
+ t.interleave_in :albums, :cascade
72
+ t.integer :singerid
73
+ t.integer :albumid
74
+ t.integer :trackid
75
+ t.string :title
76
+ t.numeric :duration
77
+ end
83
78
  end
84
79
  ```
85
80
 
86
81
  ## 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_keys` gem.
89
-
90
- An interleaved table parent/child relationship must be modelled as a `belongs_to`/`has_many` association in
82
+ An interleaved table parent/child relationship can be modelled as a `belongs_to`/`has_many` association in
91
83
  ActiveRecord. As the columns that are used to reference a parent record use a custom column name, it is required to also
92
84
  include the custom column name(s) in the `belongs_to` and `has_many` definitions.
93
85
 
@@ -108,7 +100,7 @@ class Singer < ActiveRecord::Base
108
100
  has_many :albums, foreign_key: :singerid
109
101
 
110
102
  # `tracks` is defined as INTERLEAVE IN PARENT `albums`.
111
- # The primary key of `tracks` is (`singerid`, `albumid`, `trackid`).
103
+ # The primary key of `tracks` is [`singerid`, `albumid`, `trackid`].
112
104
  # The `singerid` column can be used to associate tracks with a singer without the need to go through albums.
113
105
  # Note also that the inclusion of `singerid` as a column in `tracks` is required in order to make `tracks` a child
114
106
  # table of `albums` which has primary key (`singerid`, `albumid`).
@@ -116,24 +108,21 @@ class Singer < ActiveRecord::Base
116
108
  end
117
109
 
118
110
  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
111
  # `albums` is defined as INTERLEAVE IN PARENT `singers`.
123
112
  # The primary key of `singers` is `singerid`.
124
113
  belongs_to :singer, foreign_key: :singerid
125
114
 
126
115
  # `tracks` is defined as INTERLEAVE IN PARENT `albums`.
127
116
  # The primary key of `albums` is (`singerid`, `albumid`).
128
- has_many :tracks, foreign_key: [:singerid, :albumid]
117
+ # Rails 7.1 requires using query_constraints to define a composite foreign key.
118
+ has_many :tracks, query_constraints: [:singerid, :albumid]
129
119
  end
130
120
 
131
121
  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]
122
+ # `tracks` is defined as INTERLEAVE IN PARENT `albums`.
123
+ # The primary key of `albums` is (`singerid`, `albumid`).
124
+ # Rails 7.1 requires a composite primary key in a belongs_to relationship to be specified as query_constraints.
125
+ belongs_to :album, query_constraints: [:singerid, :albumid]
137
126
 
138
127
  # `tracks` also has a `singerid` column that can be used to associate a Track with a Singer.
139
128
  belongs_to :singer, foreign_key: :singerid
@@ -157,8 +146,10 @@ end
157
146
  The sample will automatically start a Spanner Emulator in a docker container and execute the sample
158
147
  against that emulator. The emulator will automatically be stopped when the application finishes.
159
148
 
160
- Run the application with the command
149
+ Run the application with the following commands:
161
150
 
162
151
  ```bash
152
+ export AR_VERSION="~> 7.1.2"
153
+ bundle install
163
154
  bundle exec rake run
164
155
  ```