activerecord-spanner-adapter 0.7.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|