activerecord-spanner-adapter 1.8.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/acceptance-tests-on-emulator.yaml +4 -6
  3. data/.github/workflows/ci.yaml +4 -6
  4. data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +4 -6
  5. data/.github/workflows/nightly-unit-tests.yaml +4 -6
  6. data/.github/workflows/rubocop.yaml +1 -1
  7. data/.github/workflows/samples.yaml +30 -0
  8. data/.kokoro/release.cfg +2 -2
  9. data/.kokoro/release.sh +1 -4
  10. data/.release-please-manifest.json +1 -1
  11. data/.rubocop.yml +2 -2
  12. data/CHANGELOG.md +28 -0
  13. data/Gemfile +6 -5
  14. data/README.md +26 -22
  15. data/acceptance/cases/migration/command_recorder_test.rb +7 -38
  16. data/acceptance/cases/migration/references_index_test.rb +2 -11
  17. data/acceptance/cases/models/binary_identifiers.rb +97 -0
  18. data/acceptance/cases/models/default_value_test.rb +2 -2
  19. data/acceptance/cases/tasks/database_tasks_test.rb +71 -74
  20. data/acceptance/cases/transactions/read_write_transactions_test.rb +10 -4
  21. data/acceptance/models/binary_project.rb +20 -0
  22. data/acceptance/models/string_io.rb +28 -0
  23. data/acceptance/models/user.rb +20 -0
  24. data/acceptance/test_helper.rb +22 -8
  25. data/activerecord-spanner-adapter.gemspec +3 -3
  26. data/benchmarks/application.rb +3 -7
  27. data/examples/snippets/Rakefile +28 -5
  28. data/examples/snippets/array-data-type/application.rb +1 -5
  29. data/examples/snippets/array-data-type/config/database.yml +1 -0
  30. data/examples/snippets/auto-generated-primary-key/README.md +140 -0
  31. data/examples/snippets/auto-generated-primary-key/Rakefile +13 -0
  32. data/examples/snippets/auto-generated-primary-key/application.rb +86 -0
  33. data/examples/snippets/auto-generated-primary-key/config/database.yml +10 -0
  34. data/examples/snippets/auto-generated-primary-key/db/migrate/01_create_tables.rb +29 -0
  35. data/examples/snippets/auto-generated-primary-key/db/seeds.rb +31 -0
  36. data/examples/snippets/auto-generated-primary-key/models/album.rb +11 -0
  37. data/examples/snippets/auto-generated-primary-key/models/singer.rb +11 -0
  38. data/examples/snippets/bit-reversed-sequence/application.rb +0 -4
  39. data/examples/snippets/bit-reversed-sequence/config/database.yml +1 -0
  40. data/examples/snippets/bit-reversed-sequence/db/seeds.rb +2 -2
  41. data/examples/snippets/bulk-insert/application.rb +1 -5
  42. data/examples/snippets/bulk-insert/config/database.yml +1 -0
  43. data/examples/snippets/commit-timestamp/application.rb +0 -4
  44. data/examples/snippets/commit-timestamp/config/database.yml +1 -0
  45. data/examples/snippets/config/environment.rb +5 -0
  46. data/examples/snippets/create-records/application.rb +1 -5
  47. data/examples/snippets/create-records/config/database.yml +1 -0
  48. data/examples/snippets/date-data-type/application.rb +1 -5
  49. data/examples/snippets/date-data-type/config/database.yml +1 -0
  50. data/examples/snippets/date-data-type/db/seeds.rb +1 -1
  51. data/examples/snippets/generated-column/application.rb +0 -4
  52. data/examples/snippets/generated-column/config/database.yml +1 -0
  53. data/examples/snippets/generated-column/db/seeds.rb +1 -1
  54. data/examples/snippets/hints/application.rb +0 -4
  55. data/examples/snippets/hints/config/database.yml +1 -0
  56. data/examples/snippets/hints/db/seeds.rb +1 -1
  57. data/examples/snippets/interleaved-tables/application.rb +1 -5
  58. data/examples/snippets/interleaved-tables/config/database.yml +1 -0
  59. data/examples/snippets/interleaved-tables/db/seeds.rb +1 -1
  60. data/examples/snippets/interleaved-tables/models/album.rb +6 -2
  61. data/examples/snippets/interleaved-tables/models/track.rb +5 -1
  62. data/examples/snippets/interleaved-tables-before-7.1/application.rb +1 -5
  63. data/examples/snippets/interleaved-tables-before-7.1/config/database.yml +1 -0
  64. data/examples/snippets/interleaved-tables-before-7.1/db/seeds.rb +1 -1
  65. data/examples/snippets/migrations/application.rb +0 -4
  66. data/examples/snippets/migrations/config/database.yml +1 -0
  67. data/examples/snippets/mutations/application.rb +1 -5
  68. data/examples/snippets/mutations/config/database.yml +1 -0
  69. data/examples/snippets/mutations/db/seeds.rb +1 -1
  70. data/examples/snippets/optimistic-locking/application.rb +0 -4
  71. data/examples/snippets/optimistic-locking/config/database.yml +1 -0
  72. data/examples/snippets/optimistic-locking/db/seeds.rb +1 -1
  73. data/examples/snippets/partitioned-dml/application.rb +0 -4
  74. data/examples/snippets/partitioned-dml/config/database.yml +1 -0
  75. data/examples/snippets/partitioned-dml/db/seeds.rb +1 -1
  76. data/examples/snippets/query-logs/application.rb +15 -13
  77. data/examples/snippets/query-logs/config/database.yml +1 -0
  78. data/examples/snippets/query-logs/db/seeds.rb +1 -1
  79. data/examples/snippets/quickstart/application.rb +0 -4
  80. data/examples/snippets/quickstart/config/database.yml +1 -0
  81. data/examples/snippets/quickstart/db/seeds.rb +1 -1
  82. data/examples/snippets/read-only-transactions/application.rb +0 -4
  83. data/examples/snippets/read-only-transactions/config/database.yml +1 -0
  84. data/examples/snippets/read-only-transactions/db/seeds.rb +1 -1
  85. data/examples/snippets/read-write-transactions/application.rb +2 -6
  86. data/examples/snippets/read-write-transactions/config/database.yml +1 -0
  87. data/examples/snippets/read-write-transactions/db/seeds.rb +1 -1
  88. data/examples/snippets/stale-reads/application.rb +0 -4
  89. data/examples/snippets/stale-reads/config/database.yml +1 -0
  90. data/examples/snippets/stale-reads/db/seeds.rb +1 -1
  91. data/examples/snippets/tags/application.rb +0 -4
  92. data/examples/snippets/tags/config/database.yml +1 -0
  93. data/examples/snippets/tags/db/seeds.rb +1 -1
  94. data/examples/snippets/timestamp-data-type/application.rb +0 -4
  95. data/examples/snippets/timestamp-data-type/config/database.yml +1 -0
  96. data/lib/active_record/connection_adapters/spanner/column.rb +7 -3
  97. data/lib/active_record/connection_adapters/spanner/database_statements.rb +34 -22
  98. data/lib/active_record/connection_adapters/spanner/quoting.rb +2 -1
  99. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +20 -11
  100. data/lib/active_record/connection_adapters/spanner/schema_definitions.rb +12 -2
  101. data/lib/active_record/connection_adapters/spanner/schema_statements.rb +22 -51
  102. data/lib/active_record/connection_adapters/spanner/type_metadata.rb +10 -8
  103. data/lib/active_record/connection_adapters/spanner_adapter.rb +42 -7
  104. data/lib/active_record/tasks/spanner_database_tasks.rb +4 -4
  105. data/lib/active_record/type/spanner/array.rb +4 -0
  106. data/lib/active_record/type/spanner/bytes.rb +10 -0
  107. data/lib/activerecord_spanner_adapter/base.rb +59 -32
  108. data/lib/activerecord_spanner_adapter/connection.rb +9 -5
  109. data/lib/activerecord_spanner_adapter/foreign_key.rb +9 -2
  110. data/lib/activerecord_spanner_adapter/index/column.rb +6 -1
  111. data/lib/activerecord_spanner_adapter/index.rb +10 -2
  112. data/lib/activerecord_spanner_adapter/information_schema.rb +5 -3
  113. data/lib/activerecord_spanner_adapter/primary_key.rb +2 -2
  114. data/lib/activerecord_spanner_adapter/table/column.rb +16 -4
  115. data/lib/activerecord_spanner_adapter/table.rb +8 -2
  116. data/lib/activerecord_spanner_adapter/transaction.rb +1 -1
  117. data/lib/activerecord_spanner_adapter/version.rb +1 -1
  118. data/lib/arel/visitors/spanner.rb +16 -11
  119. data/lib/spanner_client_ext.rb +4 -3
  120. metadata +23 -34
  121. data/examples/snippets/array-data-type/db/schema.rb +0 -31
  122. data/examples/snippets/bit-reversed-sequence/db/schema.rb +0 -31
  123. data/examples/snippets/bulk-insert/db/schema.rb +0 -31
  124. data/examples/snippets/commit-timestamp/db/schema.rb +0 -34
  125. data/examples/snippets/create-records/db/schema.rb +0 -31
  126. data/examples/snippets/date-data-type/db/schema.rb +0 -26
  127. data/examples/snippets/generated-column/db/schema.rb +0 -26
  128. data/examples/snippets/hints/db/schema.rb +0 -33
  129. data/examples/snippets/interleaved-tables/db/schema.rb +0 -39
  130. data/examples/snippets/interleaved-tables-before-7.1/db/schema.rb +0 -37
  131. data/examples/snippets/migrations/db/schema.rb +0 -38
  132. data/examples/snippets/mutations/db/schema.rb +0 -32
  133. data/examples/snippets/optimistic-locking/db/schema.rb +0 -34
  134. data/examples/snippets/partitioned-dml/db/schema.rb +0 -31
  135. data/examples/snippets/query-logs/db/schema.rb +0 -31
  136. data/examples/snippets/quickstart/db/schema.rb +0 -31
  137. data/examples/snippets/read-only-transactions/db/schema.rb +0 -31
  138. data/examples/snippets/read-write-transactions/db/schema.rb +0 -32
  139. data/examples/snippets/stale-reads/db/schema.rb +0 -31
  140. data/examples/snippets/tags/db/schema.rb +0 -31
  141. data/examples/snippets/timestamp-data-type/db/schema.rb +0 -26
@@ -63,7 +63,7 @@ class Application
63
63
 
64
64
  puts ""
65
65
  puts "Press any key to end the application"
66
- STDIN.getch
66
+ $stdin.getch
67
67
  end
68
68
 
69
69
  def self.execute_individual_benchmarks singer, client
@@ -120,9 +120,7 @@ class Application
120
120
  sql = "SELECT * FROM Singers WHERE id=@id"
121
121
  params = { id: singer[:id] }
122
122
  param_types = { id: :INT64 }
123
- client.execute(sql, params: params, types: param_types).rows.each do |row|
124
- return row
125
- end
123
+ client.execute(sql, params: params, types: param_types).rows.first(1)
126
124
  else
127
125
  Singer.find singer.id
128
126
  end
@@ -134,9 +132,7 @@ class Application
134
132
  sql = "SELECT * FROM Singers WHERE id=@id"
135
133
  params = { id: singer[:id] }
136
134
  param_types = { id: :INT64 }
137
- client.execute(sql, params: params, types: param_types).rows.each do |row|
138
- return row
139
- end
135
+ client.execute(sql, params: params, types: param_types).rows.first(1)
140
136
  else
141
137
  singer.reload
142
138
  end
@@ -7,19 +7,38 @@
7
7
  require_relative "config/environment"
8
8
  require "docker"
9
9
 
10
- desc "Lists all available samples."
11
- task :list do
12
- samples = Dir.entries(".").select do |entry|
10
+ def fetch_samples
11
+ Dir.entries(".").select do |entry|
13
12
  File.directory?(File.join(".", entry)) \
14
- && !%w[. ..].include?(entry) \
15
- && File.exist?(File.join(".", entry, "application.rb"))
13
+ && !%w[. ..].include?(entry) \
14
+ && File.exist?(File.join(".", entry, "application.rb"))
16
15
  end
16
+ end
17
+
18
+ desc "Lists all available samples."
19
+ task :list do
20
+ samples = fetch_samples
17
21
  puts "Available samples: "
18
22
  samples.sort.each { |dir| puts " #{dir}" }
19
23
  puts ""
20
24
  puts "Run a sample with the command `bundle exec rake run\\[<sample-name>\\]`"
21
25
  end
22
26
 
27
+ desc "Runs all samples."
28
+ task :all do
29
+ samples = fetch_samples
30
+ ar_version = ENV.fetch "AR_VERSION", "~> 7.1.0"
31
+ less_than_7_1 = ar_version.dup.to_s.sub("~>", "").strip < "7.1.0"
32
+ samples.delete "interleaved-tables" if less_than_7_1
33
+ samples.delete "interleaved-tables-before-7.1" unless less_than_7_1
34
+ samples.delete "bit-reversed-sequence" if less_than_7_1
35
+ samples.delete "auto-generated-primary-key" if less_than_7_1
36
+ samples.delete "query-logs" if less_than_7_1
37
+ samples.each do |sample|
38
+ run_sample sample
39
+ end
40
+ end
41
+
23
42
  desc "Runs a simple ActiveRecord tutorial on a Spanner emulator."
24
43
  task :run, [:sample] do |_t, args|
25
44
  sample = args[:sample]
@@ -28,7 +47,11 @@ task :run, [:sample] do |_t, args|
28
47
  puts ""
29
48
  sample = "quickstart"
30
49
  end
50
+ run_sample sample
51
+ end
31
52
 
53
+ def run_sample sample
54
+ puts "Running #{sample}"
32
55
  puts "Downloading Spanner emulator image..."
33
56
  Docker::Image.create "fromImage" => "gcr.io/cloud-spanner-emulator/emulator:latest"
34
57
  puts "Creating Spanner emulator container..."
@@ -9,7 +9,7 @@ require_relative "../config/environment"
9
9
  require_relative "models/entity_with_array_types"
10
10
 
11
11
  class Application
12
- def self.run # rubocop:disable Metrics/AbcSize
12
+ def self.run
13
13
  # Create a record with all array types.
14
14
  record = EntityWithArrayTypes.create \
15
15
  col_array_string: ["value1", "value2", "value3"],
@@ -35,10 +35,6 @@ class Application
35
35
  puts "Bytes array: #{record.col_array_bytes.map(&:read)}"
36
36
  puts "Date array: #{record.col_array_date}"
37
37
  puts "Timestamp array: #{record.col_array_timestamp}"
38
-
39
- puts ""
40
- puts "Press any key to end the application"
41
- STDIN.getch
42
38
  end
43
39
  end
44
40
 
@@ -6,3 +6,4 @@ development:
6
6
  database: testdb
7
7
  pool: 5
8
8
  timeout: 5000
9
+ schema_dump: false
@@ -0,0 +1,140 @@
1
+ # Sample - Auto-generated Primary Keys
2
+
3
+ This example shows how to use an IDENTITY column to generate the primary key of a model.
4
+
5
+ See https://cloud.google.com/spanner/docs/primary-key-default-value#identity-columns for more information
6
+ about IDENTITY columns in Cloud Spanner.
7
+
8
+ ## Requirements
9
+ Using IDENTITY for generating primary key values in ActiveRecord has the following requirements:
10
+ 1. You must use __ActiveRecord version 7.1 or higher__.
11
+ 2. Your database configuration must include a `default_sequence_kind` value like this:
12
+
13
+ ```yaml
14
+ development:
15
+ adapter: spanner
16
+ emulator_host: localhost:9010
17
+ project: test-project
18
+ instance: test-instance
19
+ database: testdb
20
+ default_sequence_kind: BIT_REVERSED_POSITIVE
21
+ pool: 5
22
+ timeout: 5000
23
+ schema_dump: false
24
+ ```
25
+
26
+ ## Creating Tables with IDENTITY in ActiveRecord
27
+ You can create an IDENTITY column using migrations in ActiveRecord by using the `:primary_key` type
28
+ for the column. Note that this only generates an IDENTITY column if you have included the option
29
+ `default_sequence_kind: BIT_REVERSED_POSITIVE` as shown above.
30
+
31
+ Any default `id` column that is added by ActiveRecord will also use the `:primary_key` type and use
32
+ an IDENTITY column.
33
+
34
+ ```ruby
35
+ # The default `id` column that is added by ActiveRecord will automatically use
36
+ # an IDENTITY column.
37
+ create_table :singers do |t|
38
+ t.string :first_name
39
+ t.string :last_name
40
+ end
41
+
42
+ # You can also explicitly create a primary key column with a different name. Use
43
+ # the :primary_key type to generate an IDENTITY column.
44
+ create_table :singers, id: false, primary_key: :singerid do |t|
45
+ # Use the ':primary_key' data type to create an auto-generated primary key column.
46
+ t.column :singerid, :primary_key, primary_key: true, null: false
47
+ t.string :first_name
48
+ t.string :last_name
49
+ end
50
+ ```
51
+
52
+ ## Example Data Model
53
+ This example uses the following table schema:
54
+
55
+ ```sql
56
+ CREATE TABLE `singers` (
57
+ `singerid` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE),
58
+ `first_name` STRING(MAX),
59
+ `last_name` STRING(MAX)
60
+ ) PRIMARY KEY (`singerid`)
61
+
62
+ CREATE TABLE `albums` (
63
+ `singerid` INT64 NOT NULL,
64
+ `albumid` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE),
65
+ `title` STRING(MAX)
66
+ ) PRIMARY KEY (`singerid`, `albumid`), INTERLEAVE IN PARENT `singers`
67
+ ```
68
+
69
+ This schema can be created in ActiveRecord 7.1 and later as follows:
70
+
71
+ ```ruby
72
+ # Execute the entire migration as one DDL batch.
73
+ connection.ddl_batch do
74
+ create_table :singers, id: false, primary_key: :singerid do |t|
75
+ # Use the ':primary_key' data type to create an auto-generated primary key column.
76
+ t.column :singerid, :primary_key, primary_key: true, null: false
77
+ t.string :first_name
78
+ t.string :last_name
79
+ end
80
+
81
+ create_table :albums, primary_key: [:singerid, :albumid], id: false do |t|
82
+ # Interleave the `albums` table in the parent table `singers`.
83
+ t.interleave_in :singers
84
+ t.integer :singerid, null: false
85
+ # Use the ':primary_key' data type to create an auto-generated primary key column.
86
+ t.column :albumid, :primary_key, null: false
87
+ t.string :title
88
+ end
89
+ end
90
+ ```
91
+
92
+ ## Auto-generated Primary Keys and Mutations
93
+
94
+ Using primary keys that are auto-generated by an IDENTITY column is not
95
+ supported in combination with mutations, because Spanner must return the
96
+ generated primary key value after a row was inserted using a `THEN RETURN`
97
+ clause. This is not supported for mutations.
98
+
99
+ A workaround for this is to explicitly set a value for the primary key of
100
+ a row before inserting it:
101
+
102
+ ```ruby
103
+ ActiveRecord::Base.transaction isolation: :buffered_mutations do
104
+ # Assign the singer a random id value. This value will be included in the insert mutation.
105
+ singer = Singer.create id: SecureRandom.uuid.gsub("-", "").hex & 0x7FFFFFFFFFFFFFFF,
106
+ first_name: "Melissa", last_name: "Garcia"
107
+ end
108
+ ```
109
+
110
+ You can also instruct the Spanner ActiveRecord provider to automatically include a
111
+ primary key value when you use mutations. The primary key value assignment can then
112
+ be omitted from your code. Add `use_client_side_id_for_mutations: true` to your
113
+ configuration for this:
114
+
115
+ ```yaml
116
+ development:
117
+ adapter: spanner
118
+ emulator_host: localhost:9010
119
+ project: test-project
120
+ instance: test-instance
121
+ database: testdb
122
+ default_sequence_kind: BIT_REVERSED_POSITIVE
123
+ use_client_side_id_for_mutations: true
124
+ pool: 5
125
+ timeout: 5000
126
+ schema_dump: false
127
+ ```
128
+
129
+ ## Running the Sample
130
+
131
+ The sample will automatically start a Spanner Emulator in a docker container and execute the sample
132
+ against that emulator. The emulator will automatically be stopped when the application finishes.
133
+
134
+ Run the application with the following commands:
135
+
136
+ ```bash
137
+ export AR_VERSION="~> 7.1.2"
138
+ bundle install
139
+ bundle exec rake run
140
+ ```
@@ -0,0 +1,13 @@
1
+ # Copyright 2025 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 auto-generated-primary keys."
11
+ task :run do
12
+ Dir.chdir("..") { sh "bundle exec rake run[auto-generated-primary-key]" }
13
+ end
@@ -0,0 +1,86 @@
1
+ # Copyright 2025 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
+ # Create a new singer using mutations.
27
+ # This requires us to set a primary key value manually.
28
+ # You can also instruct the Spanner Ruby ActiveRecord provider to that automatically
29
+ # by including this in your database configuration:
30
+ # use_client_side_id_for_mutations: true
31
+ create_new_singer_using_mutations
32
+ end
33
+
34
+ def self.find_album singerid, albumid
35
+ album = Album.find [singerid, albumid]
36
+ puts "Found album: #{album.title}"
37
+ end
38
+
39
+ def self.list_singers_albums
40
+ puts ""
41
+ puts "Listing all singers with corresponding albums"
42
+ Singer.all.order("last_name, first_name").each do |singer|
43
+ puts "#{singer.first_name} #{singer.last_name} has #{singer.albums.count} albums:"
44
+ singer.albums.order("title").each do |album|
45
+ puts " #{album.title}"
46
+ end
47
+ end
48
+ end
49
+
50
+ def self.create_new_singer
51
+ # Create a new singer. The singerid is generated by an IDENTITY column in the database and returned.
52
+ puts ""
53
+ singer = Singer.create first_name: "Melissa", last_name: "Garcia"
54
+ puts "Created a new singer '#{singer.first_name} #{singer.last_name}' with id #{singer.singerid}"
55
+
56
+ singer
57
+ end
58
+
59
+ def self.create_new_album singer
60
+ # Create a new album.
61
+ puts ""
62
+ puts "Creating a new album for #{singer.first_name} #{singer.last_name}"
63
+ # The albumid is not generated by the database.
64
+ album = singer.albums.build albumid: 1, title: "New Title"
65
+ album.save!
66
+
67
+ album
68
+ end
69
+
70
+ def self.create_new_singer_using_mutations
71
+ # Create a new singer using mutations. Mutations do not support THEN RETURN clauses,
72
+ # so we must specify a value for the primary key before inserting the row.
73
+ puts ""
74
+ singer = nil
75
+ ActiveRecord::Base.transaction isolation: :buffered_mutations do
76
+ # Assign the singer a random id value. This value will be included in the insert mutation.
77
+ singer = Singer.create id: SecureRandom.uuid.gsub("-", "").hex & 0x7FFFFFFFFFFFFFFF,
78
+ first_name: "Melissa", last_name: "Garcia"
79
+ end
80
+ puts "Inserted a new singer '#{singer.first_name} #{singer.last_name}' using mutations with id #{singer.singerid}"
81
+
82
+ singer
83
+ end
84
+ end
85
+
86
+ Application.run
@@ -0,0 +1,10 @@
1
+ development:
2
+ adapter: spanner
3
+ emulator_host: localhost:9010
4
+ project: test-project
5
+ instance: test-instance
6
+ database: testdb
7
+ default_sequence_kind: BIT_REVERSED_POSITIVE
8
+ pool: 5
9
+ timeout: 5000
10
+ schema_dump: false
@@ -0,0 +1,29 @@
1
+ # Copyright 2025 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
+ connection.default_sequence_kind = "BIT_REVERSED_POSITIVE"
10
+ # Execute the entire migration as one DDL batch.
11
+ connection.ddl_batch do
12
+ create_table :singers, id: false, primary_key: :singerid do |t|
13
+ # Use the ':primary_key' data type to create an auto-generated primary key column.
14
+ t.column :singerid, :primary_key, primary_key: true, null: false
15
+ t.string :first_name
16
+ t.string :last_name
17
+ end
18
+
19
+ create_table :albums, primary_key: [:singerid, :albumid], id: false do |t|
20
+ # Interleave the `albums` table in the parent table `singers`.
21
+ t.interleave_in :singers
22
+ t.integer :singerid, null: false
23
+ # Use the ':primary_key' data type to create an auto-generated primary key column.
24
+ t.column :albumid, :primary_key, null: false
25
+ t.string :title
26
+ end
27
+ end
28
+ end
29
+ 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"
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 2025 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,11 @@
1
+ # Copyright 2025 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
+ end
@@ -22,10 +22,6 @@ class Application
22
22
 
23
23
  # List all singers, albums and tracks.
24
24
  list_singers_albums
25
-
26
- puts ""
27
- puts "Press any key to end the application"
28
- STDIN.getch
29
25
  end
30
26
 
31
27
  def self.find_album singerid, albumid
@@ -6,3 +6,4 @@ development:
6
6
  database: testdb
7
7
  pool: 5
8
8
  timeout: 5000
9
+ schema_dump: false
@@ -4,7 +4,7 @@
4
4
  # license that can be found in the LICENSE file or at
5
5
  # https://opensource.org/licenses/MIT.
6
6
 
7
- require_relative "../../config/environment.rb"
7
+ require_relative "../../config/environment"
8
8
  require_relative "../models/singer"
9
9
  require_relative "../models/album"
10
10
 
@@ -14,7 +14,7 @@ last_names = %w[Wendelson Allison Peterson Johnson Henderson Ericsson Aronson Te
14
14
  adjectives = %w[daily happy blue generous cooked bad open]
15
15
  nouns = %w[windows potatoes bank street tree glass bottle]
16
16
 
17
- # Note: We do not use mutations to insert these rows, because letting the database generate the primary key means that
17
+ # NOTE: We do not use mutations to insert these rows, because letting the database generate the primary key means that
18
18
  # we rely on a `THEN RETURN id` clause in the insert statement. This is only supported for DML statements, and not for
19
19
  # mutations.
20
20
  ActiveRecord::Base.transaction do
@@ -46,7 +46,7 @@ class Application
46
46
  ]
47
47
  end
48
48
  puts ""
49
- puts "Created a batch of #{singers.length} singers and #{albums.length} "\
49
+ puts "Created a batch of #{singers.length} singers and #{albums.length} " \
50
50
  "albums using a transaction with buffered mutations:"
51
51
  singers.each do |s|
52
52
  puts " Created singer #{s.first_name} #{s.last_name} with id #{s.id}"
@@ -54,10 +54,6 @@ class Application
54
54
  puts " with album #{a.title} with id #{a.id}"
55
55
  end
56
56
  end
57
-
58
- puts ""
59
- puts "Press any key to end the application"
60
- STDIN.getch
61
57
  end
62
58
  end
63
59
 
@@ -6,3 +6,4 @@ development:
6
6
  database: testdb
7
7
  pool: 5
8
8
  timeout: 5000
9
+ schema_dump: false
@@ -43,10 +43,6 @@ class Application
43
43
  puts "Singer and album updated:"
44
44
  puts "#{singer.first_name} #{singer.last_name} (Last updated: #{singer.last_updated.strftime format})"
45
45
  puts " #{album.title} (Last updated: #{album.last_updated.strftime format})"
46
-
47
- puts ""
48
- puts "Press any key to end the application"
49
- STDIN.getch
50
46
  end
51
47
  end
52
48
 
@@ -6,3 +6,4 @@ development:
6
6
  database: testdb
7
7
  pool: 5
8
8
  timeout: 5000
9
+ schema_dump: false
@@ -4,11 +4,16 @@
4
4
  # license that can be found in the LICENSE file or at
5
5
  # https://opensource.org/licenses/MIT.
6
6
 
7
+ require "logger" # https://github.com/rails/rails/issues/54260
7
8
  require "active_record"
8
9
  require "bundler"
9
10
 
10
11
  Dir["../../lib/*.rb"].each { |file| require file }
11
12
 
13
+ if ActiveRecord.version >= Gem::Version.create("7.2.0")
14
+ ActiveRecord::ConnectionAdapters.register "spanner", "ActiveRecord::ConnectionAdapters::SpannerAdapter"
15
+ end
16
+
12
17
  Bundler.require
13
18
 
14
19
  ActiveRecord::Base.establish_connection(
@@ -13,7 +13,7 @@ class Application
13
13
  def self.run
14
14
  # Creating a single record without an explicit transaction will automatically save it to the database.
15
15
  # It is not recommended to call Entity.create repeatedly to insert multiple records, as each call will
16
- # use a separate Spanner transaction. Instead multiple records should be created by passing an array of
16
+ # use a separate Spanner transaction. Instead, multiple records should be created by passing an array of
17
17
  # entities to the Entity.create method.
18
18
  singer = Singer.create first_name: "Dave", last_name: "Allison"
19
19
  puts ""
@@ -32,10 +32,6 @@ class Application
32
32
  singers.each do |s|
33
33
  puts " Created singer #{s.first_name} #{s.last_name} with id #{s.id}"
34
34
  end
35
-
36
- puts ""
37
- puts "Press any key to end the application"
38
- STDIN.getch
39
35
  end
40
36
  end
41
37
 
@@ -6,3 +6,4 @@ development:
6
6
  database: testdb
7
7
  pool: 5
8
8
  timeout: 5000
9
+ schema_dump: false
@@ -17,7 +17,7 @@ class Application
17
17
  puts "#{"#{singer.first_name} #{singer.last_name}".ljust 30}#{singer.birth_date}"
18
18
  end
19
19
 
20
- # Update the birth date of a random singer using the current system time. Any time and timezone information will be
20
+ # Update the birthdate of a random singer using the current system time. Any time and timezone information will be
21
21
  # lost after saving the record as a DATE only contains the year, month and day-of-month information.
22
22
  singer = Singer.all.sample
23
23
  singer.update birth_date: Time.now
@@ -25,10 +25,6 @@ class Application
25
25
  puts ""
26
26
  puts "Updated birth date to current system time:"
27
27
  puts "#{"#{singer.first_name} #{singer.last_name}".ljust 30}#{singer.birth_date}"
28
-
29
- puts ""
30
- puts "Press any key to end the application"
31
- STDIN.getch
32
28
  end
33
29
  end
34
30
 
@@ -6,3 +6,4 @@ development:
6
6
  database: testdb
7
7
  pool: 5
8
8
  timeout: 5000
9
+ schema_dump: false
@@ -4,7 +4,7 @@
4
4
  # license that can be found in the LICENSE file or at
5
5
  # https://opensource.org/licenses/MIT.
6
6
  #
7
- require_relative "../../config/environment.rb"
7
+ require_relative "../../config/environment"
8
8
  require_relative "../models/singer"
9
9
 
10
10
  first_names = %w[Nelson Todd William Alex Dominique Adenoid Steve Nathan Beverly Annie Amy Norma Diana Regan Phyllis]
@@ -27,10 +27,6 @@ class Application
27
27
  singer.reload
28
28
  puts ""
29
29
  puts "Singer updated: #{singer.full_name}"
30
-
31
- puts ""
32
- puts "Press any key to end the application"
33
- STDIN.getch
34
30
  end
35
31
  end
36
32
 
@@ -6,3 +6,4 @@ development:
6
6
  database: testdb
7
7
  pool: 5
8
8
  timeout: 5000
9
+ schema_dump: false
@@ -4,7 +4,7 @@
4
4
  # license that can be found in the LICENSE file or at
5
5
  # https://opensource.org/licenses/MIT.
6
6
 
7
- require_relative "../../config/environment.rb"
7
+ require_relative "../../config/environment"
8
8
  require_relative "../models/singer"
9
9
 
10
10
  first_names = %w[Pete Alice John Ethel Trudy Naomi Wendy Ruben Thomas Elly]
@@ -37,10 +37,6 @@ class Application
37
37
  .distinct.order("last_name, first_name").each do |singer|
38
38
  puts singer.full_name
39
39
  end
40
-
41
- puts ""
42
- puts "Press any key to end the application"
43
- STDIN.getch
44
40
  end
45
41
  end
46
42
 
@@ -6,3 +6,4 @@ development:
6
6
  database: testdb
7
7
  pool: 5
8
8
  timeout: 5000
9
+ schema_dump: false