activerecord-spanner-adapter 1.5.1 → 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 +8 -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/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 +56 -19
- 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,28 @@
|
|
1
|
+
# Copyright 2022 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 Track < ActiveRecord::Base
|
8
|
+
# Use the `composite_primary_key` gem to create a composite primary key definition for the model.
|
9
|
+
self.primary_keys = :singerid, :albumid, :trackid
|
10
|
+
|
11
|
+
# `tracks` is defined as INTERLEAVE IN PARENT `albums`. The primary key of `albums` is ()`singerid`, `albumid`).
|
12
|
+
belongs_to :album, foreign_key: [:singerid, :albumid]
|
13
|
+
|
14
|
+
# `tracks` also has a `singerid` column should be used to associate a Track with a Singer.
|
15
|
+
belongs_to :singer, foreign_key: :singerid
|
16
|
+
|
17
|
+
# Override the default initialize method to automatically set the singer attribute when an album is given.
|
18
|
+
def initialize attributes = nil
|
19
|
+
super
|
20
|
+
self.singer ||= album&.singer
|
21
|
+
end
|
22
|
+
|
23
|
+
def album=value
|
24
|
+
super
|
25
|
+
# Ensure the singer of this track is equal to the singer of the album that is set.
|
26
|
+
self.singer = value&.singer
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# Sample - Query Logs
|
2
|
+
|
3
|
+
__NOTE__: Query logs require additional configuration for Cloud Spanner. Please read the entire file.
|
4
|
+
|
5
|
+
Rails 7.0 and higher supports [Query Logs](https://api.rubyonrails.org/classes/ActiveRecord/QueryLogs.html). Query Logs
|
6
|
+
can be used to automatically annotate all queries that are executed based on the current execution context.
|
7
|
+
|
8
|
+
The Cloud Spanner ActiveRecord provider can be used in combination with Query Logs. The query logs are automatically
|
9
|
+
translated to request tags for the queries.
|
10
|
+
See https://cloud.google.com/spanner/docs/introspection/troubleshooting-with-tags for more
|
11
|
+
information about request and transaction tags in Cloud Spanner.
|
12
|
+
|
13
|
+
## Configuration
|
14
|
+
Using Query Logs with Cloud Spanner requires some specific configuration:
|
15
|
+
1. You must set `ActiveRecord::QueryLogs.prepend_comment = true`
|
16
|
+
2. You must include `{ request_tag: "true" }` as the first tag in your configuration.
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
ActiveRecord::QueryLogs.prepend_comment = true
|
20
|
+
config.active_record.query_log_tags = [
|
21
|
+
{
|
22
|
+
request_tag: "true",
|
23
|
+
},
|
24
|
+
:namespaced_controller,
|
25
|
+
:action,
|
26
|
+
:job,
|
27
|
+
{
|
28
|
+
request_id: ->(context) { context[:controller]&.request&.request_id },
|
29
|
+
job_id: ->(context) { context[:job]&.job_id },
|
30
|
+
tenant_id: -> { Current.tenant&.id },
|
31
|
+
static: "value",
|
32
|
+
},
|
33
|
+
]
|
34
|
+
```
|
35
|
+
|
36
|
+
The sample will automatically start a Spanner Emulator in a docker container and execute the sample
|
37
|
+
against that emulator. The emulator will automatically be stopped when the application finishes.
|
38
|
+
|
39
|
+
Run the application with the command
|
40
|
+
|
41
|
+
```bash
|
42
|
+
bundle exec rake run
|
43
|
+
```
|
@@ -0,0 +1,13 @@
|
|
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"
|
8
|
+
require "sinatra/activerecord/rake"
|
9
|
+
|
10
|
+
desc "Sample showing how to use automatic query log tagging on Cloud Spanner with ActiveRecord."
|
11
|
+
task :run do
|
12
|
+
Dir.chdir("..") { sh "bundle exec rake run[query-logs]" }
|
13
|
+
end
|
@@ -0,0 +1,63 @@
|
|
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 "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
|
+
enable_query_logs
|
15
|
+
|
16
|
+
puts ""
|
17
|
+
puts "Query all Albums and include an automatically generated request tag"
|
18
|
+
albums = Album.all
|
19
|
+
puts "Queried #{albums.length} albums using an automatically generated request tag"
|
20
|
+
|
21
|
+
puts ""
|
22
|
+
puts "Press any key to end the application"
|
23
|
+
STDIN.getch
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.enable_query_logs
|
27
|
+
# Enables Query Logs in a non-Rails application. Normally, this should be done
|
28
|
+
# as described here: https://api.rubyonrails.org/classes/ActiveRecord/QueryLogs.html
|
29
|
+
ActiveRecord.query_transformers << ActiveRecord::QueryLogs
|
30
|
+
|
31
|
+
# Query log comments *MUST* be prepended to be included as a request tag.
|
32
|
+
ActiveRecord::QueryLogs.prepend_comment = true
|
33
|
+
|
34
|
+
# This block manually enables Query Logs without a full Rails application.
|
35
|
+
# This should normally not be needed in your application.
|
36
|
+
ActiveRecord::QueryLogs.taggings.merge!(
|
37
|
+
application: "example-app",
|
38
|
+
action: "run-test-application",
|
39
|
+
pid: -> { Process.pid.to_s },
|
40
|
+
socket: ->(context) { context[:connection].pool.db_config.socket },
|
41
|
+
db_host: ->(context) { context[:connection].pool.db_config.host },
|
42
|
+
database: ->(context) { context[:connection].pool.db_config.database }
|
43
|
+
)
|
44
|
+
|
45
|
+
ActiveRecord::QueryLogs.tags = [
|
46
|
+
# The first tag *MUST* be the fixed value 'request_tag:true'.
|
47
|
+
{
|
48
|
+
request_tag: "true"
|
49
|
+
},
|
50
|
+
:controller,
|
51
|
+
:action,
|
52
|
+
:job,
|
53
|
+
{
|
54
|
+
request_id: ->(context) { context[:controller]&.request&.request_id },
|
55
|
+
job_id: ->(context) { context[:job]&.job_id }
|
56
|
+
},
|
57
|
+
:db_host,
|
58
|
+
:database
|
59
|
+
]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
Application.run
|
@@ -0,0 +1,21 @@
|
|
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
|
+
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
|
12
|
+
t.string :last_name
|
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,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
|