activerecord-spanner-adapter 0.6.0 → 1.0.1
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 +1 -1
- data/.github/workflows/acceptance-tests-on-production.yaml +2 -2
- data/.github/workflows/ci.yaml +1 -1
- data/.github/workflows/release-please-label.yml +4 -4
- data/.github/workflows/release-please.yml +10 -9
- data/.github/workflows/rubocop.yaml +2 -2
- data/.release-please-manifest.json +3 -0
- data/.toys/release.rb +2 -2
- data/CHANGELOG.md +46 -23
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +1 -1
- data/README.md +3 -3
- data/acceptance/cases/models/query_test.rb +24 -0
- data/acceptance/cases/type/all_types_test.rb +16 -14
- data/examples/rails/README.md +9 -9
- data/examples/snippets/generated-column/db/schema.rb +3 -3
- data/examples/snippets/hints/README.md +19 -0
- data/examples/snippets/{interleaved-tables → hints}/Rakefile +2 -2
- data/examples/snippets/hints/application.rb +47 -0
- data/examples/snippets/{interleaved-tables → hints}/config/database.yml +0 -0
- data/examples/snippets/hints/db/migrate/01_create_tables.rb +23 -0
- data/examples/snippets/{interleaved-tables → hints}/db/schema.rb +10 -14
- data/examples/snippets/{interleaved-tables → hints}/db/seeds.rb +0 -11
- data/examples/snippets/hints/models/album.rb +9 -0
- data/examples/snippets/hints/models/singer.rb +9 -0
- data/examples/snippets/partitioned-dml/README.md +16 -0
- data/examples/snippets/partitioned-dml/Rakefile +13 -0
- data/examples/snippets/partitioned-dml/application.rb +48 -0
- data/examples/snippets/partitioned-dml/config/database.yml +8 -0
- data/examples/snippets/partitioned-dml/db/migrate/01_create_tables.rb +21 -0
- data/examples/snippets/partitioned-dml/db/schema.rb +26 -0
- data/examples/snippets/partitioned-dml/db/seeds.rb +29 -0
- data/examples/snippets/partitioned-dml/models/album.rb +9 -0
- data/examples/snippets/partitioned-dml/models/singer.rb +9 -0
- data/lib/active_record/connection_adapters/spanner_adapter.rb +1 -1
- data/lib/active_record/tasks/spanner_database_tasks.rb +1 -1
- data/lib/active_record/type/spanner/array.rb +19 -5
- data/lib/activerecord_spanner_adapter/base.rb +31 -10
- 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 +39 -0
- data/lib/spanner_client_ext.rb +4 -0
- data/release-please-config.json +19 -0
- metadata +24 -13
- data/examples/snippets/interleaved-tables/README.md +0 -152
- data/examples/snippets/interleaved-tables/application.rb +0 -109
- data/examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb +0 -44
- 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
@@ -0,0 +1,16 @@
|
|
1
|
+
# Sample - Partitioned DML
|
2
|
+
|
3
|
+
This example shows how to use Partitioned DML with the Spanner ActiveRecord adapter.
|
4
|
+
|
5
|
+
See https://cloud.google.com/spanner/docs/dml-partitioned for more information on Partitioned DML.
|
6
|
+
|
7
|
+
## Running the Sample
|
8
|
+
|
9
|
+
The sample will automatically start a Spanner Emulator in a docker container and execute the sample
|
10
|
+
against that emulator. The emulator will automatically be stopped when the application finishes.
|
11
|
+
|
12
|
+
Run the application with the command
|
13
|
+
|
14
|
+
```bash
|
15
|
+
bundle exec rake run
|
16
|
+
```
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# Copyright 2021 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
|
+
require_relative "../config/environment"
|
8
|
+
require "sinatra/activerecord/rake"
|
9
|
+
|
10
|
+
desc "Sample showing how to work with Partitioned DML in ActiveRecord."
|
11
|
+
task :run do
|
12
|
+
Dir.chdir("..") { sh "bundle exec rake run[partitioned-dml]" }
|
13
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# Copyright 2021 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
|
+
require "io/console"
|
8
|
+
require_relative "../config/environment"
|
9
|
+
require_relative "models/singer"
|
10
|
+
require_relative "models/album"
|
11
|
+
|
12
|
+
class Application
|
13
|
+
def self.run
|
14
|
+
singer_count = Singer.all.count
|
15
|
+
album_count = Album.all.count
|
16
|
+
puts ""
|
17
|
+
puts "Singers in the database: #{singer_count}"
|
18
|
+
puts "Albums in the database: #{album_count}"
|
19
|
+
|
20
|
+
puts ""
|
21
|
+
puts "Deleting all albums in the database using Partitioned DML"
|
22
|
+
# Note that a Partitioned DML transaction can contain ONLY ONE DML statement.
|
23
|
+
# If we want to delete all data in two different tables, we need to do so in two different PDML transactions.
|
24
|
+
Album.transaction isolation: :pdml do
|
25
|
+
count = Album.delete_all
|
26
|
+
puts "Deleted #{count} albums"
|
27
|
+
end
|
28
|
+
|
29
|
+
puts ""
|
30
|
+
puts "Deleting all singers in the database using Partitioned DML"
|
31
|
+
Singer.transaction isolation: :pdml do
|
32
|
+
count = Singer.delete_all
|
33
|
+
puts "Deleted #{count} singers"
|
34
|
+
end
|
35
|
+
|
36
|
+
singer_count = Singer.all.count
|
37
|
+
album_count = Album.all.count
|
38
|
+
puts ""
|
39
|
+
puts "Singers in the database: #{singer_count}"
|
40
|
+
puts "Albums in the database: #{album_count}"
|
41
|
+
|
42
|
+
puts ""
|
43
|
+
puts "Press any key to end the application"
|
44
|
+
STDIN.getch
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
Application.run
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Copyright 2021 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
|
+
class CreateTables < ActiveRecord::Migration[6.0]
|
8
|
+
def change
|
9
|
+
connection.ddl_batch do
|
10
|
+
create_table :singers do |t|
|
11
|
+
t.string :first_name, limit: 100
|
12
|
+
t.string :last_name, limit: 200, null: false
|
13
|
+
end
|
14
|
+
|
15
|
+
create_table :albums do |t|
|
16
|
+
t.string :title
|
17
|
+
t.references :singer, index: false, foreign_key: true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# This file is auto-generated from the current state of the database. Instead
|
2
|
+
# of editing this file, please use the migrations feature of Active Record to
|
3
|
+
# incrementally modify your database, and then regenerate this schema definition.
|
4
|
+
#
|
5
|
+
# This file is the source Rails uses to define your schema when running `bin/rails
|
6
|
+
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
|
7
|
+
# be faster and is potentially less error prone than running all of your
|
8
|
+
# migrations from scratch. Old migrations may fail to apply correctly if those
|
9
|
+
# migrations use external dependencies or application code.
|
10
|
+
#
|
11
|
+
# It's strongly recommended that you check this file into your version control system.
|
12
|
+
|
13
|
+
ActiveRecord::Schema.define(version: 1) do
|
14
|
+
|
15
|
+
create_table "albums", id: { limit: 8 }, force: :cascade do |t|
|
16
|
+
t.string "title"
|
17
|
+
t.integer "singer_id", limit: 8
|
18
|
+
end
|
19
|
+
|
20
|
+
create_table "singers", id: { limit: 8 }, force: :cascade do |t|
|
21
|
+
t.string "first_name", limit: 100
|
22
|
+
t.string "last_name", limit: 200, null: false
|
23
|
+
end
|
24
|
+
|
25
|
+
add_foreign_key "albums", "singers"
|
26
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Copyright 2021 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
|
+
require_relative "../../config/environment.rb"
|
8
|
+
require_relative "../models/singer"
|
9
|
+
require_relative "../models/album"
|
10
|
+
|
11
|
+
first_names = %w[Pete Alice John Ethel Trudy Naomi Wendy Ruben Thomas Elly]
|
12
|
+
last_names = %w[Wendelson Allison Peterson Johnson Henderson Ericsson Aronson Tennet Courtou]
|
13
|
+
|
14
|
+
adjectives = %w[daily happy blue generous cooked bad open]
|
15
|
+
nouns = %w[windows potatoes bank street tree glass bottle]
|
16
|
+
|
17
|
+
# This ensures all the records are inserted using one read/write transaction that will use mutations instead of DML.
|
18
|
+
ActiveRecord::Base.transaction isolation: :buffered_mutations do
|
19
|
+
singers = []
|
20
|
+
5.times do
|
21
|
+
singers << Singer.create(first_name: first_names.sample, last_name: last_names.sample)
|
22
|
+
end
|
23
|
+
|
24
|
+
albums = []
|
25
|
+
20.times do
|
26
|
+
singer = singers.sample
|
27
|
+
albums << Album.create(title: "#{adjectives.sample} #{nouns.sample}", singer: singer)
|
28
|
+
end
|
29
|
+
end
|
@@ -198,7 +198,7 @@ module ActiveRecord
|
|
198
198
|
|
199
199
|
def register_array_types m
|
200
200
|
m.register_type %r{^ARRAY<BOOL>}i, Type::Spanner::Array.new(Type::Boolean.new)
|
201
|
-
m.register_type %r{^ARRAY<BYTES\((MAX|d+)\)>}i, Type::Spanner::Array.new(Type::
|
201
|
+
m.register_type %r{^ARRAY<BYTES\((MAX|d+)\)>}i, Type::Spanner::Array.new(ActiveRecord::Type::Spanner::Bytes.new)
|
202
202
|
m.register_type %r{^ARRAY<DATE>}i, Type::Spanner::Array.new(Type::Date.new)
|
203
203
|
m.register_type %r{^ARRAY<FLOAT64>}i, Type::Spanner::Array.new(Type::Float.new)
|
204
204
|
m.register_type %r{^ARRAY<NUMERIC>}i, Type::Spanner::Array.new(Type::Decimal.new)
|
@@ -18,7 +18,7 @@ module ActiveRecord
|
|
18
18
|
@connection.create_database
|
19
19
|
rescue Google::Cloud::Error => error
|
20
20
|
if error.instance_of? Google::Cloud::AlreadyExistsError
|
21
|
-
raise ActiveRecord::
|
21
|
+
raise ActiveRecord::DatabaseAlreadyExists
|
22
22
|
end
|
23
23
|
|
24
24
|
raise error
|
@@ -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
|
@@ -15,7 +15,17 @@ module ActiveRecord
|
|
15
15
|
# Creates an object (or multiple objects) and saves it to the database. This method will use mutations instead
|
16
16
|
# of DML if there is no active transaction, or if the active transaction has been created with the option
|
17
17
|
# isolation: :buffered_mutations.
|
18
|
+
def self.create! attributes = nil, &block
|
19
|
+
return super unless spanner_adapter?
|
20
|
+
return super if active_transaction?
|
21
|
+
|
22
|
+
transaction isolation: :buffered_mutations do
|
23
|
+
return super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
18
27
|
def self.create attributes = nil, &block
|
28
|
+
return super unless spanner_adapter?
|
19
29
|
return super if active_transaction?
|
20
30
|
|
21
31
|
transaction isolation: :buffered_mutations do
|
@@ -23,8 +33,16 @@ module ActiveRecord
|
|
23
33
|
end
|
24
34
|
end
|
25
35
|
|
36
|
+
def self.spanner_adapter?
|
37
|
+
connection.adapter_name == "spanner"
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.buffered_mutations?
|
41
|
+
spanner_adapter? && connection&.current_spanner_transaction&.isolation == :buffered_mutations
|
42
|
+
end
|
43
|
+
|
26
44
|
def self._insert_record values
|
27
|
-
return super unless
|
45
|
+
return super unless buffered_mutations?
|
28
46
|
|
29
47
|
primary_key = self.primary_key
|
30
48
|
primary_key_value = nil
|
@@ -48,7 +66,7 @@ module ActiveRecord
|
|
48
66
|
values: [grpc_values.list_value]
|
49
67
|
)
|
50
68
|
)
|
51
|
-
|
69
|
+
connection.current_spanner_transaction.buffer mutation
|
52
70
|
|
53
71
|
primary_key_value
|
54
72
|
end
|
@@ -56,6 +74,7 @@ module ActiveRecord
|
|
56
74
|
# Deletes all records of this class. This method will use mutations instead of DML if there is no active
|
57
75
|
# transaction, or if the active transaction has been created with the option isolation: :buffered_mutations.
|
58
76
|
def self.delete_all
|
77
|
+
return super unless spanner_adapter?
|
59
78
|
return super if active_transaction?
|
60
79
|
|
61
80
|
transaction isolation: :buffered_mutations do
|
@@ -72,7 +91,8 @@ module ActiveRecord
|
|
72
91
|
# of DML if there is no active transaction, or if the active transaction has been created with the option
|
73
92
|
# isolation: :buffered_mutations.
|
74
93
|
def update attributes
|
75
|
-
return super
|
94
|
+
return super unless self.class.spanner_adapter?
|
95
|
+
return super if self.class.active_transaction?
|
76
96
|
|
77
97
|
transaction isolation: :buffered_mutations do
|
78
98
|
return super
|
@@ -83,7 +103,8 @@ module ActiveRecord
|
|
83
103
|
# of DML if there is no active transaction, or if the active transaction has been created with the option
|
84
104
|
# isolation: :buffered_mutations.
|
85
105
|
def destroy
|
86
|
-
return super
|
106
|
+
return super unless self.class.spanner_adapter?
|
107
|
+
return super if self.class.active_transaction?
|
87
108
|
|
88
109
|
transaction isolation: :buffered_mutations do
|
89
110
|
return super
|
@@ -110,7 +131,7 @@ module ActiveRecord
|
|
110
131
|
private_class_method :_create_grpc_values_for_insert
|
111
132
|
|
112
133
|
def _update_row attribute_names, attempted_action = "update"
|
113
|
-
return super unless
|
134
|
+
return super unless self.class.buffered_mutations?
|
114
135
|
|
115
136
|
if locking_enabled?
|
116
137
|
_execute_version_check attempted_action
|
@@ -129,7 +150,7 @@ module ActiveRecord
|
|
129
150
|
values: [grpc_values.list_value]
|
130
151
|
)
|
131
152
|
)
|
132
|
-
|
153
|
+
self.class.connection.current_spanner_transaction.buffer mutation
|
133
154
|
1 # Affected rows
|
134
155
|
end
|
135
156
|
|
@@ -161,13 +182,13 @@ module ActiveRecord
|
|
161
182
|
end
|
162
183
|
|
163
184
|
def destroy_row
|
164
|
-
return super unless
|
185
|
+
return super unless self.class.buffered_mutations?
|
165
186
|
|
166
187
|
_delete_row
|
167
188
|
end
|
168
189
|
|
169
190
|
def _delete_row
|
170
|
-
return super unless
|
191
|
+
return super unless self.class.buffered_mutations?
|
171
192
|
if locking_enabled?
|
172
193
|
_execute_version_check "destroy"
|
173
194
|
end
|
@@ -186,7 +207,7 @@ module ActiveRecord
|
|
186
207
|
key_set: { keys: [list_value] }
|
187
208
|
)
|
188
209
|
)
|
189
|
-
|
210
|
+
self.class.connection.current_spanner_transaction.buffer mutation
|
190
211
|
1 # Affected rows
|
191
212
|
end
|
192
213
|
|
@@ -210,7 +231,7 @@ module ActiveRecord
|
|
210
231
|
"WHERE `#{self.class.primary_key}` = @id AND `#{locking_column}` = @lock_version"
|
211
232
|
params = { "id" => id_in_database, "lock_version" => previous_lock_value }
|
212
233
|
param_types = { "id" => :INT64, "lock_version" => :INT64 }
|
213
|
-
locked_row =
|
234
|
+
locked_row = self.class.connection.raw_connection.execute_query sql, params: params, types: param_types
|
214
235
|
raise ActiveRecord::StaleObjectError.new(self, attempted_action) unless locked_row.rows.any?
|
215
236
|
end
|
216
237
|
end
|
@@ -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
|
@@ -30,28 +30,37 @@ module ActiveRecordSpannerAdapter
|
|
30
30
|
@mutations << mutation
|
31
31
|
end
|
32
32
|
|
33
|
+
# Begins the transaction.
|
34
|
+
#
|
35
|
+
# Read-only and PDML transactions are started by executing a BeginTransaction RPC.
|
36
|
+
# Read/write transactions are not really started by this method, and instead a
|
37
|
+
# transaction selector is prepared that will be included with the first statement
|
38
|
+
# on the transaction.
|
33
39
|
def begin
|
34
40
|
raise "Nested transactions are not allowed" if @state != :INITIALIZED
|
35
41
|
begin
|
36
|
-
@
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
@connection.session.create_snapshot strong: true
|
45
|
-
else
|
46
|
-
raise "Invalid snapshot argument: #{@isolation}"
|
47
|
-
end
|
48
|
-
when :read_only
|
49
|
-
@connection.session.create_snapshot strong: true
|
50
|
-
when :pdml
|
51
|
-
@connection.session.create_pdml
|
42
|
+
case @isolation
|
43
|
+
when Hash
|
44
|
+
if @isolation[:timestamp]
|
45
|
+
@grpc_transaction = @connection.session.create_snapshot timestamp: @isolation[:timestamp]
|
46
|
+
elsif @isolation[:staleness]
|
47
|
+
@grpc_transaction = @connection.session.create_snapshot staleness: @isolation[:staleness]
|
48
|
+
elsif @isolation[:strong]
|
49
|
+
@grpc_transaction = @connection.session.create_snapshot strong: true
|
52
50
|
else
|
53
|
-
@
|
51
|
+
raise "Invalid snapshot argument: #{@isolation}"
|
54
52
|
end
|
53
|
+
when :read_only
|
54
|
+
@grpc_transaction = @connection.session.create_snapshot strong: true
|
55
|
+
when :pdml
|
56
|
+
@grpc_transaction = @connection.session.create_pdml
|
57
|
+
else
|
58
|
+
@begin_transaction_selector = Google::Spanner::V1::TransactionSelector.new \
|
59
|
+
begin: Google::Spanner::V1::TransactionOptions.new(
|
60
|
+
read_write: Google::Spanner::V1::TransactionOptions::ReadWrite.new
|
61
|
+
)
|
62
|
+
|
63
|
+
end
|
55
64
|
@state = :STARTED
|
56
65
|
rescue Google::Cloud::NotFoundError => e
|
57
66
|
if @connection.session_not_found? e
|
@@ -66,6 +75,12 @@ module ActiveRecordSpannerAdapter
|
|
66
75
|
end
|
67
76
|
end
|
68
77
|
|
78
|
+
# Forces a BeginTransaction RPC for a read/write transaction. This is used by a
|
79
|
+
# connection if the first statement of a transaction failed.
|
80
|
+
def force_begin_read_write
|
81
|
+
@grpc_transaction = @connection.session.create_transaction
|
82
|
+
end
|
83
|
+
|
69
84
|
def next_sequence_number
|
70
85
|
@sequence_number += 1 if @committable
|
71
86
|
end
|
@@ -74,7 +89,10 @@ module ActiveRecordSpannerAdapter
|
|
74
89
|
raise "This transaction is not active" unless active?
|
75
90
|
|
76
91
|
begin
|
77
|
-
|
92
|
+
# Start a transaction with an explicit BeginTransaction RPC if the transaction only contains mutations.
|
93
|
+
force_begin_read_write if @committable && !@mutations.empty? && !@grpc_transaction
|
94
|
+
|
95
|
+
@connection.session.commit_transaction @grpc_transaction, @mutations if @committable && @grpc_transaction
|
78
96
|
@state = :COMMITTED
|
79
97
|
rescue Google::Cloud::NotFoundError => e
|
80
98
|
if @connection.session_not_found? e
|
@@ -93,7 +111,7 @@ module ActiveRecordSpannerAdapter
|
|
93
111
|
def rollback
|
94
112
|
# Allow rollback after abort and/or a failed commit.
|
95
113
|
raise "This transaction is not active" unless active? || @state == :FAILED || @state == :ABORTED
|
96
|
-
if active?
|
114
|
+
if active? && @grpc_transaction
|
97
115
|
# We do a shoot-and-forget rollback here, as the error that caused the transaction to be rolled back could
|
98
116
|
# also have invalidated the transaction (e.g. `Session not found`). If the rollback fails for any other
|
99
117
|
# reason, we also do not need to retry it or propagate the error to the application, as the transaction will
|
@@ -113,11 +131,24 @@ module ActiveRecordSpannerAdapter
|
|
113
131
|
@state = :ABORTED
|
114
132
|
end
|
115
133
|
|
134
|
+
# Sets the underlying gRPC transaction to use for this Transaction.
|
135
|
+
# This is used for queries/DML statements that inlined the BeginTransaction option and returned
|
136
|
+
# a transaction in the metadata.
|
137
|
+
def grpc_transaction= grpc
|
138
|
+
@grpc_transaction = Google::Cloud::Spanner::Transaction.from_grpc grpc, @connection.session
|
139
|
+
end
|
140
|
+
|
116
141
|
def transaction_selector
|
117
142
|
return unless active?
|
118
143
|
|
119
|
-
|
120
|
-
|
144
|
+
# Use the transaction that has been started by a BeginTransaction RPC or returned by a
|
145
|
+
# statement, if present.
|
146
|
+
return Google::Spanner::V1::TransactionSelector.new id: @grpc_transaction.transaction_id \
|
147
|
+
if @grpc_transaction
|
148
|
+
|
149
|
+
# Return a transaction selector that will instruct the statement to also start a transaction
|
150
|
+
# and return its id as a side effect.
|
151
|
+
@begin_transaction_selector
|
121
152
|
end
|
122
153
|
end
|
123
154
|
end
|
@@ -14,11 +14,24 @@ module Arel # :nodoc: all
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
+
class StatementHint
|
18
|
+
attr_reader :value
|
19
|
+
|
20
|
+
def initialize value
|
21
|
+
@value = value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
17
25
|
class Spanner < Arel::Visitors::ToSql
|
18
26
|
def compile node, collector = Arel::Collectors::SQLString.new
|
19
27
|
collector.class.module_eval { attr_accessor :hints }
|
28
|
+
collector.class.module_eval { attr_accessor :table_hints }
|
29
|
+
collector.class.module_eval { attr_accessor :join_hints }
|
20
30
|
collector.hints = {}
|
31
|
+
collector.table_hints = {}
|
32
|
+
collector.join_hints = {}
|
21
33
|
sql, binds = accept(node, collector).value
|
34
|
+
sql = collector.hints[:statement_hint].value + sql if collector.hints[:statement_hint]
|
22
35
|
binds << collector.hints[:staleness] if collector.hints[:staleness]
|
23
36
|
[sql, binds]
|
24
37
|
end
|
@@ -32,9 +45,25 @@ module Arel # :nodoc: all
|
|
32
45
|
BIND_BLOCK
|
33
46
|
end
|
34
47
|
|
48
|
+
def visit_table_hint v, collector
|
49
|
+
value = v.delete_prefix("table_hint:").strip
|
50
|
+
# TODO: This does not support FORCE_INDEX hints that reference an index that contains '@{' in the name.
|
51
|
+
start_of_hint_index = value.rindex "@{"
|
52
|
+
table_name = value[0, start_of_hint_index]
|
53
|
+
table_hint = value[start_of_hint_index, value.length]
|
54
|
+
collector.table_hints[table_name] = table_hint if table_name && table_hint
|
55
|
+
end
|
56
|
+
|
57
|
+
def visit_statement_hint v, collector
|
58
|
+
collector.hints[:statement_hint] = \
|
59
|
+
StatementHint.new v.delete_prefix("statement_hint:")
|
60
|
+
end
|
61
|
+
|
35
62
|
# rubocop:disable Naming/MethodName
|
36
63
|
def visit_Arel_Nodes_OptimizerHints o, collector
|
37
64
|
o.expr.each do |v|
|
65
|
+
visit_table_hint v, collector if v.start_with? "table_hint:"
|
66
|
+
visit_statement_hint v, collector if v.start_with? "statement_hint:"
|
38
67
|
if v.start_with? "max_staleness:"
|
39
68
|
collector.hints[:staleness] = \
|
40
69
|
StalenessHint.new max_staleness: v.delete_prefix("max_staleness:").to_f
|
@@ -59,6 +88,16 @@ module Arel # :nodoc: all
|
|
59
88
|
collector
|
60
89
|
end
|
61
90
|
|
91
|
+
def visit_Arel_Table o, collector
|
92
|
+
return super unless collector.table_hints[o.name]
|
93
|
+
if o.table_alias
|
94
|
+
collector << quote_table_name(o.name) << collector.table_hints[o.name] \
|
95
|
+
<< " " << quote_table_name(o.table_alias)
|
96
|
+
else
|
97
|
+
collector << quote_table_name(o.name) << collector.table_hints[o.name]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
62
101
|
def visit_Arel_Nodes_BindParam o, collector
|
63
102
|
# Do not generate a query parameter if the value should be set to the PENDING_COMMIT_TIMESTAMP(), as that is
|
64
103
|
# not supported as a parameter value by Cloud Spanner.
|