activerecord-spanner-adapter 1.0.0 → 1.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -1
  3. data/.github/blunderbuss.yml +2 -0
  4. data/.github/sync-repo-settings.yaml +1 -1
  5. data/.github/workflows/acceptance-tests-on-emulator.yaml +8 -6
  6. data/.github/workflows/acceptance-tests-on-production.yaml +3 -3
  7. data/.github/workflows/ci.yaml +8 -6
  8. data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +9 -5
  9. data/.github/workflows/nightly-acceptance-tests-on-production.yaml +2 -2
  10. data/.github/workflows/nightly-unit-tests.yaml +9 -5
  11. data/.github/workflows/release-please-label.yml +4 -4
  12. data/.github/workflows/release-please.yml +12 -11
  13. data/.github/workflows/rubocop.yaml +4 -4
  14. data/.release-please-manifest.json +3 -0
  15. data/CHANGELOG.md +46 -31
  16. data/CONTRIBUTING.md +1 -1
  17. data/Gemfile +6 -2
  18. data/README.md +2 -1
  19. data/acceptance/cases/interleaved_associations/has_many_associations_using_interleaved_test.rb +12 -8
  20. data/acceptance/cases/models/insert_all_test.rb +150 -0
  21. data/acceptance/cases/transactions/optimistic_locking_test.rb +5 -0
  22. data/acceptance/cases/type/all_types_test.rb +24 -25
  23. data/acceptance/cases/type/json_test.rb +0 -2
  24. data/acceptance/models/album.rb +7 -2
  25. data/acceptance/models/singer.rb +2 -2
  26. data/acceptance/models/track.rb +5 -2
  27. data/acceptance/schema/schema.rb +2 -4
  28. data/acceptance/test_helper.rb +1 -1
  29. data/activerecord-spanner-adapter.gemspec +1 -1
  30. data/examples/rails/README.md +8 -8
  31. data/examples/snippets/interleaved-tables/README.md +164 -0
  32. data/examples/snippets/interleaved-tables/Rakefile +13 -0
  33. data/examples/snippets/interleaved-tables/application.rb +126 -0
  34. data/examples/snippets/interleaved-tables/config/database.yml +8 -0
  35. data/examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb +44 -0
  36. data/examples/snippets/interleaved-tables/db/schema.rb +32 -0
  37. data/examples/snippets/interleaved-tables/db/seeds.rb +40 -0
  38. data/examples/snippets/interleaved-tables/models/album.rb +20 -0
  39. data/examples/snippets/interleaved-tables/models/singer.rb +18 -0
  40. data/examples/snippets/interleaved-tables/models/track.rb +28 -0
  41. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +10 -4
  42. data/lib/active_record/connection_adapters/spanner_adapter.rb +64 -31
  43. data/lib/active_record/type/spanner/array.rb +19 -5
  44. data/lib/activerecord_spanner_adapter/base.rb +150 -17
  45. data/lib/activerecord_spanner_adapter/relation.rb +21 -0
  46. data/lib/activerecord_spanner_adapter/version.rb +1 -1
  47. data/lib/arel/visitors/spanner.rb +10 -0
  48. data/release-please-config.json +19 -0
  49. metadata +28 -7
@@ -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
@@ -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
@@ -5,5 +5,5 @@
5
5
  # https://opensource.org/licenses/MIT.
6
6
 
7
7
  module ActiveRecordSpannerAdapter
8
- VERSION = "1.0.0".freeze
8
+ VERSION = "1.2.0".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.
@@ -0,0 +1,19 @@
1
+ {
2
+ "bump-minor-pre-major": true,
3
+ "bump-patch-for-minor-pre-major": false,
4
+ "draft": false,
5
+ "include-component-in-tag": true,
6
+ "include-v-in-tag": true,
7
+ "prerelease": false,
8
+ "release-type": "ruby-yoshi",
9
+ "skip-github-release": false,
10
+ "separate-pull-requests": true,
11
+ "tag-separator": "/",
12
+ "sequential-calls": true,
13
+ "packages": {
14
+ ".": {
15
+ "component": "activerecord-spanner-adapter",
16
+ "version-file": "lib/activerecord_spanner_adapter/version.rb"
17
+ }
18
+ }
19
+ }
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.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Google LLC
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-12-07 00:00:00.000000000 Z
11
+ date: 2022-08-03 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
@@ -214,6 +220,7 @@ extensions: []
214
220
  extra_rdoc_files: []
215
221
  files:
216
222
  - ".github/CODEOWNERS"
223
+ - ".github/blunderbuss.yml"
217
224
  - ".github/sync-repo-settings.yaml"
218
225
  - ".github/workflows/acceptance-tests-on-emulator.yaml"
219
226
  - ".github/workflows/acceptance-tests-on-production.yaml"
@@ -229,6 +236,7 @@ files:
229
236
  - ".kokoro/release.cfg"
230
237
  - ".kokoro/release.sh"
231
238
  - ".kokoro/trampoline_v2.sh"
239
+ - ".release-please-manifest.json"
232
240
  - ".rubocop.yml"
233
241
  - ".toys/release.rb"
234
242
  - ".trampolinerc"
@@ -262,6 +270,7 @@ files:
262
270
  - acceptance/cases/migration/rename_column_test.rb
263
271
  - acceptance/cases/models/calculation_query_test.rb
264
272
  - acceptance/cases/models/generated_column_test.rb
273
+ - acceptance/cases/models/insert_all_test.rb
265
274
  - acceptance/cases/models/mutation_test.rb
266
275
  - acceptance/cases/models/query_test.rb
267
276
  - acceptance/cases/sessions/session_not_found_test.rb
@@ -378,6 +387,16 @@ files:
378
387
  - examples/snippets/hints/db/seeds.rb
379
388
  - examples/snippets/hints/models/album.rb
380
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
381
400
  - examples/snippets/migrations/README.md
382
401
  - examples/snippets/migrations/Rakefile
383
402
  - examples/snippets/migrations/application.rb
@@ -483,12 +502,14 @@ files:
483
502
  - lib/activerecord_spanner_adapter/index/column.rb
484
503
  - lib/activerecord_spanner_adapter/information_schema.rb
485
504
  - lib/activerecord_spanner_adapter/primary_key.rb
505
+ - lib/activerecord_spanner_adapter/relation.rb
486
506
  - lib/activerecord_spanner_adapter/table.rb
487
507
  - lib/activerecord_spanner_adapter/table/column.rb
488
508
  - lib/activerecord_spanner_adapter/transaction.rb
489
509
  - lib/activerecord_spanner_adapter/version.rb
490
510
  - lib/arel/visitors/spanner.rb
491
511
  - lib/spanner_client_ext.rb
512
+ - release-please-config.json
492
513
  - renovate.json
493
514
  homepage: https://github.com/googleapis/ruby-spanner-activerecord
494
515
  licenses:
@@ -509,7 +530,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
509
530
  - !ruby/object:Gem::Version
510
531
  version: '0'
511
532
  requirements: []
512
- rubygems_version: 3.2.17
533
+ rubygems_version: 3.3.14
513
534
  signing_key:
514
535
  specification_version: 4
515
536
  summary: Rails ActiveRecord connector for Google Spanner Database