activerecord-spanner-adapter 1.0.1 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -1
  3. data/.github/sync-repo-settings.yaml +2 -2
  4. data/.github/workflows/acceptance-tests-on-emulator.yaml +10 -6
  5. data/.github/workflows/acceptance-tests-on-production.yaml +1 -1
  6. data/.github/workflows/ci.yaml +10 -8
  7. data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +14 -5
  8. data/.github/workflows/nightly-acceptance-tests-on-production.yaml +2 -2
  9. data/.github/workflows/nightly-unit-tests.yaml +14 -5
  10. data/.github/workflows/release-please.yml +2 -2
  11. data/.github/workflows/rubocop.yaml +3 -3
  12. data/.kokoro/release.sh +1 -3
  13. data/.release-please-manifest.json +1 -1
  14. data/.toys/release.rb +8 -2
  15. data/CHANGELOG.md +18 -0
  16. data/Gemfile +5 -1
  17. data/acceptance/cases/interleaved_associations/has_many_associations_using_interleaved_test.rb +12 -8
  18. data/acceptance/cases/models/insert_all_test.rb +150 -0
  19. data/acceptance/cases/transactions/optimistic_locking_test.rb +5 -0
  20. data/acceptance/cases/type/all_types_test.rb +10 -13
  21. data/acceptance/cases/type/json_test.rb +0 -2
  22. data/acceptance/models/album.rb +7 -2
  23. data/acceptance/models/singer.rb +2 -2
  24. data/acceptance/models/track.rb +5 -2
  25. data/acceptance/schema/schema.rb +2 -4
  26. data/acceptance/test_helper.rb +1 -1
  27. data/activerecord-spanner-adapter.gemspec +1 -1
  28. data/examples/snippets/interleaved-tables/README.md +164 -0
  29. data/examples/snippets/interleaved-tables/Rakefile +13 -0
  30. data/examples/snippets/interleaved-tables/application.rb +126 -0
  31. data/examples/snippets/interleaved-tables/config/database.yml +8 -0
  32. data/examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb +44 -0
  33. data/examples/snippets/interleaved-tables/db/schema.rb +32 -0
  34. data/examples/snippets/interleaved-tables/db/seeds.rb +40 -0
  35. data/examples/snippets/interleaved-tables/models/album.rb +20 -0
  36. data/examples/snippets/interleaved-tables/models/singer.rb +18 -0
  37. data/examples/snippets/interleaved-tables/models/track.rb +28 -0
  38. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +10 -4
  39. data/lib/active_record/connection_adapters/spanner_adapter.rb +64 -31
  40. data/lib/activerecord_spanner_adapter/base.rb +150 -17
  41. data/lib/activerecord_spanner_adapter/connection.rb +1 -1
  42. data/lib/activerecord_spanner_adapter/relation.rb +21 -0
  43. data/lib/activerecord_spanner_adapter/transaction.rb +4 -4
  44. data/lib/activerecord_spanner_adapter/version.rb +1 -1
  45. data/lib/arel/visitors/spanner.rb +10 -0
  46. metadata +25 -7
@@ -8,6 +8,7 @@ require "securerandom"
8
8
  require "google/cloud/spanner"
9
9
  require "spanner_client_ext"
10
10
  require "active_record/connection_adapters/abstract_adapter"
11
+ require "active_record/connection_adapters/abstract/connection_pool"
11
12
  require "active_record/connection_adapters/spanner/database_statements"
12
13
  require "active_record/connection_adapters/spanner/schema_statements"
13
14
  require "active_record/connection_adapters/spanner/schema_cache"
@@ -43,9 +44,9 @@ module ActiveRecord
43
44
  module ConnectionAdapters
44
45
  module AbstractPool
45
46
  def get_schema_cache connection
46
- @schema_cache ||= SpannerSchemaCache.new connection
47
- @schema_cache.connection = connection
48
- @schema_cache
47
+ self.schema_cache ||= SpannerSchemaCache.new connection
48
+ schema_cache.connection = connection
49
+ schema_cache
49
50
  end
50
51
  end
51
52
 
@@ -178,41 +179,73 @@ module ActiveRecord
178
179
  Arel::Visitors::Spanner.new self
179
180
  end
180
181
 
181
- private
182
+ def build_insert_sql insert
183
+ if current_spanner_transaction&.isolation == :buffered_mutations
184
+ raise "ActiveRecordSpannerAdapter does not support insert_sql with buffered_mutations transaction."
185
+ end
182
186
 
183
- def initialize_type_map m = type_map
184
- m.register_type "BOOL", Type::Boolean.new
185
- register_class_with_limit(
186
- m, %r{^BYTES}i, ActiveRecord::Type::Spanner::Bytes
187
- )
188
- m.register_type "DATE", Type::Date.new
189
- m.register_type "FLOAT64", Type::Float.new
190
- m.register_type "NUMERIC", Type::Decimal.new
191
- m.register_type "INT64", Type::Integer.new(limit: 8)
192
- register_class_with_limit m, %r{^STRING}i, Type::String
193
- m.register_type "TIMESTAMP", ActiveRecord::Type::Spanner::Time.new
194
- m.register_type "JSON", ActiveRecord::Type::Json.new
187
+ if insert.skip_duplicates? || insert.update_duplicates?
188
+ raise NotImplementedError, "CloudSpanner does not support skip_duplicates and update_duplicates."
189
+ end
195
190
 
196
- register_array_types m
191
+ values_list, = insert.values_list
192
+ "INSERT #{insert.into} #{values_list}"
197
193
  end
198
194
 
199
- def register_array_types m
200
- m.register_type %r{^ARRAY<BOOL>}i, Type::Spanner::Array.new(Type::Boolean.new)
201
- m.register_type %r{^ARRAY<BYTES\((MAX|d+)\)>}i, Type::Spanner::Array.new(ActiveRecord::Type::Spanner::Bytes.new)
202
- m.register_type %r{^ARRAY<DATE>}i, Type::Spanner::Array.new(Type::Date.new)
203
- m.register_type %r{^ARRAY<FLOAT64>}i, Type::Spanner::Array.new(Type::Float.new)
204
- m.register_type %r{^ARRAY<NUMERIC>}i, Type::Spanner::Array.new(Type::Decimal.new)
205
- m.register_type %r{^ARRAY<INT64>}i, Type::Spanner::Array.new(Type::Integer.new(limit: 8))
206
- m.register_type %r{^ARRAY<STRING\((MAX|d+)\)>}i, Type::Spanner::Array.new(Type::String.new)
207
- m.register_type %r{^ARRAY<TIMESTAMP>}i, Type::Spanner::Array.new(ActiveRecord::Type::Spanner::Time.new)
208
- m.register_type %r{^ARRAY<JSON>}i, Type::Spanner::Array.new(ActiveRecord::Type::Json.new)
195
+ module TypeMapBuilder
196
+ private
197
+
198
+ def initialize_type_map m = type_map
199
+ m.register_type "BOOL", Type::Boolean.new
200
+ register_class_with_limit(
201
+ m, %r{^BYTES}i, ActiveRecord::Type::Spanner::Bytes
202
+ )
203
+ m.register_type "DATE", Type::Date.new
204
+ m.register_type "FLOAT64", Type::Float.new
205
+ m.register_type "NUMERIC", Type::Decimal.new
206
+ m.register_type "INT64", Type::Integer.new(limit: 8)
207
+ register_class_with_limit m, %r{^STRING}i, Type::String
208
+ m.register_type "TIMESTAMP", ActiveRecord::Type::Spanner::Time.new
209
+ m.register_type "JSON", ActiveRecord::Type::Json.new
210
+
211
+ register_array_types m
212
+ end
213
+
214
+ def register_array_types m
215
+ m.register_type %r{^ARRAY<BOOL>}i, Type::Spanner::Array.new(Type::Boolean.new)
216
+ m.register_type %r{^ARRAY<BYTES\((MAX|d+)\)>}i,
217
+ Type::Spanner::Array.new(ActiveRecord::Type::Spanner::Bytes.new)
218
+ m.register_type %r{^ARRAY<DATE>}i, Type::Spanner::Array.new(Type::Date.new)
219
+ m.register_type %r{^ARRAY<FLOAT64>}i, Type::Spanner::Array.new(Type::Float.new)
220
+ m.register_type %r{^ARRAY<NUMERIC>}i, Type::Spanner::Array.new(Type::Decimal.new)
221
+ m.register_type %r{^ARRAY<INT64>}i, Type::Spanner::Array.new(Type::Integer.new(limit: 8))
222
+ m.register_type %r{^ARRAY<STRING\((MAX|d+)\)>}i, Type::Spanner::Array.new(Type::String.new)
223
+ m.register_type %r{^ARRAY<TIMESTAMP>}i, Type::Spanner::Array.new(ActiveRecord::Type::Spanner::Time.new)
224
+ m.register_type %r{^ARRAY<JSON>}i, Type::Spanner::Array.new(ActiveRecord::Type::Json.new)
225
+ end
226
+
227
+ def extract_limit sql_type
228
+ value = /\((.*)\)/.match sql_type
229
+ return unless value
230
+
231
+ value[1] == "MAX" ? "MAX" : value[1].to_i
232
+ end
209
233
  end
210
234
 
211
- def extract_limit sql_type
212
- value = /\((.*)\)/.match sql_type
213
- return unless value
235
+ if ActiveRecord::VERSION::MAJOR >= 7
236
+ class << self
237
+ include TypeMapBuilder
238
+ end
239
+
240
+ TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map m }
241
+
242
+ private
214
243
 
215
- value[1] == "MAX" ? "MAX" : value[1].to_i
244
+ def type_map
245
+ TYPE_MAP
246
+ end
247
+ else
248
+ include TypeMapBuilder
216
249
  end
217
250
 
218
251
  def translate_exception exception, message:, sql:, binds:
@@ -4,6 +4,8 @@
4
4
  # license that can be found in the LICENSE file or at
5
5
  # https://opensource.org/licenses/MIT.
6
6
 
7
+ require "activerecord_spanner_adapter/relation"
8
+
7
9
  module ActiveRecord
8
10
  class TableMetadata # :nodoc:
9
11
  # This attr_reader is private in ActiveRecord 6.0.x and public in 6.1.x. This makes sure it is always available in
@@ -42,35 +44,137 @@ module ActiveRecord
42
44
  end
43
45
 
44
46
  def self._insert_record values
45
- return super unless buffered_mutations?
47
+ return super unless buffered_mutations? || (primary_key && values.is_a?(Hash))
46
48
 
47
- primary_key = self.primary_key
48
- primary_key_value = nil
49
+ return _buffer_record values, :insert if buffered_mutations?
49
50
 
50
- if primary_key && values.is_a?(Hash)
51
- primary_key_value = values[primary_key]
51
+ primary_key_value =
52
+ if primary_key.is_a? Array
53
+ _set_composite_primary_key_values primary_key, values
54
+ else
55
+ _set_single_primary_key_value primary_key, values
56
+ end
57
+ if ActiveRecord::VERSION::MAJOR >= 7
58
+ im = Arel::InsertManager.new arel_table
59
+ im.insert(values.transform_keys { |name| arel_table[name] })
60
+ else
61
+ im = arel_table.compile_insert _substitute_values(values)
62
+ end
63
+ connection.insert(im, "#{self} Create", primary_key || false, primary_key_value)
64
+ end
52
65
 
53
- if !primary_key_value && prefetch_primary_key?
54
- primary_key_value = next_sequence_value
55
- values[primary_key] = primary_key_value
66
+ def self._upsert_record values
67
+ _buffer_record values, :insert_or_update
68
+ end
69
+
70
+ def self.insert_all _attributes, _returning: nil, _unique_by: nil
71
+ raise NotImplementedError, "Cloud Spanner does not support skip_duplicates."
72
+ end
73
+
74
+ def self.insert_all! attributes, returning: nil
75
+ return super unless spanner_adapter?
76
+ return super if active_transaction? && !buffered_mutations?
77
+
78
+ # This might seem inefficient, but is actually not, as it is only buffering a mutation locally.
79
+ # The mutations will be sent as one batch when the transaction is committed.
80
+ if active_transaction?
81
+ attributes.each do |record|
82
+ _insert_record record
83
+ end
84
+ else
85
+ transaction isolation: :buffered_mutations do
86
+ attributes.each do |record|
87
+ _insert_record record
88
+ end
56
89
  end
57
90
  end
91
+ end
92
+
93
+ def self.upsert_all attributes, returning: nil, unique_by: nil
94
+ return super unless spanner_adapter?
95
+ if active_transaction? && !buffered_mutations?
96
+ raise NotImplementedError, "Cloud Spanner does not support upsert using DML. " \
97
+ "Use upsert outside a transaction block or in a transaction " \
98
+ "block with isolation: :buffered_mutations"
99
+ end
100
+
101
+ # This might seem inefficient, but is actually not, as it is only buffering a mutation locally.
102
+ # The mutations will be sent as one batch when the transaction is committed.
103
+ if active_transaction?
104
+ attributes.each do |record|
105
+ _upsert_record record
106
+ end
107
+ else
108
+ transaction isolation: :buffered_mutations do
109
+ attributes.each do |record|
110
+ _upsert_record record
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ def self._buffer_record values, method
117
+ primary_key_value =
118
+ if primary_key.is_a? Array
119
+ _set_composite_primary_key_values primary_key, values
120
+ else
121
+ _set_single_primary_key_value primary_key, values
122
+ end
58
123
 
59
124
  metadata = TableMetadata.new self, arel_table
60
125
  columns, grpc_values = _create_grpc_values_for_insert metadata, values
61
126
 
127
+ write = Google::Cloud::Spanner::V1::Mutation::Write.new(
128
+ table: arel_table.name,
129
+ columns: columns,
130
+ values: [grpc_values.list_value]
131
+ )
62
132
  mutation = Google::Cloud::Spanner::V1::Mutation.new(
63
- insert: Google::Cloud::Spanner::V1::Mutation::Write.new(
64
- table: arel_table.name,
65
- columns: columns,
66
- values: [grpc_values.list_value]
67
- )
133
+ "#{method}": write
68
134
  )
135
+
69
136
  connection.current_spanner_transaction.buffer mutation
70
137
 
71
138
  primary_key_value
72
139
  end
73
140
 
141
+ def self._set_composite_primary_key_values primary_key, values
142
+ primary_key_value = []
143
+ primary_key.each do |col|
144
+ value = values[col]
145
+
146
+ if !value && prefetch_primary_key?
147
+ value =
148
+ if ActiveRecord::VERSION::MAJOR >= 7
149
+ ActiveModel::Attribute.from_database col, next_sequence_value, ActiveModel::Type::BigInteger.new
150
+ else
151
+ next_sequence_value
152
+ end
153
+ values[col] = value
154
+ end
155
+ if value.is_a? ActiveModel::Attribute
156
+ value = value.value
157
+ end
158
+ primary_key_value.append value
159
+ end
160
+ primary_key_value
161
+ end
162
+
163
+ def self._set_single_primary_key_value primary_key, values
164
+ primary_key_value = values[primary_key] || values[primary_key.to_sym]
165
+
166
+ if !primary_key_value && prefetch_primary_key?
167
+ primary_key_value = next_sequence_value
168
+ if ActiveRecord::VERSION::MAJOR >= 7
169
+ values[primary_key] = ActiveModel::Attribute.from_database primary_key, primary_key_value,
170
+ ActiveModel::Type::BigInteger.new
171
+ else
172
+ values[primary_key] = primary_key_value
173
+ end
174
+ end
175
+ primary_key_value
176
+ end
177
+
74
178
  # Deletes all records of this class. This method will use mutations instead of DML if there is no active
75
179
  # transaction, or if the active transaction has been created with the option isolation: :buffered_mutations.
76
180
  def self.delete_all
@@ -87,6 +191,14 @@ module ActiveRecord
87
191
  !(current_transaction.nil? || current_transaction.is_a?(ConnectionAdapters::NullTransaction))
88
192
  end
89
193
 
194
+ def self.unwrap_attribute attr_or_value
195
+ if attr_or_value.is_a? ActiveModel::Attribute
196
+ attr_or_value.value
197
+ else
198
+ attr_or_value
199
+ end
200
+ end
201
+
90
202
  # Updates the given attributes of the object in the database. This method will use mutations instead
91
203
  # of DML if there is no active transaction, or if the active transaction has been created with the option
92
204
  # isolation: :buffered_mutations.
@@ -117,6 +229,7 @@ module ActiveRecord
117
229
  serialized_values = []
118
230
  columns = []
119
231
  values.each_pair do |k, v|
232
+ v = unwrap_attribute v
120
233
  type = metadata.type k
121
234
  serialized_values << (type.method(:serialize).arity < 0 ? type.serialize(v, :mutation) : type.serialize(v))
122
235
  columns << metadata.arel_table[k].name
@@ -168,6 +281,7 @@ module ActiveRecord
168
281
  all_values.each do |h|
169
282
  h.each_pair do |k, v|
170
283
  type = metadata.type k
284
+ v = self.class.unwrap_attribute v
171
285
  has_serialize_options = type.method(:serialize).arity < 0
172
286
  all_serialized_values << (has_serialize_options ? type.serialize(v, :mutation) : type.serialize(v))
173
287
  all_columns << metadata.arel_table[k].name
@@ -222,15 +336,34 @@ module ActiveRecord
222
336
  serialized_values
223
337
  end
224
338
 
225
- def _execute_version_check attempted_action
339
+ def _execute_version_check attempted_action # rubocop:disable Metrics/AbcSize
226
340
  locking_column = self.class.locking_column
227
341
  previous_lock_value = read_attribute_before_type_cast locking_column
228
342
 
343
+ primary_key = self.class.primary_key
344
+ if primary_key.is_a? Array
345
+ pk_sql = ""
346
+ params = {}
347
+ param_types = {}
348
+ id = id_in_database
349
+ primary_key.each_with_index do |col, idx|
350
+ pk_sql.concat "`#{col}`=@id#{idx}"
351
+ pk_sql.concat " AND " if idx < primary_key.length - 1
352
+
353
+ params["id#{idx}"] = id[idx]
354
+ param_types["id#{idx}"] = :INT64
355
+ end
356
+ params["lock_version"] = previous_lock_value
357
+ param_types["lock_version"] = :INT64
358
+ else
359
+ pk_sql = "`#{self.class.primary_key}` = @id"
360
+ params = { "id" => id_in_database, "lock_version" => previous_lock_value }
361
+ param_types = { "id" => :INT64, "lock_version" => :INT64 }
362
+ end
363
+
229
364
  # We need to check the version using a SELECT query, as a mutation cannot include a WHERE clause.
230
365
  sql = "SELECT 1 FROM `#{self.class.arel_table.name}` " \
231
- "WHERE `#{self.class.primary_key}` = @id AND `#{locking_column}` = @lock_version"
232
- params = { "id" => id_in_database, "lock_version" => previous_lock_value }
233
- param_types = { "id" => :INT64, "lock_version" => :INT64 }
366
+ "WHERE #{pk_sql} AND `#{locking_column}` = @lock_version"
234
367
  locked_row = self.class.connection.raw_connection.execute_query sql, params: params, types: param_types
235
368
  raise ActiveRecord::StaleObjectError.new(self, attempted_action) unless locked_row.rows.any?
236
369
  end
@@ -251,7 +251,7 @@ module ActiveRecordSpannerAdapter
251
251
  # transaction fails, as that also means that no transaction id was returned.
252
252
  def create_transaction_after_failed_first_statement original_error
253
253
  transaction = current_transaction.force_begin_read_write
254
- Google::Spanner::V1::TransactionSelector.new id: transaction.transaction_id
254
+ Google::Cloud::Spanner::V1::TransactionSelector.new id: transaction.transaction_id
255
255
  rescue Google::Cloud::Error
256
256
  # Raise the original error if the BeginTransaction RPC also fails.
257
257
  raise original_error
@@ -0,0 +1,21 @@
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
+ module ActiveRecord
8
+ module CpkExtension
9
+ def cpk_subquery stmt
10
+ return super unless spanner_adapter?
11
+ # The composite_primary_key gem will by default generate WHERE clauses using an IN clause with a multi-column
12
+ # sub select, e.g.: SELECT * FROM my_table WHERE (id1, id2) IN (SELECT id1, id2 FROM my_table WHERE ...).
13
+ # This is not supported in Cloud Spanner. Instead, composite_primary_key should generate an EXISTS clause.
14
+ cpk_exists_subquery stmt
15
+ end
16
+ end
17
+
18
+ class Relation
19
+ prepend CpkExtension
20
+ end
21
+ end
@@ -55,9 +55,9 @@ module ActiveRecordSpannerAdapter
55
55
  when :pdml
56
56
  @grpc_transaction = @connection.session.create_pdml
57
57
  else
58
- @begin_transaction_selector = Google::Spanner::V1::TransactionSelector.new \
59
- begin: Google::Spanner::V1::TransactionOptions.new(
60
- read_write: Google::Spanner::V1::TransactionOptions::ReadWrite.new
58
+ @begin_transaction_selector = Google::Cloud::Spanner::V1::TransactionSelector.new \
59
+ begin: Google::Cloud::Spanner::V1::TransactionOptions.new(
60
+ read_write: Google::Cloud::Spanner::V1::TransactionOptions::ReadWrite.new
61
61
  )
62
62
 
63
63
  end
@@ -143,7 +143,7 @@ module ActiveRecordSpannerAdapter
143
143
 
144
144
  # Use the transaction that has been started by a BeginTransaction RPC or returned by a
145
145
  # statement, if present.
146
- return Google::Spanner::V1::TransactionSelector.new id: @grpc_transaction.transaction_id \
146
+ return Google::Cloud::Spanner::V1::TransactionSelector.new id: @grpc_transaction.transaction_id \
147
147
  if @grpc_transaction
148
148
 
149
149
  # Return a transaction selector that will instruct the statement to also start a transaction
@@ -5,5 +5,5 @@
5
5
  # https://opensource.org/licenses/MIT.
6
6
 
7
7
  module ActiveRecordSpannerAdapter
8
- VERSION = "1.0.1".freeze
8
+ VERSION = "1.2.1".freeze
9
9
  end
@@ -98,6 +98,16 @@ module Arel # :nodoc: all
98
98
  end
99
99
  end
100
100
 
101
+ # For ActiveRecord 7.0
102
+ def visit_ActiveModel_Attribute o, collector
103
+ # Do not generate a query parameter if the value should be set to the PENDING_COMMIT_TIMESTAMP(), as that is
104
+ # not supported as a parameter value by Cloud Spanner.
105
+ return collector << "PENDING_COMMIT_TIMESTAMP()" \
106
+ if o.type.is_a?(ActiveRecord::Type::Spanner::Time) && o.value == :commit_timestamp
107
+ collector.add_bind(o, &bind_block)
108
+ end
109
+
110
+ # For ActiveRecord 6.x
101
111
  def visit_Arel_Nodes_BindParam o, collector
102
112
  # Do not generate a query parameter if the value should be set to the PENDING_COMMIT_TIMESTAMP(), as that is
103
113
  # not supported as a parameter value by Cloud Spanner.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-spanner-adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Google LLC
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-04-21 00:00:00.000000000 Z
11
+ date: 2022-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: google-cloud-spanner
@@ -28,16 +28,22 @@ dependencies:
28
28
  name: activerecord
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 6.0.0
34
+ - - "<"
32
35
  - !ruby/object:Gem::Version
33
- version: 6.1.4
36
+ version: '7.1'
34
37
  type: :runtime
35
38
  prerelease: false
36
39
  version_requirements: !ruby/object:Gem::Requirement
37
40
  requirements:
38
- - - "~>"
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 6.0.0
44
+ - - "<"
39
45
  - !ruby/object:Gem::Version
40
- version: 6.1.4
46
+ version: '7.1'
41
47
  - !ruby/object:Gem::Dependency
42
48
  name: autotest-suffix
43
49
  requirement: !ruby/object:Gem::Requirement
@@ -264,6 +270,7 @@ files:
264
270
  - acceptance/cases/migration/rename_column_test.rb
265
271
  - acceptance/cases/models/calculation_query_test.rb
266
272
  - acceptance/cases/models/generated_column_test.rb
273
+ - acceptance/cases/models/insert_all_test.rb
267
274
  - acceptance/cases/models/mutation_test.rb
268
275
  - acceptance/cases/models/query_test.rb
269
276
  - acceptance/cases/sessions/session_not_found_test.rb
@@ -380,6 +387,16 @@ files:
380
387
  - examples/snippets/hints/db/seeds.rb
381
388
  - examples/snippets/hints/models/album.rb
382
389
  - examples/snippets/hints/models/singer.rb
390
+ - examples/snippets/interleaved-tables/README.md
391
+ - examples/snippets/interleaved-tables/Rakefile
392
+ - examples/snippets/interleaved-tables/application.rb
393
+ - examples/snippets/interleaved-tables/config/database.yml
394
+ - examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb
395
+ - examples/snippets/interleaved-tables/db/schema.rb
396
+ - examples/snippets/interleaved-tables/db/seeds.rb
397
+ - examples/snippets/interleaved-tables/models/album.rb
398
+ - examples/snippets/interleaved-tables/models/singer.rb
399
+ - examples/snippets/interleaved-tables/models/track.rb
383
400
  - examples/snippets/migrations/README.md
384
401
  - examples/snippets/migrations/Rakefile
385
402
  - examples/snippets/migrations/application.rb
@@ -485,6 +502,7 @@ files:
485
502
  - lib/activerecord_spanner_adapter/index/column.rb
486
503
  - lib/activerecord_spanner_adapter/information_schema.rb
487
504
  - lib/activerecord_spanner_adapter/primary_key.rb
505
+ - lib/activerecord_spanner_adapter/relation.rb
488
506
  - lib/activerecord_spanner_adapter/table.rb
489
507
  - lib/activerecord_spanner_adapter/table/column.rb
490
508
  - lib/activerecord_spanner_adapter/transaction.rb
@@ -512,7 +530,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
512
530
  - !ruby/object:Gem::Version
513
531
  version: '0'
514
532
  requirements: []
515
- rubygems_version: 3.3.5
533
+ rubygems_version: 3.3.14
516
534
  signing_key:
517
535
  specification_version: 4
518
536
  summary: Rails ActiveRecord connector for Google Spanner Database