activerecord-spanner-adapter 1.5.1 → 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 +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
|