activerecord-spanner-adapter 2.0.0 → 2.2.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/acceptance-tests-on-emulator.yaml +1 -1
  3. data/.github/workflows/ci.yaml +1 -1
  4. data/.kokoro/release.cfg +2 -2
  5. data/.kokoro/release.sh +1 -2
  6. data/.release-please-manifest.json +1 -1
  7. data/CHANGELOG.md +16 -0
  8. data/Gemfile +1 -1
  9. data/README.md +25 -23
  10. data/acceptance/cases/models/default_value_test.rb +2 -2
  11. data/acceptance/cases/tasks/database_tasks_test.rb +72 -75
  12. data/acceptance/cases/transactions/read_write_transactions_test.rb +10 -4
  13. data/acceptance/test_helper.rb +33 -8
  14. data/activerecord-spanner-adapter.gemspec +3 -4
  15. data/examples/snippets/Rakefile +1 -0
  16. data/examples/snippets/auto-generated-primary-key/README.md +140 -0
  17. data/examples/snippets/auto-generated-primary-key/Rakefile +13 -0
  18. data/examples/snippets/auto-generated-primary-key/application.rb +86 -0
  19. data/examples/snippets/auto-generated-primary-key/config/database.yml +10 -0
  20. data/examples/snippets/auto-generated-primary-key/db/migrate/01_create_tables.rb +29 -0
  21. data/examples/snippets/auto-generated-primary-key/db/seeds.rb +31 -0
  22. data/examples/snippets/auto-generated-primary-key/models/album.rb +11 -0
  23. data/examples/snippets/auto-generated-primary-key/models/singer.rb +11 -0
  24. data/examples/snippets/isolation-level/README.md +39 -0
  25. data/examples/snippets/isolation-level/Rakefile +13 -0
  26. data/examples/snippets/isolation-level/application.rb +36 -0
  27. data/examples/snippets/isolation-level/config/database.yml +10 -0
  28. data/examples/snippets/isolation-level/db/migrate/01_create_tables.rb +22 -0
  29. data/examples/snippets/isolation-level/db/seeds.rb +25 -0
  30. data/examples/snippets/isolation-level/models/album.rb +9 -0
  31. data/examples/snippets/isolation-level/models/singer.rb +9 -0
  32. data/lib/active_record/connection_adapters/spanner/column.rb +4 -0
  33. data/lib/active_record/connection_adapters/spanner/database_statements.rb +1 -1
  34. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +13 -2
  35. data/lib/active_record/connection_adapters/spanner/schema_statements.rb +5 -5
  36. data/lib/active_record/connection_adapters/spanner/type_metadata.rb +7 -3
  37. data/lib/active_record/connection_adapters/spanner_adapter.rb +28 -1
  38. data/lib/activerecord_spanner_adapter/base.rb +49 -16
  39. data/lib/activerecord_spanner_adapter/connection.rb +6 -1
  40. data/lib/activerecord_spanner_adapter/information_schema.rb +4 -2
  41. data/lib/activerecord_spanner_adapter/table/column.rb +4 -1
  42. data/lib/activerecord_spanner_adapter/transaction.rb +12 -2
  43. data/lib/activerecord_spanner_adapter/version.rb +1 -1
  44. metadata +28 -12
@@ -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
@@ -128,7 +150,8 @@ module ActiveRecord
128
150
  end
129
151
 
130
152
  # Spanner Connection API
131
- delegate :ddl_batch, :ddl_batch?, :start_batch_ddl, :abort_batch, :run_batch, to: :@connection
153
+ delegate :ddl_batch, :ddl_batch?, :start_batch_ddl, :abort_batch, :run_batch,
154
+ :isolation_level, :isolation_level=, to: :@connection
132
155
 
133
156
  def current_spanner_transaction
134
157
  @connection.current_transaction
@@ -148,6 +171,10 @@ module ActiveRecord
148
171
  false
149
172
  end
150
173
 
174
+ def supports_transaction_isolation?
175
+ true
176
+ end
177
+
151
178
  def supports_foreign_keys?
152
179
  true
153
180
  end
@@ -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?
@@ -14,10 +14,12 @@ module ActiveRecordSpannerAdapter
14
14
  attr_reader :database_id
15
15
  attr_reader :spanner
16
16
  attr_accessor :current_transaction
17
+ attr_accessor :isolation_level
17
18
 
18
19
  def initialize config
19
20
  @instance_id = config[:instance]
20
21
  @database_id = config[:database]
22
+ @isolation_level = config[:isolation_level]
21
23
  @spanner = self.class.spanners config
22
24
  end
23
25
 
@@ -42,6 +44,9 @@ module ActiveRecordSpannerAdapter
42
44
  # Call this method if you drop and recreate a database with the same name
43
45
  # to prevent the cached information to be used for the new database.
44
46
  def self.reset_information_schemas!
47
+ @information_schemas.each_value do |info_schema|
48
+ info_schema.connection.disconnect!
49
+ end
45
50
  @information_schemas = {}
46
51
  end
47
52
 
@@ -271,7 +276,7 @@ module ActiveRecordSpannerAdapter
271
276
 
272
277
  def begin_transaction isolation = nil
273
278
  raise "Nested transactions are not allowed" if current_transaction&.active?
274
- self.current_transaction = Transaction.new self, isolation
279
+ self.current_transaction = Transaction.new self, isolation || @isolation_level
275
280
  current_transaction.begin
276
281
  current_transaction
277
282
  end
@@ -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
@@ -55,11 +55,12 @@ module ActiveRecordSpannerAdapter
55
55
  when :pdml
56
56
  @grpc_transaction = @connection.session.create_pdml
57
57
  else
58
+ grpc_isolation = _transaction_isolation_level_to_grpc @isolation
58
59
  @begin_transaction_selector = Google::Cloud::Spanner::V1::TransactionSelector.new \
59
60
  begin: Google::Cloud::Spanner::V1::TransactionOptions.new(
60
- read_write: Google::Cloud::Spanner::V1::TransactionOptions::ReadWrite.new
61
+ read_write: Google::Cloud::Spanner::V1::TransactionOptions::ReadWrite.new,
62
+ isolation_level: grpc_isolation
61
63
  )
62
-
63
64
  end
64
65
  @state = :STARTED
65
66
  rescue Google::Cloud::NotFoundError => e
@@ -75,6 +76,15 @@ module ActiveRecordSpannerAdapter
75
76
  end
76
77
  end
77
78
 
79
+ def _transaction_isolation_level_to_grpc isolation
80
+ case isolation
81
+ when :serializable
82
+ Google::Cloud::Spanner::V1::TransactionOptions::IsolationLevel::SERIALIZABLE
83
+ when :repeatable_read
84
+ Google::Cloud::Spanner::V1::TransactionOptions::IsolationLevel::REPEATABLE_READ
85
+ end
86
+ end
87
+
78
88
  # Forces a BeginTransaction RPC for a read/write transaction. This is used by a
79
89
  # connection if the first statement of a transaction failed.
80
90
  def force_begin_read_write
@@ -5,5 +5,5 @@
5
5
  # https://opensource.org/licenses/MIT.
6
6
 
7
7
  module ActiveRecordSpannerAdapter
8
- VERSION = "2.0.0".freeze
8
+ VERSION = "2.2.0".freeze
9
9
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-spanner-adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Google LLC
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-01-23 00:00:00.000000000 Z
10
+ date: 2025-04-16 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: google-cloud-spanner
@@ -15,35 +15,35 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '2.18'
18
+ version: '2.25'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '2.18'
25
+ version: '2.25'
26
26
  - !ruby/object:Gem::Dependency
27
- name: grpc
27
+ name: google-cloud-spanner-v1
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - '='
30
+ - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: 1.64.3
32
+ version: '1.7'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - '='
37
+ - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 1.64.3
39
+ version: '1.7'
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: activerecord
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: '6.1'
46
+ version: '7.0'
47
47
  - - "<"
48
48
  - !ruby/object:Gem::Version
49
49
  version: '9'
@@ -53,7 +53,7 @@ dependencies:
53
53
  requirements:
54
54
  - - ">="
55
55
  - !ruby/object:Gem::Version
56
- version: '6.1'
56
+ version: '7.0'
57
57
  - - "<"
58
58
  - !ruby/object:Gem::Version
59
59
  version: '9'
@@ -357,6 +357,14 @@ files:
357
357
  - examples/snippets/array-data-type/db/migrate/01_create_tables.rb
358
358
  - examples/snippets/array-data-type/db/seeds.rb
359
359
  - examples/snippets/array-data-type/models/entity_with_array_types.rb
360
+ - examples/snippets/auto-generated-primary-key/README.md
361
+ - examples/snippets/auto-generated-primary-key/Rakefile
362
+ - examples/snippets/auto-generated-primary-key/application.rb
363
+ - examples/snippets/auto-generated-primary-key/config/database.yml
364
+ - examples/snippets/auto-generated-primary-key/db/migrate/01_create_tables.rb
365
+ - examples/snippets/auto-generated-primary-key/db/seeds.rb
366
+ - examples/snippets/auto-generated-primary-key/models/album.rb
367
+ - examples/snippets/auto-generated-primary-key/models/singer.rb
360
368
  - examples/snippets/bin/create_emulator_instance.rb
361
369
  - examples/snippets/bit-reversed-sequence/README.md
362
370
  - examples/snippets/bit-reversed-sequence/Rakefile
@@ -431,6 +439,14 @@ files:
431
439
  - examples/snippets/interleaved-tables/models/album.rb
432
440
  - examples/snippets/interleaved-tables/models/singer.rb
433
441
  - examples/snippets/interleaved-tables/models/track.rb
442
+ - examples/snippets/isolation-level/README.md
443
+ - examples/snippets/isolation-level/Rakefile
444
+ - examples/snippets/isolation-level/application.rb
445
+ - examples/snippets/isolation-level/config/database.yml
446
+ - examples/snippets/isolation-level/db/migrate/01_create_tables.rb
447
+ - examples/snippets/isolation-level/db/seeds.rb
448
+ - examples/snippets/isolation-level/models/album.rb
449
+ - examples/snippets/isolation-level/models/singer.rb
434
450
  - examples/snippets/migrations/README.md
435
451
  - examples/snippets/migrations/Rakefile
436
452
  - examples/snippets/migrations/application.rb
@@ -571,7 +587,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
571
587
  - !ruby/object:Gem::Version
572
588
  version: '0'
573
589
  requirements: []
574
- rubygems_version: 3.6.2
590
+ rubygems_version: 3.6.5
575
591
  specification_version: 4
576
592
  summary: Rails ActiveRecord connector for Google Spanner Database
577
593
  test_files: []