activerecord-spanner-adapter 0.7.0 → 1.1.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/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 +48 -26
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +2 -1
- data/README.md +3 -3
- data/acceptance/cases/models/insert_all_test.rb +150 -0
- data/acceptance/cases/type/all_types_test.rb +24 -25
- data/acceptance/cases/type/json_test.rb +0 -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/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 +72 -5
- data/lib/activerecord_spanner_adapter/connection.rb +46 -20
- data/lib/activerecord_spanner_adapter/information_schema.rb +2 -1
- data/lib/activerecord_spanner_adapter/transaction.rb +52 -21
- data/lib/activerecord_spanner_adapter/version.rb +1 -1
- data/lib/arel/visitors/spanner.rb +10 -0
- data/lib/spanner_client_ext.rb +4 -0
- data/release-please-config.json +19 -0
- metadata +17 -17
- data/examples/snippets/interleaved-tables/README.md +0 -152
- data/examples/snippets/interleaved-tables/Rakefile +0 -13
- data/examples/snippets/interleaved-tables/application.rb +0 -109
- data/examples/snippets/interleaved-tables/config/database.yml +0 -8
- data/examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb +0 -44
- data/examples/snippets/interleaved-tables/db/schema.rb +0 -32
- data/examples/snippets/interleaved-tables/db/seeds.rb +0 -40
- data/examples/snippets/interleaved-tables/models/album.rb +0 -15
- data/examples/snippets/interleaved-tables/models/singer.rb +0 -20
- data/examples/snippets/interleaved-tables/models/track.rb +0 -25
@@ -30,19 +30,19 @@ module ActiveRecord
|
|
30
30
|
AllTypes.create col_string: "string", col_int64: 100, col_float64: 3.14, col_numeric: 6.626, col_bool: true,
|
31
31
|
col_bytes: StringIO.new("bytes"), col_date: ::Date.new(2021, 6, 23),
|
32
32
|
col_timestamp: ::Time.new(2021, 6, 23, 17, 8, 21, "+02:00"),
|
33
|
-
col_json:
|
33
|
+
col_json: { kind: "user_renamed", change: %w[jack john]},
|
34
34
|
col_array_string: ["string1", nil, "string2"],
|
35
|
-
col_array_int64: [100, nil, 200],
|
36
|
-
col_array_float64: [3.14, nil, 2.0/3.0],
|
37
|
-
col_array_numeric: [6.626, nil, 3.20],
|
38
|
-
col_array_bool: [true, nil, false],
|
35
|
+
col_array_int64: [100, nil, 200, "300"],
|
36
|
+
col_array_float64: [3.14, nil, 2.0/3.0, "3.14"],
|
37
|
+
col_array_numeric: [6.626, nil, 3.20, "400"],
|
38
|
+
col_array_bool: [true, nil, false, "false"],
|
39
39
|
col_array_bytes: [StringIO.new("bytes1"), nil, StringIO.new("bytes2")],
|
40
|
-
col_array_date: [::Date.new(2021, 6, 23), nil, ::Date.new(2021, 6, 24)],
|
40
|
+
col_array_date: [::Date.new(2021, 6, 23), nil, ::Date.new(2021, 6, 24), "2021-06-25"],
|
41
41
|
col_array_timestamp: [::Time.new(2021, 6, 23, 17, 8, 21, "+02:00"), nil, \
|
42
|
-
::Time.new(2021, 6, 24, 17, 8, 21, "+02:00")],
|
43
|
-
col_array_json:
|
44
|
-
|
45
|
-
{
|
42
|
+
::Time.new(2021, 6, 24, 17, 8, 21, "+02:00"), "2021-06-25 17:08:21 +02:00"],
|
43
|
+
col_array_json: [{ kind: "user_renamed", change: %w[jack john]}, nil, \
|
44
|
+
{ kind: "user_renamed", change: %w[alice meredith]},
|
45
|
+
"{\"kind\":\"user_renamed\",\"change\":[\"bob\",\"carol\"]}"]
|
46
46
|
end
|
47
47
|
|
48
48
|
def test_create_record
|
@@ -66,24 +66,25 @@ module ActiveRecord
|
|
66
66
|
assert_equal ::Date.new(2021, 6, 23), record.col_date
|
67
67
|
assert_equal ::Time.new(2021, 6, 23, 17, 8, 21, "+02:00").utc, record.col_timestamp.utc
|
68
68
|
assert_equal ({"kind" => "user_renamed", "change" => %w[jack john]}),
|
69
|
-
record.col_json
|
69
|
+
record.col_json
|
70
70
|
|
71
71
|
assert_equal ["string1", nil, "string2"], record.col_array_string
|
72
|
-
assert_equal [100, nil, 200], record.col_array_int64
|
73
|
-
assert_equal [3.14, nil, 2.0/3.0], record.col_array_float64
|
74
|
-
assert_equal [6.626, nil, 3.20], record.col_array_numeric
|
75
|
-
assert_equal [true, nil, false], record.col_array_bool
|
72
|
+
assert_equal [100, nil, 200, 300], record.col_array_int64
|
73
|
+
assert_equal [3.14, nil, 2.0/3.0, 3.14], record.col_array_float64
|
74
|
+
assert_equal [6.626, nil, 3.20, 400], record.col_array_numeric
|
75
|
+
assert_equal [true, nil, false, false], record.col_array_bool
|
76
76
|
assert_equal [StringIO.new("bytes1"), nil, StringIO.new("bytes2")].map { |bytes| bytes&.read },
|
77
77
|
record.col_array_bytes.map { |bytes| bytes&.read }
|
78
|
-
assert_equal [::Date.new(2021, 6, 23), nil, ::Date.new(2021, 6, 24)], record.col_array_date
|
78
|
+
assert_equal [::Date.new(2021, 6, 23), nil, ::Date.new(2021, 6, 24), ::Date.new(2021, 06, 25)], record.col_array_date
|
79
79
|
assert_equal [::Time.new(2021, 6, 23, 17, 8, 21, "+02:00"), \
|
80
80
|
nil, \
|
81
|
-
::Time.new(2021, 6, 24, 17, 8, 21, "+02:00")].map { |timestamp| timestamp&.utc },
|
81
|
+
::Time.new(2021, 6, 24, 17, 8, 21, "+02:00"), ::Time.new(2021, 6, 25, 17, 8, 21, "+02:00")].map { |timestamp| timestamp&.utc },
|
82
82
|
record.col_array_timestamp.map { |timestamp| timestamp&.utc}
|
83
83
|
assert_equal [{"kind" => "user_renamed", "change" => %w[jack john]}, \
|
84
84
|
nil, \
|
85
|
-
{"kind" => "user_renamed", "change" => %w[alice meredith]}
|
86
|
-
|
85
|
+
{"kind" => "user_renamed", "change" => %w[alice meredith]}, \
|
86
|
+
"{\"kind\":\"user_renamed\",\"change\":[\"bob\",\"carol\"]}"],
|
87
|
+
record.col_array_json
|
87
88
|
end
|
88
89
|
end
|
89
90
|
|
@@ -98,7 +99,7 @@ module ActiveRecord
|
|
98
99
|
col_bool: false, col_bytes: StringIO.new("new bytes"),
|
99
100
|
col_date: ::Date.new(2021, 6, 28),
|
100
101
|
col_timestamp: ::Time.new(2021, 6, 28, 11, 22, 21, "+02:00"),
|
101
|
-
col_json:
|
102
|
+
col_json: { kind: "user_created", change: %w[jack alice]},
|
102
103
|
col_array_string: ["new string 1", "new string 2"],
|
103
104
|
col_array_int64: [300, 200, 100],
|
104
105
|
col_array_float64: [1.1, 2.2, 3.3],
|
@@ -107,9 +108,7 @@ module ActiveRecord
|
|
107
108
|
col_array_bytes: [StringIO.new("new bytes 1"), StringIO.new("new bytes 2")],
|
108
109
|
col_array_date: [::Date.new(2021, 6, 28)],
|
109
110
|
col_array_timestamp: [::Time.utc(2020, 12, 31, 0, 0, 0)],
|
110
|
-
col_array_json:
|
111
|
-
[""] : \
|
112
|
-
[{ kind: "user_created", change: %w[jack alice]}]
|
111
|
+
col_array_json: [{ kind: "user_created", change: %w[jack alice]}]
|
113
112
|
end
|
114
113
|
|
115
114
|
# Verify that the record was updated.
|
@@ -123,7 +122,7 @@ module ActiveRecord
|
|
123
122
|
assert_equal ::Date.new(2021, 6, 28), record.col_date
|
124
123
|
assert_equal ::Time.new(2021, 6, 28, 11, 22, 21, "+02:00").utc, record.col_timestamp.utc
|
125
124
|
assert_equal ({"kind" => "user_created", "change" => %w[jack alice]}),
|
126
|
-
|
125
|
+
record.col_json
|
127
126
|
|
128
127
|
assert_equal ["new string 1", "new string 2"], record.col_array_string
|
129
128
|
assert_equal [300, 200, 100], record.col_array_int64
|
@@ -135,7 +134,7 @@ module ActiveRecord
|
|
135
134
|
assert_equal [::Date.new(2021, 6, 28)], record.col_array_date
|
136
135
|
assert_equal [::Time.utc(2020, 12, 31, 0, 0, 0)], record.col_array_timestamp.map(&:utc)
|
137
136
|
assert_equal [{"kind" => "user_created", "change" => %w[jack alice]}],
|
138
|
-
record.col_array_json
|
137
|
+
record.col_array_json
|
139
138
|
end
|
140
139
|
end
|
141
140
|
|
data/acceptance/schema/schema.rb
CHANGED
@@ -17,8 +17,7 @@ ActiveRecord::Schema.define do
|
|
17
17
|
t.column :col_bytes, :binary
|
18
18
|
t.column :col_date, :date
|
19
19
|
t.column :col_timestamp, :datetime
|
20
|
-
t.column :col_json, :json
|
21
|
-
t.column :col_json, :string if ENV["SPANNER_EMULATOR_HOST"]
|
20
|
+
t.column :col_json, :json
|
22
21
|
|
23
22
|
t.column :col_array_string, :string, array: true
|
24
23
|
t.column :col_array_int64, :bigint, array: true
|
@@ -28,8 +27,7 @@ ActiveRecord::Schema.define do
|
|
28
27
|
t.column :col_array_bytes, :binary, array: true
|
29
28
|
t.column :col_array_date, :date, array: true
|
30
29
|
t.column :col_array_timestamp, :datetime, array: true
|
31
|
-
t.column :col_array_json, :json, array: true
|
32
|
-
t.column :col_array_json, :string, array: true if ENV["SPANNER_EMULATOR_HOST"]
|
30
|
+
t.column :col_array_json, :json, array: true
|
33
31
|
end
|
34
32
|
|
35
33
|
create_table :firms do |t|
|
data/acceptance/test_helper.rb
CHANGED
@@ -25,7 +25,7 @@ Gem::Specification.new do |spec|
|
|
25
25
|
spec.required_ruby_version = ">= 2.5"
|
26
26
|
|
27
27
|
spec.add_dependency "google-cloud-spanner", "~> 2.10"
|
28
|
-
spec.add_runtime_dependency "activerecord", "
|
28
|
+
spec.add_runtime_dependency "activerecord", [">= 6.0.0", "< 7.1"]
|
29
29
|
|
30
30
|
spec.add_development_dependency "autotest-suffix", "~> 1.1"
|
31
31
|
spec.add_development_dependency "bundler", "~> 2.0"
|
data/examples/rails/README.md
CHANGED
@@ -142,17 +142,17 @@ Replace `[PROJECT_ID]` with the project id you are currently using.
|
|
142
142
|
### Create database
|
143
143
|
|
144
144
|
You now can run the following command to create the database:
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
145
|
+
|
146
|
+
```shell
|
147
|
+
./bin/rails db:create
|
148
|
+
```
|
149
|
+
|
150
|
+
You should see output like the following: `Created database 'blog_dev'`
|
151
|
+
|
152
152
|
### Generate a Model and apply the migration
|
153
153
|
1. Use the model generato to define a model:
|
154
154
|
```shell
|
155
|
-
bin/rails generate model Article title:string body:text
|
155
|
+
./bin/rails generate model Article title:string body:text
|
156
156
|
```
|
157
157
|
1. Apply the migration:
|
158
158
|
```shell
|
@@ -10,14 +10,20 @@ module ActiveRecord
|
|
10
10
|
class SchemaCreation < SchemaCreation
|
11
11
|
private
|
12
12
|
|
13
|
-
# rubocop:disable Naming/MethodName, Metrics/AbcSize
|
13
|
+
# rubocop:disable Naming/MethodName, Metrics/AbcSize, Metrics/PerceivedComplexity
|
14
14
|
|
15
15
|
def visit_TableDefinition o
|
16
16
|
create_sql = +"CREATE TABLE #{quote_table_name o.name} "
|
17
17
|
statements = o.columns.map { |c| accept c }
|
18
18
|
|
19
|
-
|
20
|
-
|
19
|
+
if ActiveRecord::VERSION::MAJOR >= 7
|
20
|
+
o.foreign_keys.each do |fk|
|
21
|
+
statements << accept(fk)
|
22
|
+
end
|
23
|
+
else
|
24
|
+
o.foreign_keys.each do |to_table, options|
|
25
|
+
statements << foreign_key_in_create(o.name, to_table, options)
|
26
|
+
end
|
21
27
|
end
|
22
28
|
|
23
29
|
create_sql << "(#{statements.join ', '}) " if statements.any?
|
@@ -106,7 +112,7 @@ module ActiveRecord
|
|
106
112
|
sql
|
107
113
|
end
|
108
114
|
|
109
|
-
# rubocop:enable Naming/MethodName, Metrics/AbcSize
|
115
|
+
# rubocop:enable Naming/MethodName, Metrics/AbcSize, Metrics/PerceivedComplexity
|
110
116
|
|
111
117
|
def add_column_options! sql, options
|
112
118
|
if options[:null] == false || options[:primary_key] == true
|
@@ -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
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
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
|
-
|
184
|
-
|
185
|
-
|
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
|
-
|
191
|
+
values_list, = insert.values_list
|
192
|
+
"INSERT #{insert.into} #{values_list}"
|
197
193
|
end
|
198
194
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
m
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
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
|
-
|
212
|
-
|
213
|
-
|
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
|
-
|
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:
|
@@ -15,15 +15,29 @@ module ActiveRecord
|
|
15
15
|
@element_type = element_type
|
16
16
|
end
|
17
17
|
|
18
|
-
def
|
18
|
+
def cast value
|
19
19
|
return super if value.nil?
|
20
|
-
return super unless @element_type.is_a? Type::Decimal
|
21
20
|
return super unless value.respond_to? :map
|
22
21
|
|
23
|
-
# Convert a decimal (NUMERIC) array to a String array to prevent it from being encoded as a FLOAT64 array.
|
24
22
|
value.map do |v|
|
25
|
-
|
26
|
-
|
23
|
+
@element_type.cast v
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def serialize value
|
28
|
+
return super if value.nil?
|
29
|
+
return super unless value.respond_to? :map
|
30
|
+
|
31
|
+
if @element_type.is_a? ActiveRecord::Type::Decimal
|
32
|
+
# Convert a decimal (NUMERIC) array to a String array to prevent it from being encoded as a FLOAT64 array.
|
33
|
+
value.map do |v|
|
34
|
+
next if v.nil?
|
35
|
+
v.to_s
|
36
|
+
end
|
37
|
+
else
|
38
|
+
value.map do |v|
|
39
|
+
@element_type.serialize v
|
40
|
+
end
|
27
41
|
end
|
28
42
|
end
|
29
43
|
end
|
@@ -41,14 +41,69 @@ module ActiveRecord
|
|
41
41
|
spanner_adapter? && connection&.current_spanner_transaction&.isolation == :buffered_mutations
|
42
42
|
end
|
43
43
|
|
44
|
+
def self.insert_all _attributes, _returning: nil, _unique_by: nil
|
45
|
+
raise NotImplementedError, "Cloud Spanner does not support skip_duplicates."
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.insert_all! attributes, returning: nil
|
49
|
+
return super unless spanner_adapter?
|
50
|
+
return super if active_transaction? && !buffered_mutations?
|
51
|
+
|
52
|
+
# This might seem inefficient, but is actually not, as it is only buffering a mutation locally.
|
53
|
+
# The mutations will be sent as one batch when the transaction is committed.
|
54
|
+
if active_transaction?
|
55
|
+
attributes.each do |record|
|
56
|
+
_insert_record record
|
57
|
+
end
|
58
|
+
else
|
59
|
+
transaction isolation: :buffered_mutations do
|
60
|
+
attributes.each do |record|
|
61
|
+
_insert_record record
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.upsert_all attributes, returning: nil, unique_by: nil
|
68
|
+
return super unless spanner_adapter?
|
69
|
+
if active_transaction? && !buffered_mutations?
|
70
|
+
raise NotImplementedError, "Cloud Spanner does not support upsert using DML. " \
|
71
|
+
"Use upsert outside a transaction block or in a transaction " \
|
72
|
+
"block with isolation: :buffered_mutations"
|
73
|
+
end
|
74
|
+
|
75
|
+
# This might seem inefficient, but is actually not, as it is only buffering a mutation locally.
|
76
|
+
# The mutations will be sent as one batch when the transaction is committed.
|
77
|
+
if active_transaction?
|
78
|
+
attributes.each do |record|
|
79
|
+
_upsert_record record
|
80
|
+
end
|
81
|
+
else
|
82
|
+
transaction isolation: :buffered_mutations do
|
83
|
+
attributes.each do |record|
|
84
|
+
_upsert_record record
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
44
90
|
def self._insert_record values
|
45
91
|
return super unless buffered_mutations?
|
46
92
|
|
93
|
+
_buffer_record values, :insert
|
94
|
+
end
|
95
|
+
|
96
|
+
def self._upsert_record values
|
97
|
+
_buffer_record values, :insert_or_update
|
98
|
+
end
|
99
|
+
|
100
|
+
def self._buffer_record values, method
|
47
101
|
primary_key = self.primary_key
|
48
102
|
primary_key_value = nil
|
49
103
|
|
50
104
|
if primary_key && values.is_a?(Hash)
|
51
105
|
primary_key_value = values[primary_key]
|
106
|
+
primary_key_value ||= values[:"#{primary_key}"]
|
52
107
|
|
53
108
|
if !primary_key_value && prefetch_primary_key?
|
54
109
|
primary_key_value = next_sequence_value
|
@@ -59,13 +114,15 @@ module ActiveRecord
|
|
59
114
|
metadata = TableMetadata.new self, arel_table
|
60
115
|
columns, grpc_values = _create_grpc_values_for_insert metadata, values
|
61
116
|
|
117
|
+
write = Google::Cloud::Spanner::V1::Mutation::Write.new(
|
118
|
+
table: arel_table.name,
|
119
|
+
columns: columns,
|
120
|
+
values: [grpc_values.list_value]
|
121
|
+
)
|
62
122
|
mutation = Google::Cloud::Spanner::V1::Mutation.new(
|
63
|
-
|
64
|
-
table: arel_table.name,
|
65
|
-
columns: columns,
|
66
|
-
values: [grpc_values.list_value]
|
67
|
-
)
|
123
|
+
"#{method}": write
|
68
124
|
)
|
125
|
+
|
69
126
|
connection.current_spanner_transaction.buffer mutation
|
70
127
|
|
71
128
|
primary_key_value
|
@@ -87,6 +144,14 @@ module ActiveRecord
|
|
87
144
|
!(current_transaction.nil? || current_transaction.is_a?(ConnectionAdapters::NullTransaction))
|
88
145
|
end
|
89
146
|
|
147
|
+
def self.unwrap_attribute attr_or_value
|
148
|
+
if attr_or_value.is_a? ActiveModel::Attribute
|
149
|
+
attr_or_value.value
|
150
|
+
else
|
151
|
+
attr_or_value
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
90
155
|
# Updates the given attributes of the object in the database. This method will use mutations instead
|
91
156
|
# of DML if there is no active transaction, or if the active transaction has been created with the option
|
92
157
|
# isolation: :buffered_mutations.
|
@@ -117,6 +182,7 @@ module ActiveRecord
|
|
117
182
|
serialized_values = []
|
118
183
|
columns = []
|
119
184
|
values.each_pair do |k, v|
|
185
|
+
v = unwrap_attribute v
|
120
186
|
type = metadata.type k
|
121
187
|
serialized_values << (type.method(:serialize).arity < 0 ? type.serialize(v, :mutation) : type.serialize(v))
|
122
188
|
columns << metadata.arel_table[k].name
|
@@ -168,6 +234,7 @@ module ActiveRecord
|
|
168
234
|
all_values.each do |h|
|
169
235
|
h.each_pair do |k, v|
|
170
236
|
type = metadata.type k
|
237
|
+
v = self.class.unwrap_attribute v
|
171
238
|
has_serialize_options = type.method(:serialize).arity < 0
|
172
239
|
all_serialized_values << (has_serialize_options ? type.serialize(v, :mutation) : type.serialize(v))
|
173
240
|
all_columns << metadata.arel_table[k].name
|
@@ -208,27 +208,53 @@ module ActiveRecordSpannerAdapter
|
|
208
208
|
self.current_transaction = nil
|
209
209
|
end
|
210
210
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
211
|
+
selector = transaction_selector || single_use_selector
|
212
|
+
execute_sql_request sql, converted_params, types, selector
|
213
|
+
end
|
214
|
+
|
215
|
+
def execute_sql_request sql, converted_params, types, selector
|
216
|
+
res = session.execute_query \
|
217
|
+
sql,
|
218
|
+
params: converted_params,
|
219
|
+
types: types,
|
220
|
+
transaction: selector,
|
221
|
+
seqno: (current_transaction&.next_sequence_number)
|
222
|
+
current_transaction.grpc_transaction = res.metadata.transaction \
|
223
|
+
if current_transaction && res&.metadata&.transaction
|
224
|
+
res
|
225
|
+
rescue Google::Cloud::AbortedError
|
226
|
+
# Mark the current transaction as aborted to prevent any unnecessary further requests on the transaction.
|
227
|
+
current_transaction&.mark_aborted
|
228
|
+
raise
|
229
|
+
rescue Google::Cloud::NotFoundError => e
|
230
|
+
if session_not_found?(e) || transaction_not_found?(e)
|
231
|
+
reset!
|
232
|
+
# Force a retry of the entire transaction if this statement was executed as part of a transaction.
|
233
|
+
# Otherwise, just retry the statement itself.
|
234
|
+
raise_aborted_err if current_transaction&.active?
|
235
|
+
retry
|
236
|
+
end
|
237
|
+
raise
|
238
|
+
rescue Google::Cloud::Error => e
|
239
|
+
# Check if it was the first statement in a transaction that included a BeginTransaction
|
240
|
+
# option in the request. If so, execute an explicit BeginTransaction and then retry the
|
241
|
+
# request without the BeginTransaction option.
|
242
|
+
if current_transaction && selector&.begin&.read_write
|
243
|
+
selector = create_transaction_after_failed_first_statement e
|
244
|
+
retry
|
231
245
|
end
|
246
|
+
# It was not the first statement, so propagate the error.
|
247
|
+
raise
|
248
|
+
end
|
249
|
+
|
250
|
+
# Creates a transaction using a BeginTransaction RPC. This is used if the first statement of a
|
251
|
+
# transaction fails, as that also means that no transaction id was returned.
|
252
|
+
def create_transaction_after_failed_first_statement original_error
|
253
|
+
transaction = current_transaction.force_begin_read_write
|
254
|
+
Google::Spanner::V1::TransactionSelector.new id: transaction.transaction_id
|
255
|
+
rescue Google::Cloud::Error
|
256
|
+
# Raise the original error if the BeginTransaction RPC also fails.
|
257
|
+
raise original_error
|
232
258
|
end
|
233
259
|
|
234
260
|
# Transactions
|
@@ -62,7 +62,8 @@ module ActiveRecordSpannerAdapter
|
|
62
62
|
end
|
63
63
|
|
64
64
|
def table_columns table_name, column_name: nil
|
65
|
-
sql = +"SELECT COLUMN_NAME, SPANNER_TYPE, IS_NULLABLE,
|
65
|
+
sql = +"SELECT COLUMN_NAME, SPANNER_TYPE, IS_NULLABLE,"
|
66
|
+
sql << " CAST(COLUMN_DEFAULT AS STRING) AS COLUMN_DEFAULT, ORDINAL_POSITION"
|
66
67
|
sql << " FROM INFORMATION_SCHEMA.COLUMNS"
|
67
68
|
sql << " WHERE TABLE_NAME=%<table_name>s"
|
68
69
|
sql << " AND COLUMN_NAME=%<column_name>s" if column_name
|