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.
- checksums.yaml +4 -4
- data/.github/workflows/acceptance-tests-on-emulator.yaml +1 -1
- data/.github/workflows/ci.yaml +1 -1
- data/.kokoro/release.cfg +2 -2
- data/.kokoro/release.sh +1 -2
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +16 -0
- data/Gemfile +1 -1
- data/README.md +25 -23
- data/acceptance/cases/models/default_value_test.rb +2 -2
- data/acceptance/cases/tasks/database_tasks_test.rb +72 -75
- data/acceptance/cases/transactions/read_write_transactions_test.rb +10 -4
- data/acceptance/test_helper.rb +33 -8
- data/activerecord-spanner-adapter.gemspec +3 -4
- data/examples/snippets/Rakefile +1 -0
- data/examples/snippets/auto-generated-primary-key/README.md +140 -0
- data/examples/snippets/auto-generated-primary-key/Rakefile +13 -0
- data/examples/snippets/auto-generated-primary-key/application.rb +86 -0
- data/examples/snippets/auto-generated-primary-key/config/database.yml +10 -0
- data/examples/snippets/auto-generated-primary-key/db/migrate/01_create_tables.rb +29 -0
- data/examples/snippets/auto-generated-primary-key/db/seeds.rb +31 -0
- data/examples/snippets/auto-generated-primary-key/models/album.rb +11 -0
- data/examples/snippets/auto-generated-primary-key/models/singer.rb +11 -0
- data/examples/snippets/isolation-level/README.md +39 -0
- data/examples/snippets/isolation-level/Rakefile +13 -0
- data/examples/snippets/isolation-level/application.rb +36 -0
- data/examples/snippets/isolation-level/config/database.yml +10 -0
- data/examples/snippets/isolation-level/db/migrate/01_create_tables.rb +22 -0
- data/examples/snippets/isolation-level/db/seeds.rb +25 -0
- data/examples/snippets/isolation-level/models/album.rb +9 -0
- data/examples/snippets/isolation-level/models/singer.rb +9 -0
- data/lib/active_record/connection_adapters/spanner/column.rb +4 -0
- data/lib/active_record/connection_adapters/spanner/database_statements.rb +1 -1
- data/lib/active_record/connection_adapters/spanner/schema_creation.rb +13 -2
- data/lib/active_record/connection_adapters/spanner/schema_statements.rb +5 -5
- data/lib/active_record/connection_adapters/spanner/type_metadata.rb +7 -3
- data/lib/active_record/connection_adapters/spanner_adapter.rb +28 -1
- data/lib/activerecord_spanner_adapter/base.rb +49 -16
- data/lib/activerecord_spanner_adapter/connection.rb +6 -1
- data/lib/activerecord_spanner_adapter/information_schema.rb +4 -2
- data/lib/activerecord_spanner_adapter/table/column.rb +4 -1
- data/lib/activerecord_spanner_adapter/transaction.rb +12 -2
- data/lib/activerecord_spanner_adapter/version.rb +1 -1
- 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,
|
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 =
|
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
|
77
|
-
|
78
|
-
|
79
|
-
|
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.
|
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
|
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
|
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.
|
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-
|
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
|
+
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.
|
25
|
+
version: '2.25'
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
|
-
name:
|
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.
|
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.
|
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: '
|
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: '
|
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.
|
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: []
|