activerecord-spanner-adapter 1.5.0 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/acceptance-tests-on-emulator.yaml +1 -1
- data/.github/workflows/acceptance-tests-on-production.yaml +5 -3
- data/.github/workflows/ci.yaml +1 -1
- data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +1 -1
- data/.github/workflows/nightly-acceptance-tests-on-production.yaml +5 -3
- data/.github/workflows/nightly-unit-tests.yaml +1 -1
- data/.github/workflows/release-please-label.yml +1 -1
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +14 -0
- data/Gemfile +5 -2
- data/README.md +10 -10
- data/acceptance/cases/interleaved_associations/has_many_associations_using_interleaved_test.rb +6 -0
- data/acceptance/cases/migration/change_schema_test.rb +19 -3
- data/acceptance/cases/migration/schema_dumper_test.rb +10 -1
- data/acceptance/cases/models/insert_all_test.rb +22 -0
- data/acceptance/cases/models/interleave_test.rb +6 -0
- data/acceptance/cases/tasks/database_tasks_test.rb +340 -2
- data/acceptance/cases/transactions/optimistic_locking_test.rb +6 -0
- data/acceptance/cases/transactions/read_write_transactions_test.rb +24 -0
- data/acceptance/models/table_with_sequence.rb +10 -0
- data/acceptance/schema/schema.rb +65 -19
- data/acceptance/test_helper.rb +1 -1
- data/activerecord-spanner-adapter.gemspec +1 -1
- data/examples/snippets/bit-reversed-sequence/README.md +103 -0
- data/examples/snippets/bit-reversed-sequence/Rakefile +13 -0
- data/examples/snippets/bit-reversed-sequence/application.rb +68 -0
- data/examples/snippets/bit-reversed-sequence/config/database.yml +8 -0
- data/examples/snippets/bit-reversed-sequence/db/migrate/01_create_tables.rb +33 -0
- data/examples/snippets/bit-reversed-sequence/db/schema.rb +31 -0
- data/examples/snippets/bit-reversed-sequence/db/seeds.rb +31 -0
- data/examples/snippets/bit-reversed-sequence/models/album.rb +11 -0
- data/examples/snippets/bit-reversed-sequence/models/singer.rb +15 -0
- data/examples/snippets/interleaved-tables/README.md +44 -53
- data/examples/snippets/interleaved-tables/Rakefile +2 -2
- data/examples/snippets/interleaved-tables/application.rb +2 -2
- data/examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb +12 -18
- data/examples/snippets/interleaved-tables/db/schema.rb +9 -7
- data/examples/snippets/interleaved-tables/db/seeds.rb +1 -1
- data/examples/snippets/interleaved-tables/models/album.rb +3 -7
- data/examples/snippets/interleaved-tables/models/singer.rb +1 -1
- data/examples/snippets/interleaved-tables/models/track.rb +6 -7
- data/examples/snippets/interleaved-tables-before-7.1/README.md +167 -0
- data/examples/snippets/interleaved-tables-before-7.1/Rakefile +13 -0
- data/examples/snippets/interleaved-tables-before-7.1/application.rb +126 -0
- data/examples/snippets/interleaved-tables-before-7.1/config/database.yml +8 -0
- data/examples/snippets/interleaved-tables-before-7.1/db/migrate/01_create_tables.rb +44 -0
- data/examples/snippets/interleaved-tables-before-7.1/db/schema.rb +37 -0
- data/examples/snippets/interleaved-tables-before-7.1/db/seeds.rb +40 -0
- data/examples/snippets/interleaved-tables-before-7.1/models/album.rb +20 -0
- data/examples/snippets/interleaved-tables-before-7.1/models/singer.rb +18 -0
- data/examples/snippets/interleaved-tables-before-7.1/models/track.rb +28 -0
- data/examples/snippets/query-logs/README.md +43 -0
- data/examples/snippets/query-logs/Rakefile +13 -0
- data/examples/snippets/query-logs/application.rb +63 -0
- data/examples/snippets/query-logs/config/database.yml +8 -0
- data/examples/snippets/query-logs/db/migrate/01_create_tables.rb +21 -0
- data/examples/snippets/query-logs/db/schema.rb +31 -0
- data/examples/snippets/query-logs/db/seeds.rb +24 -0
- data/examples/snippets/query-logs/models/album.rb +9 -0
- data/examples/snippets/query-logs/models/singer.rb +9 -0
- data/lib/active_record/connection_adapters/spanner/column.rb +13 -0
- data/lib/active_record/connection_adapters/spanner/database_statements.rb +144 -35
- data/lib/active_record/connection_adapters/spanner/schema_cache.rb +3 -21
- data/lib/active_record/connection_adapters/spanner/schema_creation.rb +11 -0
- data/lib/active_record/connection_adapters/spanner/schema_definitions.rb +4 -0
- data/lib/active_record/connection_adapters/spanner/schema_statements.rb +3 -2
- data/lib/active_record/connection_adapters/spanner_adapter.rb +28 -9
- data/lib/activerecord_spanner_adapter/base.rb +58 -21
- data/lib/activerecord_spanner_adapter/information_schema.rb +33 -24
- data/lib/activerecord_spanner_adapter/primary_key.rb +1 -1
- data/lib/activerecord_spanner_adapter/table/column.rb +4 -9
- data/lib/activerecord_spanner_adapter/version.rb +1 -1
- data/lib/arel/visitors/spanner.rb +3 -1
- metadata +33 -4
@@ -0,0 +1,31 @@
|
|
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[7.1].define(version: 1) do
|
14
|
+
connection.start_batch_ddl
|
15
|
+
|
16
|
+
create_table "albums", force: :cascade do |t|
|
17
|
+
t.string "title"
|
18
|
+
t.integer "singer_id", limit: 8
|
19
|
+
end
|
20
|
+
|
21
|
+
create_table "singers", force: :cascade do |t|
|
22
|
+
t.string "first_name"
|
23
|
+
t.string "last_name"
|
24
|
+
end
|
25
|
+
|
26
|
+
add_foreign_key "albums", "singers"
|
27
|
+
connection.run_batch
|
28
|
+
rescue
|
29
|
+
abort_batch
|
30
|
+
raise
|
31
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# Copyright 2023 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 = ["Pete", "Alice", "John", "Ethel", "Trudy", "Naomi", "Wendy", "Ruben", "Thomas", "Elly"]
|
12
|
+
last_names = ["Wendelson", "Allison", "Peterson", "Johnson", "Henderson", "Ericsson", "Aronson", "Tennet", "Courtou"]
|
13
|
+
|
14
|
+
adjectives = ["daily", "happy", "blue", "generous", "cooked", "bad", "open"]
|
15
|
+
nouns = ["windows", "potatoes", "bank", "street", "tree", "glass", "bottle"]
|
16
|
+
|
17
|
+
5.times do
|
18
|
+
Singer.create first_name: first_names.sample, last_name: last_names.sample
|
19
|
+
end
|
20
|
+
|
21
|
+
20.times do
|
22
|
+
singer_id = Singer.all.sample.id
|
23
|
+
Album.create title: "#{adjectives.sample} #{nouns.sample}", singer_id: singer_id
|
24
|
+
end
|
@@ -10,6 +10,15 @@ module ActiveRecord
|
|
10
10
|
module ConnectionAdapters
|
11
11
|
module Spanner
|
12
12
|
class Column < ConnectionAdapters::Column
|
13
|
+
# rubocop:disable Style/MethodDefParentheses
|
14
|
+
def initialize(name, default, sql_type_metadata = nil, null = true,
|
15
|
+
default_function = nil, collation: nil, comment: nil,
|
16
|
+
primary_key: false, **)
|
17
|
+
# rubocop:enable Style/MethodDefParentheses
|
18
|
+
super
|
19
|
+
@primary_key = primary_key
|
20
|
+
end
|
21
|
+
|
13
22
|
def has_default? # rubocop:disable Naming/PredicateName
|
14
23
|
super && !virtual?
|
15
24
|
end
|
@@ -17,6 +26,10 @@ module ActiveRecord
|
|
17
26
|
def virtual?
|
18
27
|
sql_type_metadata.generated
|
19
28
|
end
|
29
|
+
|
30
|
+
def primary_key?
|
31
|
+
@primary_key
|
32
|
+
end
|
20
33
|
end
|
21
34
|
end
|
22
35
|
end
|
@@ -6,14 +6,34 @@
|
|
6
6
|
|
7
7
|
# frozen_string_literal: true
|
8
8
|
|
9
|
+
require "active_record/gem_version"
|
10
|
+
|
9
11
|
module ActiveRecord
|
10
12
|
module ConnectionAdapters
|
11
13
|
module Spanner
|
12
14
|
module DatabaseStatements
|
15
|
+
VERSION_7_1_0 = Gem::Version.create "7.1.0"
|
16
|
+
RequestOptions = Google::Cloud::Spanner::V1::RequestOptions
|
17
|
+
|
13
18
|
# DDL, DML and DQL Statements
|
14
19
|
|
15
20
|
def execute sql, name = nil, binds = []
|
21
|
+
internal_execute sql, name, binds
|
22
|
+
end
|
23
|
+
|
24
|
+
def internal_exec_query sql, name = "SQL", binds = [], prepare: false, async: false
|
25
|
+
result = internal_execute sql, name, binds, prepare: prepare, async: async
|
26
|
+
ActiveRecord::Result.new(
|
27
|
+
result.fields.keys.map(&:to_s), result.rows.map(&:values)
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
def internal_execute sql, name = "SQL", binds = [],
|
32
|
+
prepare: false, async: false # rubocop:disable Lint/UnusedMethodArgument
|
16
33
|
statement_type = sql_statement_type sql
|
34
|
+
# Call `transform` to invoke any query transformers that might have been registered.
|
35
|
+
sql = transform sql
|
36
|
+
append_request_tag_from_query_logs sql, binds
|
17
37
|
|
18
38
|
if preventing_writes? && [:dml, :ddl].include?(statement_type)
|
19
39
|
raise ActiveRecord::ReadOnlyError(
|
@@ -24,49 +44,127 @@ module ActiveRecord
|
|
24
44
|
if statement_type == :ddl
|
25
45
|
execute_ddl sql
|
26
46
|
else
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
47
|
+
execute_query_or_dml statement_type, sql, name, binds
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def execute_query_or_dml statement_type, sql, name, binds
|
52
|
+
transaction_required = statement_type == :dml
|
53
|
+
materialize_transactions
|
54
|
+
|
55
|
+
# First process and remove any hints in the binds that indicate that
|
56
|
+
# a different read staleness should be used than the default.
|
57
|
+
staleness_hint = binds.find { |b| b.is_a? Arel::Visitors::StalenessHint }
|
58
|
+
if staleness_hint
|
59
|
+
selector = Google::Cloud::Spanner::Session.single_use_transaction staleness_hint.value
|
60
|
+
binds.delete staleness_hint
|
61
|
+
end
|
62
|
+
request_options = binds.find { |b| b.is_a? RequestOptions }
|
63
|
+
if request_options
|
64
|
+
binds.delete request_options
|
65
|
+
end
|
66
|
+
|
67
|
+
log_args = [sql, name]
|
68
|
+
log_args.concat [binds, type_casted_binds(binds)] if log_statement_binds
|
41
69
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
if transaction_required
|
49
|
-
transaction do
|
50
|
-
@connection.execute_query sql, params: params, types: types, request_options: request_options
|
51
|
-
end
|
52
|
-
else
|
53
|
-
@connection.execute_query sql, params: params, types: types, single_use_selector: selector,
|
54
|
-
request_options: request_options
|
70
|
+
log(*log_args) do
|
71
|
+
types, params = to_types_and_params binds
|
72
|
+
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
|
73
|
+
if transaction_required
|
74
|
+
transaction do
|
75
|
+
@connection.execute_query sql, params: params, types: types, request_options: request_options
|
55
76
|
end
|
77
|
+
else
|
78
|
+
@connection.execute_query sql, params: params, types: types, single_use_selector: selector,
|
79
|
+
request_options: request_options
|
56
80
|
end
|
57
81
|
end
|
58
82
|
end
|
59
83
|
end
|
60
84
|
|
61
|
-
def
|
62
|
-
|
85
|
+
def append_request_tag_from_query_logs sql, binds
|
86
|
+
legacy_formatter_prefix = "/*request_tag:true,"
|
87
|
+
sql_commenter_prefix = "/*request_tag='true',"
|
88
|
+
if sql.start_with? legacy_formatter_prefix
|
89
|
+
append_request_tag_from_query_logs_with_format sql, binds, legacy_formatter_prefix
|
90
|
+
elsif sql.start_with? sql_commenter_prefix
|
91
|
+
append_request_tag_from_query_logs_with_format sql, binds, sql_commenter_prefix
|
92
|
+
end
|
63
93
|
end
|
64
94
|
|
65
|
-
def
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
95
|
+
def append_request_tag_from_query_logs_with_format sql, binds, prefix
|
96
|
+
end_of_comment = sql.index "*/", prefix.length
|
97
|
+
return unless end_of_comment
|
98
|
+
|
99
|
+
request_tag = sql[prefix.length, end_of_comment - prefix.length]
|
100
|
+
options = binds.find { |bind| bind.is_a? RequestOptions } || RequestOptions.new
|
101
|
+
if options.request_tag == ""
|
102
|
+
options.request_tag = request_tag
|
103
|
+
else
|
104
|
+
options.request_tag += "," + request_tag
|
105
|
+
end
|
106
|
+
|
107
|
+
binds.append options
|
108
|
+
end
|
109
|
+
|
110
|
+
# The method signatures for executing queries and DML statements changed between Rails 7.0 and 7.1.
|
111
|
+
|
112
|
+
if ActiveRecord.gem_version >= VERSION_7_1_0
|
113
|
+
def sql_for_insert sql, pk, binds, returning
|
114
|
+
if pk && !_has_pk_binding(pk, binds)
|
115
|
+
# Add the primary key to the columns that should be returned if there is no value specified for it.
|
116
|
+
returning ||= []
|
117
|
+
returning |= if pk.respond_to? :each
|
118
|
+
pk
|
119
|
+
else
|
120
|
+
[pk]
|
121
|
+
end
|
122
|
+
end
|
123
|
+
if returning&.any?
|
124
|
+
returning_columns_statement = returning.map { |c| quote_column_name c }.join(", ")
|
125
|
+
sql = "#{sql} THEN RETURN #{returning_columns_statement}"
|
126
|
+
end
|
127
|
+
|
128
|
+
[sql, binds]
|
129
|
+
end
|
130
|
+
|
131
|
+
def query sql, name = nil
|
132
|
+
exec_query sql, name
|
133
|
+
end
|
134
|
+
else # ActiveRecord.gem_version < VERSION_7_1_0
|
135
|
+
def query sql, name = nil
|
136
|
+
exec_query sql, name
|
137
|
+
end
|
138
|
+
|
139
|
+
def exec_query sql, name = "SQL", binds = [], prepare: false # rubocop:disable Lint/UnusedMethodArgument
|
140
|
+
result = execute sql, name, binds
|
141
|
+
ActiveRecord::Result.new(
|
142
|
+
result.fields.keys.map(&:to_s), result.rows.map(&:values)
|
143
|
+
)
|
144
|
+
end
|
145
|
+
|
146
|
+
def sql_for_insert sql, pk, binds
|
147
|
+
if pk && !_has_pk_binding(pk, binds)
|
148
|
+
# Add the primary key to the columns that should be returned if there is no value specified for it.
|
149
|
+
returning_columns_statement = if pk.respond_to? :each
|
150
|
+
pk.map { |c| quote_column_name c }.join(", ")
|
151
|
+
else
|
152
|
+
quote_column_name pk
|
153
|
+
end
|
154
|
+
sql = "#{sql} THEN RETURN #{returning_columns_statement}" if returning_columns_statement
|
155
|
+
end
|
156
|
+
super
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def _has_pk_binding pk, binds
|
161
|
+
if pk.respond_to? :each
|
162
|
+
has_value = true
|
163
|
+
pk.each { |col| has_value &&= binds.any? { |bind| bind.name == col } }
|
164
|
+
has_value
|
165
|
+
else
|
166
|
+
binds.any? { |bind| bind.name == pk }
|
167
|
+
end
|
70
168
|
end
|
71
169
|
|
72
170
|
def exec_mutation mutation
|
@@ -219,6 +317,9 @@ module ActiveRecord
|
|
219
317
|
if bind.respond_to? :type
|
220
318
|
type = ActiveRecord::Type::Spanner::SpannerActiveRecordConverter
|
221
319
|
.convert_active_model_type_to_spanner(bind.type)
|
320
|
+
elsif bind.class == Symbol
|
321
|
+
# This ensures that for example :environment is sent as the string 'environment' to Cloud Spanner.
|
322
|
+
type = :STRING
|
222
323
|
end
|
223
324
|
[
|
224
325
|
# Generates binds for named parameters in the format `@p1, @p2, ...`
|
@@ -226,7 +327,15 @@ module ActiveRecord
|
|
226
327
|
]
|
227
328
|
end.to_h
|
228
329
|
params = binds.enum_for(:each_with_index).map do |bind, i|
|
229
|
-
type = bind.respond_to?
|
330
|
+
type = if bind.respond_to? :type
|
331
|
+
bind.type
|
332
|
+
elsif bind.class == Symbol
|
333
|
+
# This ensures that for example :environment is sent as the string 'environment' to Cloud Spanner.
|
334
|
+
:STRING
|
335
|
+
else
|
336
|
+
# The Cloud Spanner default type is INT64 if no other type is known.
|
337
|
+
ActiveModel::Type::Integer
|
338
|
+
end
|
230
339
|
bind_value = bind.respond_to?(:value) ? bind.value : bind
|
231
340
|
value = ActiveRecord::Type::Spanner::SpannerActiveRecordConverter
|
232
341
|
.serialize_with_transaction_isolation_level(type, bind_value, :dml)
|
@@ -6,37 +6,19 @@
|
|
6
6
|
|
7
7
|
module ActiveRecord
|
8
8
|
module ConnectionAdapters
|
9
|
-
class SpannerSchemaCache
|
9
|
+
class SpannerSchemaCache
|
10
10
|
def initialize conn
|
11
|
+
@connection = conn
|
11
12
|
@primary_and_parent_keys = {}
|
12
|
-
super
|
13
|
-
end
|
14
|
-
|
15
|
-
def initialize_dup other
|
16
|
-
@primary_and_parent_keys = @primary_and_parent_keys.dup
|
17
|
-
super
|
18
|
-
end
|
19
|
-
|
20
|
-
def encode_with coder
|
21
|
-
coder["primary_and_parent_keys"] = @primary_and_parent_keys
|
22
|
-
super
|
23
|
-
end
|
24
|
-
|
25
|
-
def init_with coder
|
26
|
-
@primary_and_parent_keys = coder["primary_and_parent_keys"]
|
27
|
-
super
|
28
13
|
end
|
29
14
|
|
30
15
|
def primary_and_parent_keys table_name
|
31
16
|
@primary_and_parent_keys[table_name] ||=
|
32
|
-
|
33
|
-
connection.primary_and_parent_keys table_name
|
34
|
-
end
|
17
|
+
@connection.primary_and_parent_keys table_name
|
35
18
|
end
|
36
19
|
|
37
20
|
def clear!
|
38
21
|
@primary_and_parent_keys.clear
|
39
|
-
super
|
40
22
|
end
|
41
23
|
end
|
42
24
|
end
|
@@ -39,6 +39,17 @@ module ActiveRecord
|
|
39
39
|
|
40
40
|
primary_keys = if o.primary_keys
|
41
41
|
o.primary_keys
|
42
|
+
elsif o.options[:primary_key]
|
43
|
+
columns = if o.options[:primary_key].is_a? Array
|
44
|
+
o.options[:primary_key]
|
45
|
+
else
|
46
|
+
[o.options[:primary_key]]
|
47
|
+
end
|
48
|
+
pk_names = []
|
49
|
+
columns.each do |c|
|
50
|
+
pk_names.append c.to_s
|
51
|
+
end
|
52
|
+
PrimaryKeyDefinition.new pk_names
|
42
53
|
else
|
43
54
|
pk_names = o.columns.each_with_object [] do |c, r|
|
44
55
|
if c.type == :primary_key || c.primary_key?
|
@@ -42,6 +42,10 @@ module ActiveRecord
|
|
42
42
|
|
43
43
|
super
|
44
44
|
end
|
45
|
+
|
46
|
+
def valid_column_definition_options
|
47
|
+
super + [:type, :array, :allow_commit_timestamp, :as, :stored, :parent_key, :passed_type, :index]
|
48
|
+
end
|
45
49
|
end
|
46
50
|
|
47
51
|
class Table < ActiveRecord::ConnectionAdapters::Table
|
@@ -109,7 +109,7 @@ module ActiveRecord
|
|
109
109
|
information_schema { |i| i.table_columns table_name }
|
110
110
|
end
|
111
111
|
|
112
|
-
def new_column_from_field _table_name, field
|
112
|
+
def new_column_from_field _table_name, field, _definitions = nil
|
113
113
|
Spanner::Column.new \
|
114
114
|
field.name,
|
115
115
|
field.default,
|
@@ -118,7 +118,8 @@ module ActiveRecord
|
|
118
118
|
field.allow_commit_timestamp,
|
119
119
|
field.generated),
|
120
120
|
field.nullable,
|
121
|
-
field.default_function
|
121
|
+
field.default_function,
|
122
|
+
primary_key: field.primary_key
|
122
123
|
end
|
123
124
|
|
124
125
|
def fetch_type_metadata sql_type, ordinal_position = nil, allow_commit_timestamp = nil, generated = nil
|
@@ -42,14 +42,6 @@ module ActiveRecord
|
|
42
42
|
end
|
43
43
|
|
44
44
|
module ConnectionAdapters
|
45
|
-
module AbstractPool
|
46
|
-
def get_schema_cache connection
|
47
|
-
self.schema_cache ||= SpannerSchemaCache.new connection
|
48
|
-
schema_cache.connection = connection
|
49
|
-
schema_cache
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
45
|
class SpannerAdapter < AbstractAdapter
|
54
46
|
ADAPTER_NAME = "spanner".freeze
|
55
47
|
NATIVE_DATABASE_TYPES = {
|
@@ -78,8 +70,10 @@ module ActiveRecord
|
|
78
70
|
class_attribute :log_statement_binds, instance_writer: false, default: false
|
79
71
|
|
80
72
|
def initialize connection, logger, connection_options, config
|
81
|
-
|
73
|
+
@connection = connection
|
82
74
|
@connection_options = connection_options
|
75
|
+
super connection, logger, config
|
76
|
+
@raw_connection ||= connection
|
83
77
|
end
|
84
78
|
|
85
79
|
def max_identifier_length
|
@@ -117,6 +111,10 @@ module ActiveRecord
|
|
117
111
|
end
|
118
112
|
alias reconnect! reset!
|
119
113
|
|
114
|
+
def spanner_schema_cache
|
115
|
+
@spanner_schema_cache ||= SpannerSchemaCache.new self
|
116
|
+
end
|
117
|
+
|
120
118
|
# Spanner Connection API
|
121
119
|
delegate :ddl_batch, :ddl_batch?, :start_batch_ddl, :abort_batch, :run_batch, to: :@connection
|
122
120
|
|
@@ -186,6 +184,10 @@ module ActiveRecord
|
|
186
184
|
SecureRandom.uuid.gsub("-", "").hex & 0x7FFFFFFFFFFFFFFF
|
187
185
|
end
|
188
186
|
|
187
|
+
def return_value_after_insert? column
|
188
|
+
column.auto_incremented_by_db? || column.primary_key?
|
189
|
+
end
|
190
|
+
|
189
191
|
def arel_visitor
|
190
192
|
Arel::Visitors::Spanner.new self
|
191
193
|
end
|
@@ -259,6 +261,23 @@ module ActiveRecord
|
|
259
261
|
include TypeMapBuilder
|
260
262
|
end
|
261
263
|
|
264
|
+
def transform sql
|
265
|
+
if ActiveRecord::VERSION::MAJOR >= 7
|
266
|
+
transform_query sql
|
267
|
+
else
|
268
|
+
sql
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
# Overwrite the standard log method to be able to translate exceptions.
|
273
|
+
def log sql, name = "SQL", binds = [], type_casted_binds = [], statement_name = nil, *args
|
274
|
+
super
|
275
|
+
rescue ActiveRecord::StatementInvalid
|
276
|
+
raise
|
277
|
+
rescue StandardError => e
|
278
|
+
raise translate_exception_class(e, sql, binds)
|
279
|
+
end
|
280
|
+
|
262
281
|
def translate_exception exception, message:, sql:, binds:
|
263
282
|
if exception.is_a? Google::Cloud::FailedPreconditionError
|
264
283
|
case exception.message
|
@@ -4,6 +4,7 @@
|
|
4
4
|
# license that can be found in the LICENSE file or at
|
5
5
|
# https://opensource.org/licenses/MIT.
|
6
6
|
|
7
|
+
require "active_record/gem_version"
|
7
8
|
require "activerecord_spanner_adapter/relation"
|
8
9
|
|
9
10
|
module ActiveRecord
|
@@ -14,6 +15,8 @@ module ActiveRecord
|
|
14
15
|
end
|
15
16
|
|
16
17
|
class Base
|
18
|
+
VERSION_7_1 = Gem::Version.create "7.1.0"
|
19
|
+
|
17
20
|
# Creates an object (or multiple objects) and saves it to the database. This method will use mutations instead
|
18
21
|
# of DML if there is no active transaction, or if the active transaction has been created with the option
|
19
22
|
# isolation: :buffered_mutations.
|
@@ -30,7 +33,9 @@ module ActiveRecord
|
|
30
33
|
return super unless spanner_adapter?
|
31
34
|
return super if active_transaction?
|
32
35
|
|
33
|
-
|
36
|
+
# Only use mutations to create new records if the primary key is generated client-side.
|
37
|
+
isolation = sequence_name ? nil : :buffered_mutations
|
38
|
+
transaction isolation: isolation do
|
34
39
|
return super
|
35
40
|
end
|
36
41
|
end
|
@@ -43,35 +48,67 @@ module ActiveRecord
|
|
43
48
|
spanner_adapter? && connection&.current_spanner_transaction&.isolation == :buffered_mutations
|
44
49
|
end
|
45
50
|
|
46
|
-
def self._insert_record values
|
51
|
+
def self._insert_record values, returning = []
|
47
52
|
return super unless buffered_mutations? || (primary_key && values.is_a?(Hash))
|
48
53
|
|
49
|
-
|
54
|
+
# Mutations cannot be used in combination with a sequence, as mutations do not support a THEN RETURN clause.
|
55
|
+
if buffered_mutations? && sequence_name
|
56
|
+
raise StatementInvalid, "Mutations cannot be used to create records that use a sequence " \
|
57
|
+
"to generate the primary key. #{self} uses #{sequence_name}."
|
58
|
+
end
|
50
59
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
else
|
55
|
-
_set_single_primary_key_value primary_key, values
|
56
|
-
end
|
60
|
+
return _buffer_record values, :insert, returning if buffered_mutations?
|
61
|
+
|
62
|
+
primary_key_value = _set_primary_key_value values
|
57
63
|
if ActiveRecord::VERSION::MAJOR >= 7
|
58
64
|
im = Arel::InsertManager.new arel_table
|
59
65
|
im.insert(values.transform_keys { |name| arel_table[name] })
|
60
66
|
else
|
61
67
|
im = arel_table.compile_insert _substitute_values(values)
|
62
68
|
end
|
63
|
-
connection.insert(im, "#{self} Create", primary_key || false, primary_key_value)
|
69
|
+
result = connection.insert(im, "#{self} Create", primary_key || false, primary_key_value)
|
70
|
+
|
71
|
+
_convert_primary_key result, returning
|
64
72
|
end
|
65
73
|
|
66
|
-
def self.
|
67
|
-
|
74
|
+
def self._set_primary_key_value values
|
75
|
+
if primary_key.is_a? Array
|
76
|
+
_set_composite_primary_key_values primary_key, values
|
77
|
+
else
|
78
|
+
_set_single_primary_key_value primary_key, values
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def self._convert_primary_key primary_key_value, returning
|
83
|
+
# Rails 7.1 and higher supports composite primary keys, and therefore require the provider to return an array
|
84
|
+
# instead of a single value in all cases. The order of the values should be equal to the order of the returning
|
85
|
+
# columns (or the primary key if no returning columns were specified).
|
86
|
+
return primary_key_value if ActiveRecord.gem_version < VERSION_7_1
|
87
|
+
return primary_key_value if primary_key_value.is_a?(Array) && primary_key_value.length == 1
|
88
|
+
return [primary_key_value] unless primary_key_value.is_a? Array
|
89
|
+
|
90
|
+
# Re-order the returned values according to the returning columns if it is not equal to the primary key of the
|
91
|
+
# table.
|
92
|
+
keys = returning || primary_key
|
93
|
+
return primary_key_value if keys == primary_key
|
94
|
+
|
95
|
+
primary_key_values_hash = Hash[primary_key.zip(primary_key_value)]
|
96
|
+
values = []
|
97
|
+
keys.each do |column|
|
98
|
+
values.append primary_key_values_hash[column]
|
99
|
+
end
|
100
|
+
values
|
101
|
+
end
|
102
|
+
|
103
|
+
def self._upsert_record values, returning
|
104
|
+
_buffer_record values, :insert_or_update, returning
|
68
105
|
end
|
69
106
|
|
70
|
-
def self.insert_all _attributes,
|
71
|
-
raise NotImplementedError, "Cloud Spanner does not support skip_duplicates."
|
107
|
+
def self.insert_all _attributes, **_kwargs
|
108
|
+
raise NotImplementedError, "Cloud Spanner does not support skip_duplicates. Use insert! or upsert instead."
|
72
109
|
end
|
73
110
|
|
74
|
-
def self.insert_all! attributes, returning: nil
|
111
|
+
def self.insert_all! attributes, returning: nil, **_kwargs
|
75
112
|
return super unless spanner_adapter?
|
76
113
|
return super if active_transaction? && !buffered_mutations?
|
77
114
|
|
@@ -90,7 +127,7 @@ module ActiveRecord
|
|
90
127
|
end
|
91
128
|
end
|
92
129
|
|
93
|
-
def self.upsert_all attributes, returning: nil, unique_by: nil
|
130
|
+
def self.upsert_all attributes, returning: nil, unique_by: nil, **_kwargs
|
94
131
|
return super unless spanner_adapter?
|
95
132
|
if active_transaction? && !buffered_mutations?
|
96
133
|
raise NotImplementedError, "Cloud Spanner does not support upsert using DML. " \
|
@@ -102,18 +139,18 @@ module ActiveRecord
|
|
102
139
|
# The mutations will be sent as one batch when the transaction is committed.
|
103
140
|
if active_transaction?
|
104
141
|
attributes.each do |record|
|
105
|
-
_upsert_record record
|
142
|
+
_upsert_record record, returning
|
106
143
|
end
|
107
144
|
else
|
108
145
|
transaction isolation: :buffered_mutations do
|
109
146
|
attributes.each do |record|
|
110
|
-
_upsert_record record
|
147
|
+
_upsert_record record, returning
|
111
148
|
end
|
112
149
|
end
|
113
150
|
end
|
114
151
|
end
|
115
152
|
|
116
|
-
def self._buffer_record values, method
|
153
|
+
def self._buffer_record values, method, returning
|
117
154
|
primary_key_value =
|
118
155
|
if primary_key.is_a? Array
|
119
156
|
_set_composite_primary_key_values primary_key, values
|
@@ -132,10 +169,9 @@ module ActiveRecord
|
|
132
169
|
mutation = Google::Cloud::Spanner::V1::Mutation.new(
|
133
170
|
"#{method}": write
|
134
171
|
)
|
135
|
-
|
136
172
|
connection.current_spanner_transaction.buffer mutation
|
137
173
|
|
138
|
-
primary_key_value
|
174
|
+
_convert_primary_key primary_key_value, returning
|
139
175
|
end
|
140
176
|
|
141
177
|
def self._set_composite_primary_key_values primary_key, values
|
@@ -174,6 +210,7 @@ module ActiveRecord
|
|
174
210
|
def self._set_single_primary_key_value primary_key, values
|
175
211
|
primary_key_value = values[primary_key] || values[primary_key.to_sym]
|
176
212
|
|
213
|
+
return primary_key_value if sequence_name
|
177
214
|
return primary_key_value unless prefetch_primary_key?
|
178
215
|
|
179
216
|
if primary_key_value.nil?
|