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.
- checksums.yaml +4 -4
- data/.github/workflows/acceptance-tests-on-emulator.yaml +1 -1
- data/.github/workflows/acceptance-tests-on-production.yaml +5 -3
- data/.github/workflows/ci.yaml +1 -1
- data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +1 -1
- data/.github/workflows/nightly-acceptance-tests-on-production.yaml +5 -3
- data/.github/workflows/nightly-unit-tests.yaml +1 -1
- data/.github/workflows/release-please-label.yml +1 -1
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +8 -0
- data/Gemfile +5 -2
- data/README.md +10 -10
- data/acceptance/cases/interleaved_associations/has_many_associations_using_interleaved_test.rb +6 -0
- data/acceptance/cases/migration/change_schema_test.rb +19 -3
- data/acceptance/cases/migration/schema_dumper_test.rb +10 -1
- data/acceptance/cases/models/interleave_test.rb +6 -0
- data/acceptance/cases/tasks/database_tasks_test.rb +340 -2
- data/acceptance/cases/transactions/optimistic_locking_test.rb +6 -0
- data/acceptance/cases/transactions/read_write_transactions_test.rb +24 -0
- data/acceptance/models/table_with_sequence.rb +10 -0
- data/acceptance/schema/schema.rb +65 -19
- data/acceptance/test_helper.rb +1 -1
- data/activerecord-spanner-adapter.gemspec +1 -1
- data/examples/snippets/bit-reversed-sequence/README.md +103 -0
- data/examples/snippets/bit-reversed-sequence/Rakefile +13 -0
- data/examples/snippets/bit-reversed-sequence/application.rb +68 -0
- data/examples/snippets/bit-reversed-sequence/config/database.yml +8 -0
- data/examples/snippets/bit-reversed-sequence/db/migrate/01_create_tables.rb +33 -0
- data/examples/snippets/bit-reversed-sequence/db/schema.rb +31 -0
- data/examples/snippets/bit-reversed-sequence/db/seeds.rb +31 -0
- data/examples/snippets/bit-reversed-sequence/models/album.rb +11 -0
- data/examples/snippets/bit-reversed-sequence/models/singer.rb +15 -0
- data/examples/snippets/interleaved-tables/README.md +44 -53
- data/examples/snippets/interleaved-tables/Rakefile +2 -2
- data/examples/snippets/interleaved-tables/application.rb +2 -2
- data/examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb +12 -18
- data/examples/snippets/interleaved-tables/db/schema.rb +9 -7
- data/examples/snippets/interleaved-tables/db/seeds.rb +1 -1
- data/examples/snippets/interleaved-tables/models/album.rb +3 -7
- data/examples/snippets/interleaved-tables/models/singer.rb +1 -1
- data/examples/snippets/interleaved-tables/models/track.rb +6 -7
- data/examples/snippets/interleaved-tables-before-7.1/README.md +167 -0
- data/examples/snippets/interleaved-tables-before-7.1/Rakefile +13 -0
- data/examples/snippets/interleaved-tables-before-7.1/application.rb +126 -0
- data/examples/snippets/interleaved-tables-before-7.1/config/database.yml +8 -0
- data/examples/snippets/interleaved-tables-before-7.1/db/migrate/01_create_tables.rb +44 -0
- data/examples/snippets/interleaved-tables-before-7.1/db/schema.rb +37 -0
- data/examples/snippets/interleaved-tables-before-7.1/db/seeds.rb +40 -0
- data/examples/snippets/interleaved-tables-before-7.1/models/album.rb +20 -0
- data/examples/snippets/interleaved-tables-before-7.1/models/singer.rb +18 -0
- data/examples/snippets/interleaved-tables-before-7.1/models/track.rb +28 -0
- data/examples/snippets/query-logs/README.md +43 -0
- data/examples/snippets/query-logs/Rakefile +13 -0
- data/examples/snippets/query-logs/application.rb +63 -0
- data/examples/snippets/query-logs/config/database.yml +8 -0
- data/examples/snippets/query-logs/db/migrate/01_create_tables.rb +21 -0
- data/examples/snippets/query-logs/db/schema.rb +31 -0
- data/examples/snippets/query-logs/db/seeds.rb +24 -0
- data/examples/snippets/query-logs/models/album.rb +9 -0
- data/examples/snippets/query-logs/models/singer.rb +9 -0
- data/lib/active_record/connection_adapters/spanner/column.rb +13 -0
- data/lib/active_record/connection_adapters/spanner/database_statements.rb +144 -35
- data/lib/active_record/connection_adapters/spanner/schema_cache.rb +3 -21
- data/lib/active_record/connection_adapters/spanner/schema_creation.rb +11 -0
- data/lib/active_record/connection_adapters/spanner/schema_definitions.rb +4 -0
- data/lib/active_record/connection_adapters/spanner/schema_statements.rb +3 -2
- data/lib/active_record/connection_adapters/spanner_adapter.rb +28 -9
- data/lib/activerecord_spanner_adapter/base.rb +56 -19
- data/lib/activerecord_spanner_adapter/information_schema.rb +33 -24
- data/lib/activerecord_spanner_adapter/primary_key.rb +1 -1
- data/lib/activerecord_spanner_adapter/table/column.rb +4 -9
- data/lib/activerecord_spanner_adapter/version.rb +1 -1
- data/lib/arel/visitors/spanner.rb +3 -1
- 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
|
data/acceptance/schema/schema.rb
CHANGED
@@ -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
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
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
|
data/acceptance/test_helper.rb
CHANGED
@@ -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.
|
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,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
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
#
|
133
|
-
|
134
|
-
|
135
|
-
|
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
|
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
|
```
|