activerecord-spanner-adapter 1.5.0 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) 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 +14 -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/insert_all_test.rb +22 -0
  17. data/acceptance/cases/models/interleave_test.rb +6 -0
  18. data/acceptance/cases/tasks/database_tasks_test.rb +340 -2
  19. data/acceptance/cases/transactions/optimistic_locking_test.rb +6 -0
  20. data/acceptance/cases/transactions/read_write_transactions_test.rb +24 -0
  21. data/acceptance/models/table_with_sequence.rb +10 -0
  22. data/acceptance/schema/schema.rb +65 -19
  23. data/acceptance/test_helper.rb +1 -1
  24. data/activerecord-spanner-adapter.gemspec +1 -1
  25. data/examples/snippets/bit-reversed-sequence/README.md +103 -0
  26. data/examples/snippets/bit-reversed-sequence/Rakefile +13 -0
  27. data/examples/snippets/bit-reversed-sequence/application.rb +68 -0
  28. data/examples/snippets/bit-reversed-sequence/config/database.yml +8 -0
  29. data/examples/snippets/bit-reversed-sequence/db/migrate/01_create_tables.rb +33 -0
  30. data/examples/snippets/bit-reversed-sequence/db/schema.rb +31 -0
  31. data/examples/snippets/bit-reversed-sequence/db/seeds.rb +31 -0
  32. data/examples/snippets/bit-reversed-sequence/models/album.rb +11 -0
  33. data/examples/snippets/bit-reversed-sequence/models/singer.rb +15 -0
  34. data/examples/snippets/interleaved-tables/README.md +44 -53
  35. data/examples/snippets/interleaved-tables/Rakefile +2 -2
  36. data/examples/snippets/interleaved-tables/application.rb +2 -2
  37. data/examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb +12 -18
  38. data/examples/snippets/interleaved-tables/db/schema.rb +9 -7
  39. data/examples/snippets/interleaved-tables/db/seeds.rb +1 -1
  40. data/examples/snippets/interleaved-tables/models/album.rb +3 -7
  41. data/examples/snippets/interleaved-tables/models/singer.rb +1 -1
  42. data/examples/snippets/interleaved-tables/models/track.rb +6 -7
  43. data/examples/snippets/interleaved-tables-before-7.1/README.md +167 -0
  44. data/examples/snippets/interleaved-tables-before-7.1/Rakefile +13 -0
  45. data/examples/snippets/interleaved-tables-before-7.1/application.rb +126 -0
  46. data/examples/snippets/interleaved-tables-before-7.1/config/database.yml +8 -0
  47. data/examples/snippets/interleaved-tables-before-7.1/db/migrate/01_create_tables.rb +44 -0
  48. data/examples/snippets/interleaved-tables-before-7.1/db/schema.rb +37 -0
  49. data/examples/snippets/interleaved-tables-before-7.1/db/seeds.rb +40 -0
  50. data/examples/snippets/interleaved-tables-before-7.1/models/album.rb +20 -0
  51. data/examples/snippets/interleaved-tables-before-7.1/models/singer.rb +18 -0
  52. data/examples/snippets/interleaved-tables-before-7.1/models/track.rb +28 -0
  53. data/examples/snippets/query-logs/README.md +43 -0
  54. data/examples/snippets/query-logs/Rakefile +13 -0
  55. data/examples/snippets/query-logs/application.rb +63 -0
  56. data/examples/snippets/query-logs/config/database.yml +8 -0
  57. data/examples/snippets/query-logs/db/migrate/01_create_tables.rb +21 -0
  58. data/examples/snippets/query-logs/db/schema.rb +31 -0
  59. data/examples/snippets/query-logs/db/seeds.rb +24 -0
  60. data/examples/snippets/query-logs/models/album.rb +9 -0
  61. data/examples/snippets/query-logs/models/singer.rb +9 -0
  62. data/lib/active_record/connection_adapters/spanner/column.rb +13 -0
  63. data/lib/active_record/connection_adapters/spanner/database_statements.rb +144 -35
  64. data/lib/active_record/connection_adapters/spanner/schema_cache.rb +3 -21
  65. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +11 -0
  66. data/lib/active_record/connection_adapters/spanner/schema_definitions.rb +4 -0
  67. data/lib/active_record/connection_adapters/spanner/schema_statements.rb +3 -2
  68. data/lib/active_record/connection_adapters/spanner_adapter.rb +28 -9
  69. data/lib/activerecord_spanner_adapter/base.rb +58 -21
  70. data/lib/activerecord_spanner_adapter/information_schema.rb +33 -24
  71. data/lib/activerecord_spanner_adapter/primary_key.rb +1 -1
  72. data/lib/activerecord_spanner_adapter/table/column.rb +4 -9
  73. data/lib/activerecord_spanner_adapter/version.rb +1 -1
  74. data/lib/arel/visitors/spanner.rb +3 -1
  75. metadata +33 -4
@@ -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
@@ -0,0 +1,28 @@
1
+ # Copyright 2022 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ class Track < ActiveRecord::Base
8
+ # Use the `composite_primary_key` gem to create a composite primary key definition for the model.
9
+ self.primary_keys = :singerid, :albumid, :trackid
10
+
11
+ # `tracks` is defined as INTERLEAVE IN PARENT `albums`. The primary key of `albums` is ()`singerid`, `albumid`).
12
+ belongs_to :album, foreign_key: [:singerid, :albumid]
13
+
14
+ # `tracks` also has a `singerid` column should be used to associate a Track with a Singer.
15
+ belongs_to :singer, foreign_key: :singerid
16
+
17
+ # Override the default initialize method to automatically set the singer attribute when an album is given.
18
+ def initialize attributes = nil
19
+ super
20
+ self.singer ||= album&.singer
21
+ end
22
+
23
+ def album=value
24
+ super
25
+ # Ensure the singer of this track is equal to the singer of the album that is set.
26
+ self.singer = value&.singer
27
+ end
28
+ end
@@ -0,0 +1,43 @@
1
+ # Sample - Query Logs
2
+
3
+ __NOTE__: Query logs require additional configuration for Cloud Spanner. Please read the entire file.
4
+
5
+ Rails 7.0 and higher supports [Query Logs](https://api.rubyonrails.org/classes/ActiveRecord/QueryLogs.html). Query Logs
6
+ can be used to automatically annotate all queries that are executed based on the current execution context.
7
+
8
+ The Cloud Spanner ActiveRecord provider can be used in combination with Query Logs. The query logs are automatically
9
+ translated to request tags for the queries.
10
+ See https://cloud.google.com/spanner/docs/introspection/troubleshooting-with-tags for more
11
+ information about request and transaction tags in Cloud Spanner.
12
+
13
+ ## Configuration
14
+ Using Query Logs with Cloud Spanner requires some specific configuration:
15
+ 1. You must set `ActiveRecord::QueryLogs.prepend_comment = true`
16
+ 2. You must include `{ request_tag: "true" }` as the first tag in your configuration.
17
+
18
+ ```ruby
19
+ ActiveRecord::QueryLogs.prepend_comment = true
20
+ config.active_record.query_log_tags = [
21
+ {
22
+ request_tag: "true",
23
+ },
24
+ :namespaced_controller,
25
+ :action,
26
+ :job,
27
+ {
28
+ request_id: ->(context) { context[:controller]&.request&.request_id },
29
+ job_id: ->(context) { context[:job]&.job_id },
30
+ tenant_id: -> { Current.tenant&.id },
31
+ static: "value",
32
+ },
33
+ ]
34
+ ```
35
+
36
+ The sample will automatically start a Spanner Emulator in a docker container and execute the sample
37
+ against that emulator. The emulator will automatically be stopped when the application finishes.
38
+
39
+ Run the application with the command
40
+
41
+ ```bash
42
+ bundle exec rake run
43
+ ```
@@ -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 use automatic query log tagging on Cloud Spanner with ActiveRecord."
11
+ task :run do
12
+ Dir.chdir("..") { sh "bundle exec rake run[query-logs]" }
13
+ end
@@ -0,0 +1,63 @@
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
+ enable_query_logs
15
+
16
+ puts ""
17
+ puts "Query all Albums and include an automatically generated request tag"
18
+ albums = Album.all
19
+ puts "Queried #{albums.length} albums using an automatically generated request tag"
20
+
21
+ puts ""
22
+ puts "Press any key to end the application"
23
+ STDIN.getch
24
+ end
25
+
26
+ def self.enable_query_logs
27
+ # Enables Query Logs in a non-Rails application. Normally, this should be done
28
+ # as described here: https://api.rubyonrails.org/classes/ActiveRecord/QueryLogs.html
29
+ ActiveRecord.query_transformers << ActiveRecord::QueryLogs
30
+
31
+ # Query log comments *MUST* be prepended to be included as a request tag.
32
+ ActiveRecord::QueryLogs.prepend_comment = true
33
+
34
+ # This block manually enables Query Logs without a full Rails application.
35
+ # This should normally not be needed in your application.
36
+ ActiveRecord::QueryLogs.taggings.merge!(
37
+ application: "example-app",
38
+ action: "run-test-application",
39
+ pid: -> { Process.pid.to_s },
40
+ socket: ->(context) { context[:connection].pool.db_config.socket },
41
+ db_host: ->(context) { context[:connection].pool.db_config.host },
42
+ database: ->(context) { context[:connection].pool.db_config.database }
43
+ )
44
+
45
+ ActiveRecord::QueryLogs.tags = [
46
+ # The first tag *MUST* be the fixed value 'request_tag:true'.
47
+ {
48
+ request_tag: "true"
49
+ },
50
+ :controller,
51
+ :action,
52
+ :job,
53
+ {
54
+ request_id: ->(context) { context[:controller]&.request&.request_id },
55
+ job_id: ->(context) { context[:job]&.job_id }
56
+ },
57
+ :db_host,
58
+ :database
59
+ ]
60
+ end
61
+ end
62
+
63
+ 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,21 @@
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[6.0]
8
+ def change
9
+ connection.ddl_batch do
10
+ create_table :singers do |t|
11
+ t.string :first_name
12
+ t.string :last_name
13
+ end
14
+
15
+ create_table :albums do |t|
16
+ t.string :title
17
+ t.references :singer, index: false, foreign_key: true
18
+ end
19
+ end
20
+ end
21
+ end