activerecord-spanner-adapter 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/acceptance-tests-on-emulator.yaml +1 -1
- data/.github/workflows/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?
|