activerecord-spanner-adapter 2.0.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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/.kokoro/release.cfg +2 -2
  3. data/.kokoro/release.sh +1 -2
  4. data/.release-please-manifest.json +1 -1
  5. data/CHANGELOG.md +10 -0
  6. data/README.md +25 -23
  7. data/acceptance/cases/models/default_value_test.rb +2 -2
  8. data/acceptance/cases/tasks/database_tasks_test.rb +71 -74
  9. data/acceptance/cases/transactions/read_write_transactions_test.rb +10 -4
  10. data/acceptance/test_helper.rb +21 -8
  11. data/activerecord-spanner-adapter.gemspec +1 -1
  12. data/examples/snippets/Rakefile +1 -0
  13. data/examples/snippets/auto-generated-primary-key/README.md +140 -0
  14. data/examples/snippets/auto-generated-primary-key/Rakefile +13 -0
  15. data/examples/snippets/auto-generated-primary-key/application.rb +86 -0
  16. data/examples/snippets/auto-generated-primary-key/config/database.yml +10 -0
  17. data/examples/snippets/auto-generated-primary-key/db/migrate/01_create_tables.rb +29 -0
  18. data/examples/snippets/auto-generated-primary-key/db/seeds.rb +31 -0
  19. data/examples/snippets/auto-generated-primary-key/models/album.rb +11 -0
  20. data/examples/snippets/auto-generated-primary-key/models/singer.rb +11 -0
  21. data/lib/active_record/connection_adapters/spanner/column.rb +4 -0
  22. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +13 -2
  23. data/lib/active_record/connection_adapters/spanner/schema_statements.rb +5 -5
  24. data/lib/active_record/connection_adapters/spanner/type_metadata.rb +7 -3
  25. data/lib/active_record/connection_adapters/spanner_adapter.rb +22 -0
  26. data/lib/activerecord_spanner_adapter/base.rb +49 -16
  27. data/lib/activerecord_spanner_adapter/information_schema.rb +4 -2
  28. data/lib/activerecord_spanner_adapter/table/column.rb +4 -1
  29. data/lib/activerecord_spanner_adapter/version.rb +1 -1
  30. metadata +13 -5
@@ -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
@@ -19,6 +19,10 @@ module ActiveRecord
19
19
  @primary_key = primary_key
20
20
  end
21
21
 
22
+ def auto_incremented_by_db?
23
+ sql_type_metadata.is_identity
24
+ end
25
+
22
26
  def has_default?
23
27
  super && !virtual?
24
28
  end
@@ -8,6 +8,11 @@ module ActiveRecord
8
8
  module ConnectionAdapters
9
9
  module Spanner
10
10
  class SchemaCreation < SchemaCreation
11
+ def initialize connection
12
+ super
13
+ @connection = connection
14
+ end
15
+
11
16
  private
12
17
 
13
18
  # rubocop:disable Naming/MethodName, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
@@ -128,14 +133,18 @@ module ActiveRecord
128
133
  sql
129
134
  end
130
135
 
131
- # rubocop:enable Naming/MethodName, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
132
-
133
136
  def add_column_options! column, sql, options
134
137
  if options[:null] == false || options[:primary_key] == true
135
138
  sql << " NOT NULL"
136
139
  end
137
140
  if options.key? :default
138
141
  sql << " DEFAULT (#{quote_default_expression options[:default], column})"
142
+ elsif column.type == :primary_key
143
+ if @connection.use_auto_increment?
144
+ sql << " AUTO_INCREMENT"
145
+ elsif @connection.use_identity?
146
+ sql << " GENERATED BY DEFAULT AS IDENTITY (#{@connection.default_sequence_kind})"
147
+ end
139
148
  end
140
149
 
141
150
  if !options[:allow_commit_timestamp].nil? &&
@@ -158,6 +167,8 @@ module ActiveRecord
158
167
 
159
168
  sql
160
169
  end
170
+
171
+ # rubocop:enable Naming/MethodName, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
161
172
  end
162
173
  end
163
174
  end
@@ -21,8 +21,6 @@ module ActiveRecord
21
21
  # [Schema Doc](https://cloud.google.com/spanner/docs/information-schema)
22
22
  #
23
23
  module SchemaStatements
24
- VERSION_6_1_0 = Gem::Version.create "6.1.0"
25
- VERSION_6_0_3 = Gem::Version.create "6.0.3"
26
24
  VERSION_7_2 = Gem::Version.create "7.2.0"
27
25
 
28
26
  def current_database
@@ -125,18 +123,20 @@ module ActiveRecord
125
123
  fetch_type_metadata(field.spanner_type,
126
124
  field.ordinal_position,
127
125
  field.allow_commit_timestamp,
128
- field.generated),
126
+ field.generated,
127
+ is_identity: field.is_identity),
129
128
  field.nullable,
130
129
  field.default_function,
131
130
  primary_key: field.primary_key
132
131
  end
133
132
 
134
- def fetch_type_metadata sql_type, ordinal_position = nil, allow_commit_timestamp = nil, generated = nil
133
+ def fetch_type_metadata sql_type, ordinal_position = nil, allow_commit_timestamp = nil, generated = nil,
134
+ is_identity: false
135
135
  Spanner::TypeMetadata.new \
136
136
  super(sql_type),
137
137
  ordinal_position: ordinal_position,
138
138
  allow_commit_timestamp: allow_commit_timestamp,
139
- generated: generated
139
+ generated: generated, is_identity: is_identity
140
140
  end
141
141
 
142
142
  def add_column table_name, column_name, type, **options
@@ -17,12 +17,15 @@ module ActiveRecord
17
17
  attr_reader :ordinal_position
18
18
  attr_reader :allow_commit_timestamp
19
19
  attr_reader :generated
20
+ attr_reader :is_identity
20
21
 
21
- def initialize type_metadata, ordinal_position: nil, allow_commit_timestamp: nil, generated: nil
22
+ def initialize type_metadata, ordinal_position: nil, allow_commit_timestamp: nil, generated: nil,
23
+ is_identity: false
22
24
  super type_metadata
23
25
  @ordinal_position = ordinal_position
24
26
  @allow_commit_timestamp = allow_commit_timestamp
25
27
  @generated = generated
28
+ @is_identity = is_identity
26
29
  end
27
30
 
28
31
  def == other
@@ -30,12 +33,13 @@ module ActiveRecord
30
33
  __getobj__ == other.__getobj__ &&
31
34
  ordinal_position == other.ordinal_position &&
32
35
  allow_commit_timestamp == other.allow_commit_timestamp &&
33
- generated == other.generated
36
+ generated == other.generated &&
37
+ is_identity == other.is_identity
34
38
  end
35
39
  alias eql? ==
36
40
 
37
41
  def hash
38
- [TypeMetadata.name, __getobj__, ordinal_position, allow_commit_timestamp, generated].hash
42
+ [TypeMetadata.name, __getobj__, ordinal_position, allow_commit_timestamp, generated, is_identity].hash
39
43
  end
40
44
 
41
45
  private
@@ -71,6 +71,9 @@ module ActiveRecord
71
71
  # Determines whether or not to log query binds when executing statements
72
72
  class_attribute :log_statement_binds, instance_writer: false, default: false
73
73
 
74
+ attr_accessor :default_sequence_kind
75
+ attr_accessor :use_client_side_id_for_mutations
76
+
74
77
  def initialize config_or_deprecated_connection, deprecated_logger = nil,
75
78
  deprecated_connection_options = nil, deprecated_config = nil
76
79
  if config_or_deprecated_connection.is_a? Hash
@@ -86,6 +89,25 @@ module ActiveRecord
86
89
  end
87
90
  # Spanner does not support unprepared statements
88
91
  @prepared_statements = true
92
+ # The default for default_sequence_kind will be changed to BIT_REVERSED_POSITIVE
93
+ # in the next major version. The default value is currently DISABLED to prevent
94
+ # breaking changes to existing code.
95
+ @default_sequence_kind = @config.fetch :default_sequence_kind, "DISABLED"
96
+ @use_auto_increment = @default_sequence_kind&.casecmp? "AUTO_INCREMENT"
97
+ @auto_increment_disabled = @default_sequence_kind&.casecmp? "DISABLED"
98
+ @use_client_side_id_for_mutations = self.class.type_cast_config_to_boolean(
99
+ @config.fetch(:use_client_side_id_for_mutations, false)
100
+ )
101
+ end
102
+
103
+ def use_auto_increment?
104
+ "AUTO_INCREMENT".casecmp?(@default_sequence_kind || "")
105
+ end
106
+
107
+ def use_identity?
108
+ !use_auto_increment? \
109
+ && @default_sequence_kind \
110
+ && !@default_sequence_kind.casecmp?("DISABLED")
89
111
  end
90
112
 
91
113
  def max_identifier_length
@@ -35,7 +35,7 @@ module ActiveRecord
35
35
  return super if active_transaction?
36
36
 
37
37
  # Only use mutations to create new records if the primary key is generated client-side.
38
- isolation = sequence_name ? nil : :buffered_mutations
38
+ isolation = has_auto_generated_primary_key? ? nil : :buffered_mutations
39
39
  transaction isolation: isolation do
40
40
  return super
41
41
  end
@@ -53,6 +53,21 @@ module ActiveRecord
53
53
  !(buffered_mutations? || (primary_key && values.is_a?(Hash))) || !spanner_adapter?
54
54
  end
55
55
 
56
+ def self.has_auto_generated_primary_key?
57
+ return true if sequence_name
58
+ pk = primary_key
59
+ if pk.is_a? Array
60
+ return pk.any? do |col|
61
+ columns_hash[col].auto_incremented_by_db?
62
+ end
63
+ end
64
+ columns_hash[pk].auto_incremented_by_db?
65
+ end
66
+
67
+ def self.is_auto_generated? col
68
+ columns_hash[col]&.auto_incremented_by_db?
69
+ end
70
+
56
71
  def self._internal_insert_record values
57
72
  if ActiveRecord.gem_version < VERSION_7_2
58
73
  _insert_record values
@@ -73,10 +88,13 @@ module ActiveRecord
73
88
  return super
74
89
  end
75
90
 
76
- # Mutations cannot be used in combination with a sequence, as mutations do not support a THEN RETURN clause.
77
- if buffered_mutations? && sequence_name
78
- raise StatementInvalid, "Mutations cannot be used to create records that use a sequence " \
79
- "to generate the primary key. #{self} uses #{sequence_name}."
91
+ # Mutations cannot be used in combination with an auto-generated primary key,
92
+ # as mutations do not support a THEN RETURN clause.
93
+ if buffered_mutations? \
94
+ && has_auto_generated_primary_key? \
95
+ && !_has_all_primary_key_values?(primary_key, values) \
96
+ && !connection.use_client_side_id_for_mutations
97
+ raise StatementInvalid, "Mutations cannot be used to create records that use an auto-generated primary key."
80
98
  end
81
99
 
82
100
  return _buffer_record values, :insert, returning if buffered_mutations?
@@ -85,7 +103,7 @@ module ActiveRecord
85
103
  end
86
104
 
87
105
  def self._insert_record_dml values, returning
88
- primary_key_value = _set_primary_key_value values
106
+ primary_key_value = _set_primary_key_value values, false
89
107
  if ActiveRecord::VERSION::MAJOR >= 7
90
108
  im = Arel::InsertManager.new arel_table
91
109
  im.insert(values.transform_keys { |name| arel_table[name] })
@@ -97,11 +115,11 @@ module ActiveRecord
97
115
  _convert_primary_key result, returning
98
116
  end
99
117
 
100
- def self._set_primary_key_value values
118
+ def self._set_primary_key_value values, is_mutation
101
119
  if primary_key.is_a? Array
102
- _set_composite_primary_key_values primary_key, values
120
+ _set_composite_primary_key_values primary_key, values, is_mutation
103
121
  else
104
- _set_single_primary_key_value primary_key, values
122
+ _set_single_primary_key_value primary_key, values, is_mutation
105
123
  end
106
124
  end
107
125
 
@@ -192,9 +210,9 @@ module ActiveRecord
192
210
  def self._buffer_record values, method, returning
193
211
  primary_key_value =
194
212
  if primary_key.is_a? Array
195
- _set_composite_primary_key_values primary_key, values
213
+ _set_composite_primary_key_values primary_key, values, true
196
214
  else
197
- _set_single_primary_key_value primary_key, values
215
+ _set_single_primary_key_value primary_key, values, true
198
216
  end
199
217
 
200
218
  metadata = TableMetadata.new self, arel_table
@@ -213,13 +231,25 @@ module ActiveRecord
213
231
  _convert_primary_key primary_key_value, returning
214
232
  end
215
233
 
216
- def self._set_composite_primary_key_values primary_key, values
234
+ def self._has_all_primary_key_values? primary_key, values
235
+ if primary_key.is_a? Array
236
+ all = TrueClass
237
+ primary_key.each do |key|
238
+ all &&= values.key? key
239
+ end
240
+ all
241
+ else
242
+ values.key? primary_key
243
+ end
244
+ end
245
+
246
+ def self._set_composite_primary_key_values primary_key, values, is_mutation
217
247
  primary_key.map do |col|
218
- _set_composite_primary_key_value col, values
248
+ _set_composite_primary_key_value col, values, is_mutation
219
249
  end
220
250
  end
221
251
 
222
- def self._set_composite_primary_key_value primary_key, values
252
+ def self._set_composite_primary_key_value primary_key, values, is_mutation
223
253
  value = values[primary_key]
224
254
  type = ActiveModel::Type::BigInteger.new
225
255
 
@@ -228,6 +258,8 @@ module ActiveRecord
228
258
  value = value.value
229
259
  end
230
260
 
261
+ return value if is_auto_generated?(primary_key) \
262
+ && !(is_mutation && connection.use_client_side_id_for_mutations)
231
263
  return value unless prefetch_primary_key?
232
264
 
233
265
  if value.nil?
@@ -244,10 +276,11 @@ module ActiveRecord
244
276
  value
245
277
  end
246
278
 
247
- def self._set_single_primary_key_value primary_key, values
279
+ def self._set_single_primary_key_value primary_key, values, is_mutation
248
280
  primary_key_value = values[primary_key] || values[primary_key.to_sym]
249
281
 
250
- return primary_key_value if sequence_name
282
+ return primary_key_value if has_auto_generated_primary_key? \
283
+ && !(is_mutation && connection.use_client_side_id_for_mutations)
251
284
  return primary_key_value unless prefetch_primary_key?
252
285
 
253
286
  if primary_key_value.nil?
@@ -66,7 +66,8 @@ module ActiveRecordSpannerAdapter
66
66
  def table_columns table_name, column_name: nil, schema_name: ""
67
67
  primary_keys = table_primary_keys(table_name).map(&:name)
68
68
  sql = +"SELECT COLUMN_NAME, SPANNER_TYPE, IS_NULLABLE, GENERATION_EXPRESSION,"
69
- sql << " CAST(COLUMN_DEFAULT AS STRING) AS COLUMN_DEFAULT, ORDINAL_POSITION"
69
+ sql << " CAST(COLUMN_DEFAULT AS STRING) AS COLUMN_DEFAULT, ORDINAL_POSITION,"
70
+ sql << " IS_IDENTITY"
70
71
  sql << " FROM INFORMATION_SCHEMA.COLUMNS"
71
72
  sql << " WHERE TABLE_NAME=%<table_name>s"
72
73
  sql << " AND TABLE_SCHEMA=%<schema_name>s"
@@ -114,7 +115,8 @@ module ActiveRecordSpannerAdapter
114
115
  default: default,
115
116
  default_function: default_function,
116
117
  generated: row["GENERATION_EXPRESSION"].present?,
117
- primary_key: primary_key
118
+ primary_key: primary_key,
119
+ is_identity: row["IS_IDENTITY"] == "YES"
118
120
  end
119
121
 
120
122
  def table_column table_name, column_name, schema_name: ""
@@ -19,6 +19,7 @@ module ActiveRecordSpannerAdapter
19
19
  attr_accessor :generated
20
20
  attr_accessor :primary_key
21
21
  attr_accessor :nullable
22
+ attr_accessor :is_identity
22
23
 
23
24
  def initialize \
24
25
  table_name,
@@ -32,7 +33,8 @@ module ActiveRecordSpannerAdapter
32
33
  default: nil,
33
34
  default_function: nil,
34
35
  generated: nil,
35
- primary_key: false
36
+ primary_key: false,
37
+ is_identity: false
36
38
  @schema_name = schema_name.to_s
37
39
  @table_name = table_name.to_s
38
40
  @name = name.to_s
@@ -45,6 +47,7 @@ module ActiveRecordSpannerAdapter
45
47
  @default_function = default_function
46
48
  @generated = generated == true
47
49
  @primary_key = primary_key
50
+ @is_identity = is_identity
48
51
  end
49
52
 
50
53
  def spanner_type