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.
- 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
|