activerecord-spanner-adapter 1.0.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/CODEOWNERS +1 -1
- data/.github/blunderbuss.yml +2 -0
- data/.github/sync-repo-settings.yaml +1 -1
- data/.github/workflows/acceptance-tests-on-emulator.yaml +8 -6
- data/.github/workflows/acceptance-tests-on-production.yaml +3 -3
- data/.github/workflows/ci.yaml +8 -6
- data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +9 -5
- data/.github/workflows/nightly-acceptance-tests-on-production.yaml +2 -2
- data/.github/workflows/nightly-unit-tests.yaml +9 -5
- data/.github/workflows/release-please-label.yml +4 -4
- data/.github/workflows/release-please.yml +12 -11
- data/.github/workflows/rubocop.yaml +4 -4
- data/.release-please-manifest.json +3 -0
- data/CHANGELOG.md +46 -31
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +6 -2
- data/README.md +2 -1
- data/acceptance/cases/interleaved_associations/has_many_associations_using_interleaved_test.rb +12 -8
- data/acceptance/cases/models/insert_all_test.rb +150 -0
- data/acceptance/cases/transactions/optimistic_locking_test.rb +5 -0
- data/acceptance/cases/type/all_types_test.rb +24 -25
- data/acceptance/cases/type/json_test.rb +0 -2
- data/acceptance/models/album.rb +7 -2
- data/acceptance/models/singer.rb +2 -2
- data/acceptance/models/track.rb +5 -2
- data/acceptance/schema/schema.rb +2 -4
- data/acceptance/test_helper.rb +1 -1
- data/activerecord-spanner-adapter.gemspec +1 -1
- data/examples/rails/README.md +8 -8
- data/examples/snippets/interleaved-tables/README.md +164 -0
- data/examples/snippets/interleaved-tables/Rakefile +13 -0
- data/examples/snippets/interleaved-tables/application.rb +126 -0
- data/examples/snippets/interleaved-tables/config/database.yml +8 -0
- data/examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb +44 -0
- data/examples/snippets/interleaved-tables/db/schema.rb +32 -0
- data/examples/snippets/interleaved-tables/db/seeds.rb +40 -0
- data/examples/snippets/interleaved-tables/models/album.rb +20 -0
- data/examples/snippets/interleaved-tables/models/singer.rb +18 -0
- data/examples/snippets/interleaved-tables/models/track.rb +28 -0
- data/lib/active_record/connection_adapters/spanner/schema_creation.rb +10 -4
- data/lib/active_record/connection_adapters/spanner_adapter.rb +64 -31
- data/lib/active_record/type/spanner/array.rb +19 -5
- data/lib/activerecord_spanner_adapter/base.rb +150 -17
- data/lib/activerecord_spanner_adapter/relation.rb +21 -0
- data/lib/activerecord_spanner_adapter/version.rb +1 -1
- data/lib/arel/visitors/spanner.rb +10 -0
- data/release-please-config.json +19 -0
- 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
|
-
|
48
|
-
primary_key_value = nil
|
49
|
+
return _buffer_record values, :insert if buffered_mutations?
|
49
50
|
|
50
|
-
|
51
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
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
|
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
|
@@ -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.
|
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:
|
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:
|
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:
|
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.
|
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
|