activerecord-spanner-adapter 0.3.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/CODEOWNERS +7 -0
- data/.github/sync-repo-settings.yaml +16 -0
- data/.github/workflows/acceptance-tests-on-emulator.yaml +45 -0
- data/.github/workflows/acceptance-tests-on-production.yaml +36 -0
- data/.github/workflows/ci.yaml +33 -0
- data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +52 -0
- data/.github/workflows/nightly-acceptance-tests-on-production.yaml +35 -0
- data/.github/workflows/nightly-unit-tests.yaml +40 -0
- data/.github/workflows/release-please-label.yml +25 -0
- data/.github/workflows/release-please.yml +39 -0
- data/.github/workflows/rubocop.yaml +31 -0
- data/.gitignore +67 -5
- data/.kokoro/populate-secrets.sh +77 -0
- data/.kokoro/release.cfg +33 -0
- data/.kokoro/release.sh +15 -0
- data/.kokoro/trampoline_v2.sh +489 -0
- data/.rubocop.yml +46 -0
- data/.toys/release.rb +18 -0
- data/.trampolinerc +48 -0
- data/.yardopts +11 -0
- data/CHANGELOG.md +26 -0
- data/CODE_OF_CONDUCT.md +40 -0
- data/CONTRIBUTING.md +79 -0
- data/Gemfile +9 -4
- data/LICENSE +6 -6
- data/README.md +67 -30
- data/Rakefile +79 -3
- data/SECURITY.md +7 -0
- data/acceptance/cases/associations/has_many_associations_test.rb +119 -0
- data/acceptance/cases/associations/has_many_through_associations_test.rb +63 -0
- data/acceptance/cases/associations/has_one_associations_test.rb +79 -0
- data/acceptance/cases/associations/has_one_through_associations_test.rb +98 -0
- data/acceptance/cases/interleaved_associations/has_many_associations_using_interleaved_test.rb +211 -0
- data/acceptance/cases/migration/change_schema_test.rb +433 -0
- data/acceptance/cases/migration/change_table_test.rb +115 -0
- data/acceptance/cases/migration/column_attributes_test.rb +122 -0
- data/acceptance/cases/migration/column_positioning_test.rb +48 -0
- data/acceptance/cases/migration/columns_test.rb +201 -0
- data/acceptance/cases/migration/command_recorder_test.rb +406 -0
- data/acceptance/cases/migration/create_join_table_test.rb +216 -0
- data/acceptance/cases/migration/ddl_batching_test.rb +80 -0
- data/acceptance/cases/migration/foreign_key_test.rb +297 -0
- data/acceptance/cases/migration/index_test.rb +211 -0
- data/acceptance/cases/migration/references_foreign_key_test.rb +259 -0
- data/acceptance/cases/migration/references_index_test.rb +135 -0
- data/acceptance/cases/migration/references_statements_test.rb +166 -0
- data/acceptance/cases/migration/rename_column_test.rb +96 -0
- data/acceptance/cases/models/calculation_query_test.rb +128 -0
- data/acceptance/cases/models/generated_column_test.rb +126 -0
- data/acceptance/cases/models/mutation_test.rb +122 -0
- data/acceptance/cases/models/query_test.rb +147 -0
- data/acceptance/cases/sessions/session_not_found_test.rb +121 -0
- data/acceptance/cases/transactions/optimistic_locking_test.rb +141 -0
- data/acceptance/cases/transactions/read_only_transactions_test.rb +67 -0
- data/acceptance/cases/transactions/read_write_transactions_test.rb +248 -0
- data/acceptance/cases/type/all_types_test.rb +152 -0
- data/acceptance/cases/type/binary_test.rb +59 -0
- data/acceptance/cases/type/boolean_test.rb +31 -0
- data/acceptance/cases/type/date_test.rb +32 -0
- data/acceptance/cases/type/date_time_test.rb +30 -0
- data/acceptance/cases/type/float_test.rb +27 -0
- data/acceptance/cases/type/integer_test.rb +44 -0
- data/acceptance/cases/type/numeric_test.rb +27 -0
- data/acceptance/cases/type/string_test.rb +79 -0
- data/acceptance/cases/type/text_test.rb +30 -0
- data/acceptance/cases/type/time_test.rb +87 -0
- data/acceptance/models/account.rb +13 -0
- data/acceptance/models/address.rb +9 -0
- data/acceptance/models/album.rb +12 -0
- data/acceptance/models/all_types.rb +8 -0
- data/acceptance/models/author.rb +11 -0
- data/acceptance/models/club.rb +12 -0
- data/acceptance/models/comment.rb +9 -0
- data/acceptance/models/customer.rb +9 -0
- data/acceptance/models/department.rb +9 -0
- data/acceptance/models/firm.rb +10 -0
- data/acceptance/models/member.rb +13 -0
- data/acceptance/models/member_type.rb +9 -0
- data/acceptance/models/membership.rb +10 -0
- data/acceptance/models/organization.rb +9 -0
- data/acceptance/models/post.rb +10 -0
- data/acceptance/models/singer.rb +10 -0
- data/acceptance/models/track.rb +20 -0
- data/acceptance/models/transaction.rb +9 -0
- data/acceptance/schema/schema.rb +143 -0
- data/acceptance/test_helper.rb +260 -0
- data/activerecord-spanner-adapter.gemspec +32 -17
- data/assets/solidus-db.png +0 -0
- data/benchmarks/README.md +17 -0
- data/benchmarks/Rakefile +14 -0
- data/benchmarks/application.rb +308 -0
- data/benchmarks/config/database.yml +8 -0
- data/benchmarks/config/environment.rb +12 -0
- data/benchmarks/db/migrate/01_create_tables.rb +25 -0
- data/benchmarks/db/schema.rb +29 -0
- data/benchmarks/models/album.rb +9 -0
- data/benchmarks/models/singer.rb +9 -0
- data/bin/console +6 -7
- data/examples/rails/README.md +262 -0
- data/examples/snippets/README.md +29 -0
- data/examples/snippets/Rakefile +57 -0
- data/examples/snippets/array-data-type/README.md +45 -0
- data/examples/snippets/array-data-type/Rakefile +13 -0
- data/examples/snippets/array-data-type/application.rb +45 -0
- data/examples/snippets/array-data-type/config/database.yml +8 -0
- data/examples/snippets/array-data-type/db/migrate/01_create_tables.rb +24 -0
- data/examples/snippets/array-data-type/db/schema.rb +26 -0
- data/examples/snippets/array-data-type/db/seeds.rb +5 -0
- data/examples/snippets/array-data-type/models/entity_with_array_types.rb +18 -0
- data/examples/snippets/bin/create_emulator_instance.rb +18 -0
- data/examples/snippets/bulk-insert/README.md +21 -0
- data/examples/snippets/bulk-insert/Rakefile +13 -0
- data/examples/snippets/bulk-insert/application.rb +64 -0
- data/examples/snippets/bulk-insert/config/database.yml +8 -0
- data/examples/snippets/bulk-insert/db/migrate/01_create_tables.rb +21 -0
- data/examples/snippets/bulk-insert/db/schema.rb +26 -0
- data/examples/snippets/bulk-insert/db/seeds.rb +5 -0
- data/examples/snippets/bulk-insert/models/album.rb +9 -0
- data/examples/snippets/bulk-insert/models/singer.rb +9 -0
- data/examples/snippets/commit-timestamp/README.md +18 -0
- data/examples/snippets/commit-timestamp/Rakefile +13 -0
- data/examples/snippets/commit-timestamp/application.rb +53 -0
- data/examples/snippets/commit-timestamp/config/database.yml +8 -0
- data/examples/snippets/commit-timestamp/db/migrate/01_create_tables.rb +26 -0
- data/examples/snippets/commit-timestamp/db/schema.rb +29 -0
- data/examples/snippets/commit-timestamp/db/seeds.rb +5 -0
- data/examples/snippets/commit-timestamp/models/album.rb +9 -0
- data/examples/snippets/commit-timestamp/models/singer.rb +9 -0
- data/examples/snippets/config/environment.rb +21 -0
- data/examples/snippets/create-records/README.md +12 -0
- data/examples/snippets/create-records/Rakefile +13 -0
- data/examples/snippets/create-records/application.rb +42 -0
- data/examples/snippets/create-records/config/database.yml +8 -0
- data/examples/snippets/create-records/db/migrate/01_create_tables.rb +21 -0
- data/examples/snippets/create-records/db/schema.rb +26 -0
- data/examples/snippets/create-records/db/seeds.rb +5 -0
- data/examples/snippets/create-records/models/album.rb +9 -0
- data/examples/snippets/create-records/models/singer.rb +9 -0
- data/examples/snippets/date-data-type/README.md +19 -0
- data/examples/snippets/date-data-type/Rakefile +13 -0
- data/examples/snippets/date-data-type/application.rb +35 -0
- data/examples/snippets/date-data-type/config/database.yml +8 -0
- data/examples/snippets/date-data-type/db/migrate/01_create_tables.rb +20 -0
- data/examples/snippets/date-data-type/db/schema.rb +21 -0
- data/examples/snippets/date-data-type/db/seeds.rb +16 -0
- data/examples/snippets/date-data-type/models/singer.rb +8 -0
- data/examples/snippets/generated-column/README.md +41 -0
- data/examples/snippets/generated-column/Rakefile +13 -0
- data/examples/snippets/generated-column/application.rb +37 -0
- data/examples/snippets/generated-column/config/database.yml +8 -0
- data/examples/snippets/generated-column/db/migrate/01_create_tables.rb +23 -0
- data/examples/snippets/generated-column/db/schema.rb +21 -0
- data/examples/snippets/generated-column/db/seeds.rb +18 -0
- data/examples/snippets/generated-column/models/singer.rb +8 -0
- data/examples/snippets/interleaved-tables/README.md +152 -0
- data/examples/snippets/interleaved-tables/Rakefile +13 -0
- data/examples/snippets/interleaved-tables/application.rb +109 -0
- data/examples/snippets/interleaved-tables/config/database.yml +8 -0
- data/examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb +44 -0
- data/examples/snippets/interleaved-tables/db/schema.rb +32 -0
- data/examples/snippets/interleaved-tables/db/seeds.rb +40 -0
- data/examples/snippets/interleaved-tables/models/album.rb +15 -0
- data/examples/snippets/interleaved-tables/models/singer.rb +20 -0
- data/examples/snippets/interleaved-tables/models/track.rb +25 -0
- data/examples/snippets/migrations/README.md +43 -0
- data/examples/snippets/migrations/Rakefile +13 -0
- data/examples/snippets/migrations/application.rb +26 -0
- data/examples/snippets/migrations/config/database.yml +8 -0
- data/examples/snippets/migrations/db/migrate/01_create_tables.rb +28 -0
- data/examples/snippets/migrations/db/schema.rb +33 -0
- data/examples/snippets/migrations/db/seeds.rb +5 -0
- data/examples/snippets/migrations/models/album.rb +10 -0
- data/examples/snippets/migrations/models/singer.rb +10 -0
- data/examples/snippets/migrations/models/track.rb +9 -0
- data/examples/snippets/mutations/README.md +34 -0
- data/examples/snippets/mutations/Rakefile +13 -0
- data/examples/snippets/mutations/application.rb +47 -0
- data/examples/snippets/mutations/config/database.yml +8 -0
- data/examples/snippets/mutations/db/migrate/01_create_tables.rb +22 -0
- data/examples/snippets/mutations/db/schema.rb +27 -0
- data/examples/snippets/mutations/db/seeds.rb +25 -0
- data/examples/snippets/mutations/models/album.rb +9 -0
- data/examples/snippets/mutations/models/singer.rb +9 -0
- data/examples/snippets/optimistic-locking/README.md +12 -0
- data/examples/snippets/optimistic-locking/Rakefile +13 -0
- data/examples/snippets/optimistic-locking/application.rb +48 -0
- data/examples/snippets/optimistic-locking/config/database.yml +8 -0
- data/examples/snippets/optimistic-locking/db/migrate/01_create_tables.rb +26 -0
- data/examples/snippets/optimistic-locking/db/schema.rb +29 -0
- data/examples/snippets/optimistic-locking/db/seeds.rb +25 -0
- data/examples/snippets/optimistic-locking/models/album.rb +9 -0
- data/examples/snippets/optimistic-locking/models/singer.rb +9 -0
- data/examples/snippets/quickstart/README.md +26 -0
- data/examples/snippets/quickstart/Rakefile +13 -0
- data/examples/snippets/quickstart/application.rb +51 -0
- data/examples/snippets/quickstart/config/database.yml +8 -0
- data/examples/snippets/quickstart/db/migrate/01_create_tables.rb +21 -0
- data/examples/snippets/quickstart/db/schema.rb +26 -0
- data/examples/snippets/quickstart/db/seeds.rb +24 -0
- data/examples/snippets/quickstart/models/album.rb +9 -0
- data/examples/snippets/quickstart/models/singer.rb +9 -0
- data/examples/snippets/read-only-transactions/README.md +13 -0
- data/examples/snippets/read-only-transactions/Rakefile +13 -0
- data/examples/snippets/read-only-transactions/application.rb +49 -0
- data/examples/snippets/read-only-transactions/config/database.yml +8 -0
- data/examples/snippets/read-only-transactions/db/migrate/01_create_tables.rb +21 -0
- data/examples/snippets/read-only-transactions/db/schema.rb +26 -0
- data/examples/snippets/read-only-transactions/db/seeds.rb +24 -0
- data/examples/snippets/read-only-transactions/models/album.rb +9 -0
- data/examples/snippets/read-only-transactions/models/singer.rb +9 -0
- data/examples/snippets/read-write-transactions/README.md +12 -0
- data/examples/snippets/read-write-transactions/Rakefile +13 -0
- data/examples/snippets/read-write-transactions/application.rb +39 -0
- data/examples/snippets/read-write-transactions/config/database.yml +8 -0
- data/examples/snippets/read-write-transactions/db/migrate/01_create_tables.rb +22 -0
- data/examples/snippets/read-write-transactions/db/schema.rb +27 -0
- data/examples/snippets/read-write-transactions/db/seeds.rb +25 -0
- data/examples/snippets/read-write-transactions/models/album.rb +9 -0
- data/examples/snippets/read-write-transactions/models/singer.rb +9 -0
- data/examples/snippets/timestamp-data-type/README.md +17 -0
- data/examples/snippets/timestamp-data-type/Rakefile +13 -0
- data/examples/snippets/timestamp-data-type/application.rb +42 -0
- data/examples/snippets/timestamp-data-type/config/database.yml +8 -0
- data/examples/snippets/timestamp-data-type/db/migrate/01_create_tables.rb +21 -0
- data/examples/snippets/timestamp-data-type/db/schema.rb +21 -0
- data/examples/snippets/timestamp-data-type/db/seeds.rb +6 -0
- data/examples/snippets/timestamp-data-type/models/meeting.rb +19 -0
- data/examples/solidus/README.md +172 -0
- data/lib/active_record/connection_adapters/spanner/database_statements.rb +224 -269
- data/lib/active_record/connection_adapters/spanner/quoting.rb +42 -50
- data/lib/active_record/connection_adapters/spanner/schema_cache.rb +43 -0
- data/lib/active_record/connection_adapters/spanner/schema_creation.rb +125 -9
- data/lib/active_record/connection_adapters/spanner/schema_definitions.rb +122 -0
- data/lib/active_record/connection_adapters/spanner/schema_dumper.rb +19 -0
- data/lib/active_record/connection_adapters/spanner/schema_statements.rb +553 -139
- data/lib/active_record/connection_adapters/spanner/type_metadata.rb +37 -0
- data/lib/active_record/connection_adapters/spanner_adapter.rb +182 -78
- data/lib/active_record/tasks/spanner_database_tasks.rb +74 -0
- data/lib/active_record/type/spanner/array.rb +32 -0
- data/lib/active_record/type/spanner/bytes.rb +26 -0
- data/lib/active_record/type/spanner/spanner_active_record_converter.rb +32 -0
- data/lib/active_record/type/spanner/time.rb +37 -0
- data/lib/activerecord-spanner-adapter.rb +23 -0
- data/lib/activerecord_spanner_adapter/base.rb +217 -0
- data/lib/activerecord_spanner_adapter/connection.rb +324 -0
- data/lib/activerecord_spanner_adapter/errors.rb +13 -0
- data/lib/activerecord_spanner_adapter/foreign_key.rb +29 -0
- data/lib/activerecord_spanner_adapter/index/column.rb +38 -0
- data/lib/activerecord_spanner_adapter/index.rb +80 -0
- data/lib/activerecord_spanner_adapter/information_schema.rb +261 -0
- data/lib/activerecord_spanner_adapter/primary_key.rb +31 -0
- data/lib/activerecord_spanner_adapter/table/column.rb +59 -0
- data/lib/activerecord_spanner_adapter/table.rb +61 -0
- data/lib/activerecord_spanner_adapter/transaction.rb +113 -0
- data/lib/activerecord_spanner_adapter/version.rb +9 -0
- data/lib/arel/visitors/spanner.rb +35 -0
- data/lib/spanner_client_ext.rb +82 -0
- data/renovate.json +5 -0
- metadata +387 -34
- data/.travis.yml +0 -5
- data/lib/active_record/connection_adapters/spanner/client.rb +0 -190
- data/lib/active_record/connection_adapters/spanner.rb +0 -10
- data/lib/activerecord-spanner-adapter/version.rb +0 -3
@@ -0,0 +1,26 @@
|
|
1
|
+
# Copyright 2020 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
|
+
# frozen_string_literal: true
|
8
|
+
|
9
|
+
module ActiveRecord
|
10
|
+
module Type
|
11
|
+
module Spanner
|
12
|
+
class Bytes < ActiveRecord::Type::Binary
|
13
|
+
def serialize value
|
14
|
+
return super value if value.nil?
|
15
|
+
|
16
|
+
if value.respond_to?(:read) && value.respond_to?(:rewind)
|
17
|
+
value.rewind
|
18
|
+
value = value.read
|
19
|
+
end
|
20
|
+
|
21
|
+
Base64.strict_encode64 value.force_encoding("ASCII-8BIT")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# Copyright 2021 Google LLC
|
2
|
+
#
|
3
|
+
# Use of this source code is governed by an MIT-style
|
4
|
+
# license that can be found in the LICENSE file or at
|
5
|
+
# https://opensource.org/licenses/MIT.
|
6
|
+
|
7
|
+
# frozen_string_literal: true
|
8
|
+
|
9
|
+
module ActiveRecord
|
10
|
+
module Type
|
11
|
+
module Spanner
|
12
|
+
class SpannerActiveRecordConverter
|
13
|
+
##
|
14
|
+
# Converts an ActiveModel::Type to a Spanner type code.
|
15
|
+
def self.convert_active_model_type_to_spanner type # rubocop:disable Metrics/CyclomaticComplexity
|
16
|
+
case type
|
17
|
+
when NilClass then nil
|
18
|
+
when ActiveModel::Type::Integer, ActiveModel::Type::BigInteger then :INT64
|
19
|
+
when ActiveModel::Type::Boolean then :BOOL
|
20
|
+
when ActiveModel::Type::String, ActiveModel::Type::ImmutableString then :STRING
|
21
|
+
when ActiveModel::Type::Binary, ActiveRecord::Type::Spanner::Bytes then :BYTES
|
22
|
+
when ActiveModel::Type::Float then :FLOAT64
|
23
|
+
when ActiveModel::Type::Decimal then :NUMERIC
|
24
|
+
when ActiveModel::Type::DateTime, ActiveModel::Type::Time, ActiveRecord::Type::Spanner::Time then :TIMESTAMP
|
25
|
+
when ActiveModel::Type::Date then :DATE
|
26
|
+
when ActiveRecord::Type::Spanner::Array then [convert_active_model_type_to_spanner(type.element_type)]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# Copyright 2020 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
|
+
# frozen_string_literal: true
|
8
|
+
|
9
|
+
module ActiveRecord
|
10
|
+
module Type
|
11
|
+
module Spanner
|
12
|
+
class Time < ActiveRecord::Type::Time
|
13
|
+
def serialize value, *options
|
14
|
+
return "PENDING_COMMIT_TIMESTAMP()" if value == :commit_timestamp && options.length && options[0] == :dml
|
15
|
+
return "spanner.commit_timestamp()" if value == :commit_timestamp && options.length && options[0] == :mutation
|
16
|
+
val = super value
|
17
|
+
val.acts_like?(:time) ? val.utc.rfc3339(9) : val
|
18
|
+
end
|
19
|
+
|
20
|
+
def user_input_in_time_zone value
|
21
|
+
return value.in_time_zone if value.is_a? ::Time
|
22
|
+
super value
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def cast_value value
|
28
|
+
if value.is_a? ::String
|
29
|
+
value = value.empty? ? nil : ::Time.parse(value)
|
30
|
+
end
|
31
|
+
|
32
|
+
value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Copyright 2020 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 "activerecord_spanner_adapter/version"
|
8
|
+
|
9
|
+
if defined?(Rails)
|
10
|
+
module ActiveRecord
|
11
|
+
module ConnectionAdapters
|
12
|
+
class SpannerRailtie < ::Rails::Railtie
|
13
|
+
rake_tasks do
|
14
|
+
require "active_record/tasks/spanner_database_tasks"
|
15
|
+
end
|
16
|
+
|
17
|
+
ActiveSupport.on_load :active_record do
|
18
|
+
require "active_record/connection_adapters/spanner_adapter"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,217 @@
|
|
1
|
+
# Copyright 2021 Google LLC
|
2
|
+
#
|
3
|
+
# Use of this source code is governed by an MIT-style
|
4
|
+
# license that can be found in the LICENSE file or at
|
5
|
+
# https://opensource.org/licenses/MIT.
|
6
|
+
|
7
|
+
module ActiveRecord
|
8
|
+
class TableMetadata # :nodoc:
|
9
|
+
# This attr_reader is private in ActiveRecord 6.0.x and public in 6.1.x. This makes sure it is always available in
|
10
|
+
# the Spanner adapter.
|
11
|
+
attr_reader :arel_table
|
12
|
+
end
|
13
|
+
|
14
|
+
class Base
|
15
|
+
# Creates an object (or multiple objects) and saves it to the database. This method will use mutations instead
|
16
|
+
# of DML if there is no active transaction, or if the active transaction has been created with the option
|
17
|
+
# isolation: :buffered_mutations.
|
18
|
+
def self.create attributes = nil, &block
|
19
|
+
return super if active_transaction?
|
20
|
+
|
21
|
+
transaction isolation: :buffered_mutations do
|
22
|
+
return super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self._insert_record values
|
27
|
+
return super unless Base.connection&.current_spanner_transaction&.isolation == :buffered_mutations
|
28
|
+
|
29
|
+
primary_key = self.primary_key
|
30
|
+
primary_key_value = nil
|
31
|
+
|
32
|
+
if primary_key && values.is_a?(Hash)
|
33
|
+
primary_key_value = values[primary_key]
|
34
|
+
|
35
|
+
if !primary_key_value && prefetch_primary_key?
|
36
|
+
primary_key_value = next_sequence_value
|
37
|
+
values[primary_key] = primary_key_value
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
metadata = TableMetadata.new self, arel_table
|
42
|
+
columns, grpc_values = _create_grpc_values_for_insert metadata, values
|
43
|
+
|
44
|
+
mutation = Google::Cloud::Spanner::V1::Mutation.new(
|
45
|
+
insert: Google::Cloud::Spanner::V1::Mutation::Write.new(
|
46
|
+
table: arel_table.name,
|
47
|
+
columns: columns,
|
48
|
+
values: [grpc_values.list_value]
|
49
|
+
)
|
50
|
+
)
|
51
|
+
Base.connection.current_spanner_transaction.buffer mutation
|
52
|
+
|
53
|
+
primary_key_value
|
54
|
+
end
|
55
|
+
|
56
|
+
# Deletes all records of this class. This method will use mutations instead of DML if there is no active
|
57
|
+
# transaction, or if the active transaction has been created with the option isolation: :buffered_mutations.
|
58
|
+
def self.delete_all
|
59
|
+
return super if active_transaction?
|
60
|
+
|
61
|
+
transaction isolation: :buffered_mutations do
|
62
|
+
return super
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.active_transaction?
|
67
|
+
current_transaction = connection.current_transaction
|
68
|
+
!(current_transaction.nil? || current_transaction.is_a?(ConnectionAdapters::NullTransaction))
|
69
|
+
end
|
70
|
+
|
71
|
+
# Updates the given attributes of the object in the database. This method will use mutations instead
|
72
|
+
# of DML if there is no active transaction, or if the active transaction has been created with the option
|
73
|
+
# isolation: :buffered_mutations.
|
74
|
+
def update attributes
|
75
|
+
return super if Base.active_transaction?
|
76
|
+
|
77
|
+
transaction isolation: :buffered_mutations do
|
78
|
+
return super
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Deletes the object in the database. This method will use mutations instead
|
83
|
+
# of DML if there is no active transaction, or if the active transaction has been created with the option
|
84
|
+
# isolation: :buffered_mutations.
|
85
|
+
def destroy
|
86
|
+
return super if Base.active_transaction?
|
87
|
+
|
88
|
+
transaction isolation: :buffered_mutations do
|
89
|
+
return super
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def self._create_grpc_values_for_insert metadata, values
|
96
|
+
serialized_values = []
|
97
|
+
columns = []
|
98
|
+
values.each_pair do |k, v|
|
99
|
+
type = metadata.type k
|
100
|
+
serialized_values << (type.method(:serialize).arity < 0 ? type.serialize(v, :mutation) : type.serialize(v))
|
101
|
+
columns << metadata.arel_table[k].name
|
102
|
+
end
|
103
|
+
[columns, Google::Protobuf::Value.new(list_value:
|
104
|
+
Google::Protobuf::ListValue.new(
|
105
|
+
values: serialized_values.map do |value|
|
106
|
+
Google::Cloud::Spanner::Convert.object_to_grpc_value value
|
107
|
+
end
|
108
|
+
))]
|
109
|
+
end
|
110
|
+
private_class_method :_create_grpc_values_for_insert
|
111
|
+
|
112
|
+
def _update_row attribute_names, attempted_action = "update"
|
113
|
+
return super unless Base.connection&.current_spanner_transaction&.isolation == :buffered_mutations
|
114
|
+
|
115
|
+
if locking_enabled?
|
116
|
+
_execute_version_check attempted_action
|
117
|
+
attribute_names << self.class.locking_column
|
118
|
+
self[self.class.locking_column] += 1
|
119
|
+
end
|
120
|
+
|
121
|
+
metadata = TableMetadata.new self.class, self.class.arel_table
|
122
|
+
values = attributes_with_values attribute_names
|
123
|
+
columns, grpc_values = _create_grpc_values_for_update metadata, values
|
124
|
+
|
125
|
+
mutation = Google::Cloud::Spanner::V1::Mutation.new(
|
126
|
+
update: Google::Cloud::Spanner::V1::Mutation::Write.new(
|
127
|
+
table: self.class.arel_table.name,
|
128
|
+
columns: columns,
|
129
|
+
values: [grpc_values.list_value]
|
130
|
+
)
|
131
|
+
)
|
132
|
+
Base.connection.current_spanner_transaction.buffer mutation
|
133
|
+
1 # Affected rows
|
134
|
+
end
|
135
|
+
|
136
|
+
def _create_grpc_values_for_update metadata, values
|
137
|
+
constraints = {}
|
138
|
+
keys = self.class.primary_and_parent_key
|
139
|
+
keys.each do |key|
|
140
|
+
constraints[key] = attribute_in_database key
|
141
|
+
end
|
142
|
+
|
143
|
+
# Use both the where values + the values that are actually set.
|
144
|
+
all_values = [constraints, values]
|
145
|
+
all_serialized_values = []
|
146
|
+
all_columns = []
|
147
|
+
all_values.each do |h|
|
148
|
+
h.each_pair do |k, v|
|
149
|
+
type = metadata.type k
|
150
|
+
has_serialize_options = type.method(:serialize).arity < 0
|
151
|
+
all_serialized_values << (has_serialize_options ? type.serialize(v, :mutation) : type.serialize(v))
|
152
|
+
all_columns << metadata.arel_table[k].name
|
153
|
+
end
|
154
|
+
end
|
155
|
+
[all_columns, Google::Protobuf::Value.new(list_value:
|
156
|
+
Google::Protobuf::ListValue.new(
|
157
|
+
values: all_serialized_values.map do |value|
|
158
|
+
Google::Cloud::Spanner::Convert.object_to_grpc_value value
|
159
|
+
end
|
160
|
+
))]
|
161
|
+
end
|
162
|
+
|
163
|
+
def destroy_row
|
164
|
+
return super unless Base.connection&.current_spanner_transaction&.isolation == :buffered_mutations
|
165
|
+
|
166
|
+
_delete_row
|
167
|
+
end
|
168
|
+
|
169
|
+
def _delete_row
|
170
|
+
return super unless Base.connection&.current_spanner_transaction&.isolation == :buffered_mutations
|
171
|
+
if locking_enabled?
|
172
|
+
_execute_version_check "destroy"
|
173
|
+
end
|
174
|
+
|
175
|
+
metadata = TableMetadata.new self.class, self.class.arel_table
|
176
|
+
keys = self.class.primary_and_parent_key
|
177
|
+
serialized_values = serialize_keys metadata, keys
|
178
|
+
list_value = Google::Protobuf::ListValue.new(
|
179
|
+
values: serialized_values.map do |value|
|
180
|
+
Google::Cloud::Spanner::Convert.object_to_grpc_value value
|
181
|
+
end
|
182
|
+
)
|
183
|
+
mutation = Google::Cloud::Spanner::V1::Mutation.new(
|
184
|
+
delete: Google::Cloud::Spanner::V1::Mutation::Delete.new(
|
185
|
+
table: self.class.arel_table.name,
|
186
|
+
key_set: { keys: [list_value] }
|
187
|
+
)
|
188
|
+
)
|
189
|
+
Base.connection.current_spanner_transaction.buffer mutation
|
190
|
+
1 # Affected rows
|
191
|
+
end
|
192
|
+
|
193
|
+
def serialize_keys metadata, keys
|
194
|
+
serialized_values = []
|
195
|
+
keys.each do |key|
|
196
|
+
type = metadata.type key
|
197
|
+
has_serialize_options = type.method(:serialize).arity < 0
|
198
|
+
serialized_values << type.serialize(attribute_in_database(key), :mutation) if has_serialize_options
|
199
|
+
serialized_values << type.serialize(attribute_in_database(key)) unless has_serialize_options
|
200
|
+
end
|
201
|
+
serialized_values
|
202
|
+
end
|
203
|
+
|
204
|
+
def _execute_version_check attempted_action
|
205
|
+
locking_column = self.class.locking_column
|
206
|
+
previous_lock_value = read_attribute_before_type_cast locking_column
|
207
|
+
|
208
|
+
# We need to check the version using a SELECT query, as a mutation cannot include a WHERE clause.
|
209
|
+
sql = "SELECT 1 FROM `#{self.class.arel_table.name}` " \
|
210
|
+
"WHERE `#{self.class.primary_key}` = @id AND `#{locking_column}` = @lock_version"
|
211
|
+
params = { "id" => id_in_database, "lock_version" => previous_lock_value }
|
212
|
+
param_types = { "id" => :INT64, "lock_version" => :INT64 }
|
213
|
+
locked_row = Base.connection.raw_connection.execute_query sql, params: params, types: param_types
|
214
|
+
raise ActiveRecord::StaleObjectError.new(self, attempted_action) unless locked_row.rows.any?
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
@@ -0,0 +1,324 @@
|
|
1
|
+
# Copyright 2020 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 "google/cloud/spanner"
|
8
|
+
require "spanner_client_ext"
|
9
|
+
require "activerecord_spanner_adapter/information_schema"
|
10
|
+
|
11
|
+
module ActiveRecordSpannerAdapter
|
12
|
+
class Connection
|
13
|
+
attr_reader :instance_id, :database_id, :spanner
|
14
|
+
attr_accessor :current_transaction
|
15
|
+
|
16
|
+
def initialize config
|
17
|
+
@instance_id = config[:instance]
|
18
|
+
@database_id = config[:database]
|
19
|
+
@spanner = self.class.spanners config
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.spanners config
|
23
|
+
config = config.symbolize_keys
|
24
|
+
@spanners ||= {}
|
25
|
+
@mutex ||= Mutex.new
|
26
|
+
@mutex.synchronize do
|
27
|
+
@spanners[database_path(config)] ||= Google::Cloud::Spanner.new(
|
28
|
+
project_id: config[:project],
|
29
|
+
credentials: config[:credentials],
|
30
|
+
emulator_host: config[:emulator_host],
|
31
|
+
scope: config[:scope],
|
32
|
+
timeout: config[:timeout],
|
33
|
+
lib_name: "spanner-activerecord-adapter",
|
34
|
+
lib_version: ActiveRecordSpannerAdapter::VERSION
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.information_schema config
|
40
|
+
@information_schemas ||= {}
|
41
|
+
@information_schemas[database_path(config)] ||= \
|
42
|
+
ActiveRecordSpannerAdapter::InformationSchema.new new(config)
|
43
|
+
end
|
44
|
+
|
45
|
+
def session
|
46
|
+
@last_used = Time.current
|
47
|
+
@session ||= spanner.create_session instance_id, database_id
|
48
|
+
end
|
49
|
+
alias connect! session
|
50
|
+
|
51
|
+
def active?
|
52
|
+
# This method should not initialize a session.
|
53
|
+
unless @session
|
54
|
+
return false
|
55
|
+
end
|
56
|
+
# Assume that it is still active if it has been used in the past 50 minutes.
|
57
|
+
if ((Time.current - @last_used) / 60).round < 50
|
58
|
+
return true
|
59
|
+
end
|
60
|
+
session.execute_query "SELECT 1"
|
61
|
+
true
|
62
|
+
rescue StandardError
|
63
|
+
false
|
64
|
+
end
|
65
|
+
|
66
|
+
def disconnect!
|
67
|
+
session.release!
|
68
|
+
true
|
69
|
+
ensure
|
70
|
+
@session = nil
|
71
|
+
end
|
72
|
+
|
73
|
+
def reset!
|
74
|
+
disconnect!
|
75
|
+
session
|
76
|
+
true
|
77
|
+
end
|
78
|
+
|
79
|
+
# Database Operations
|
80
|
+
|
81
|
+
def create_database
|
82
|
+
job = spanner.create_database instance_id, database_id
|
83
|
+
job.wait_until_done!
|
84
|
+
raise Google::Cloud::Error.from_error job.error if job.error?
|
85
|
+
job.database
|
86
|
+
end
|
87
|
+
|
88
|
+
def database
|
89
|
+
@database ||= begin
|
90
|
+
database = spanner.database instance_id, database_id
|
91
|
+
unless database
|
92
|
+
raise ActiveRecord::NoDatabaseError(
|
93
|
+
"#{spanner.project}/#{instance_id}/#{database_id}"
|
94
|
+
)
|
95
|
+
end
|
96
|
+
database
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# DDL Statements
|
101
|
+
|
102
|
+
# @params [Array<String>, String] sql Single or list of statements
|
103
|
+
def execute_ddl statements, operation_id: nil, wait_until_done: true
|
104
|
+
raise "DDL cannot be executed during a transaction" if current_transaction&.active?
|
105
|
+
self.current_transaction = nil
|
106
|
+
|
107
|
+
statements = Array statements
|
108
|
+
return unless statements.any?
|
109
|
+
|
110
|
+
# If a DDL batch is active we only buffer the statements on the connection until the batch is run.
|
111
|
+
if @ddl_batch
|
112
|
+
@ddl_batch.push(*statements)
|
113
|
+
return true
|
114
|
+
end
|
115
|
+
|
116
|
+
execute_ddl_statements statements, operation_id, wait_until_done
|
117
|
+
end
|
118
|
+
|
119
|
+
# DDL Batching
|
120
|
+
|
121
|
+
##
|
122
|
+
# Executes a set of DDL statements as one batch. This method raises an error if no block is given.
|
123
|
+
#
|
124
|
+
# @example
|
125
|
+
# connection.ddl_batch do
|
126
|
+
# connection.execute_ddl "CREATE TABLE `Users` (Id INT64, Name STRING(MAX)) PRIMARY KEY (Id)"
|
127
|
+
# connection.execute_ddl "CREATE INDEX Idx_Users_Name ON `Users` (Name)"
|
128
|
+
# end
|
129
|
+
def ddl_batch
|
130
|
+
raise Google::Cloud::FailedPreconditionError, "No block given for the DDL batch" unless block_given?
|
131
|
+
begin
|
132
|
+
start_batch_ddl
|
133
|
+
yield
|
134
|
+
run_batch
|
135
|
+
rescue StandardError
|
136
|
+
abort_batch
|
137
|
+
raise
|
138
|
+
ensure
|
139
|
+
@ddl_batch = nil
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
##
|
144
|
+
# Returns true if this connection is currently executing a DDL batch, and otherwise false.
|
145
|
+
def ddl_batch?
|
146
|
+
return true if @ddl_batch
|
147
|
+
false
|
148
|
+
end
|
149
|
+
|
150
|
+
##
|
151
|
+
# Starts a manual DDL batch. The batch must be ended by calling either run_batch or abort_batch.
|
152
|
+
#
|
153
|
+
# @example
|
154
|
+
# begin
|
155
|
+
# connection.start_batch_ddl
|
156
|
+
# connection.execute_ddl "CREATE TABLE `Users` (Id INT64, Name STRING(MAX)) PRIMARY KEY (Id)"
|
157
|
+
# connection.execute_ddl "CREATE INDEX Idx_Users_Name ON `Users` (Name)"
|
158
|
+
# connection.run_batch
|
159
|
+
# rescue StandardError
|
160
|
+
# connection.abort_batch
|
161
|
+
# raise
|
162
|
+
# end
|
163
|
+
def start_batch_ddl
|
164
|
+
if @ddl_batch
|
165
|
+
raise Google::Cloud::FailedPreconditionError, "A DDL batch is already active on this connection"
|
166
|
+
end
|
167
|
+
@ddl_batch = []
|
168
|
+
end
|
169
|
+
|
170
|
+
##
|
171
|
+
# Aborts the current batch on this connection. This is a no-op if there is no batch on this connection.
|
172
|
+
#
|
173
|
+
# @see start_batch_ddl
|
174
|
+
def abort_batch
|
175
|
+
@ddl_batch = nil
|
176
|
+
end
|
177
|
+
|
178
|
+
##
|
179
|
+
# Runs the current batch on this connection. This will raise a FailedPreconditionError if there is no
|
180
|
+
# active batch on this connection.
|
181
|
+
#
|
182
|
+
# @see start_batch_ddl
|
183
|
+
def run_batch
|
184
|
+
unless @ddl_batch
|
185
|
+
raise Google::Cloud::FailedPreconditionError, "There is no batch active on this connection"
|
186
|
+
end
|
187
|
+
# Just return if the batch is empty.
|
188
|
+
return true if @ddl_batch.empty?
|
189
|
+
begin
|
190
|
+
execute_ddl_statements @ddl_batch, nil, true
|
191
|
+
ensure
|
192
|
+
@ddl_batch = nil
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# DQL, DML Statements
|
197
|
+
|
198
|
+
def execute_query sql, params: nil, types: nil
|
199
|
+
if params
|
200
|
+
converted_params, types = \
|
201
|
+
Google::Cloud::Spanner::Convert.to_input_params_and_types(
|
202
|
+
params, types
|
203
|
+
)
|
204
|
+
end
|
205
|
+
|
206
|
+
# Clear the transaction from the previous statement.
|
207
|
+
unless current_transaction&.active?
|
208
|
+
self.current_transaction = nil
|
209
|
+
end
|
210
|
+
|
211
|
+
begin
|
212
|
+
session.execute_query \
|
213
|
+
sql,
|
214
|
+
params: converted_params,
|
215
|
+
types: types,
|
216
|
+
transaction: transaction_selector,
|
217
|
+
seqno: (current_transaction&.next_sequence_number)
|
218
|
+
rescue Google::Cloud::AbortedError
|
219
|
+
# Mark the current transaction as aborted to prevent any unnecessary further requests on the transaction.
|
220
|
+
current_transaction&.mark_aborted
|
221
|
+
raise
|
222
|
+
rescue Google::Cloud::NotFoundError => e
|
223
|
+
if session_not_found?(e) || transaction_not_found?(e)
|
224
|
+
reset!
|
225
|
+
# Force a retry of the entire transaction if this statement was executed as part of a transaction.
|
226
|
+
# Otherwise, just retry the statement itself.
|
227
|
+
raise_aborted_err if current_transaction&.active?
|
228
|
+
retry
|
229
|
+
end
|
230
|
+
raise
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Transactions
|
235
|
+
|
236
|
+
def begin_transaction isolation = nil
|
237
|
+
raise "Nested transactions are not allowed" if current_transaction&.active?
|
238
|
+
self.current_transaction = Transaction.new self, isolation
|
239
|
+
current_transaction.begin
|
240
|
+
current_transaction
|
241
|
+
end
|
242
|
+
|
243
|
+
def commit_transaction
|
244
|
+
raise "This connection does not have a transaction" unless current_transaction
|
245
|
+
current_transaction.commit
|
246
|
+
end
|
247
|
+
|
248
|
+
def rollback_transaction
|
249
|
+
raise "This connection does not have a transaction" unless current_transaction
|
250
|
+
current_transaction.rollback
|
251
|
+
end
|
252
|
+
|
253
|
+
def transaction_selector
|
254
|
+
return current_transaction&.transaction_selector if current_transaction&.active?
|
255
|
+
end
|
256
|
+
|
257
|
+
def truncate table_name
|
258
|
+
session.delete table_name
|
259
|
+
end
|
260
|
+
|
261
|
+
def self.database_path config
|
262
|
+
"#{config[:emulator_host]}/#{config[:project]}/#{config[:instance]}/#{config[:database]}"
|
263
|
+
end
|
264
|
+
|
265
|
+
def session_not_found? err
|
266
|
+
if err.respond_to?(:metadata) && err.metadata["google.rpc.resourceinfo-bin"]
|
267
|
+
resource_info = Google::Rpc::ResourceInfo.decode err.metadata["google.rpc.resourceinfo-bin"]
|
268
|
+
type = resource_info["resource_type"]
|
269
|
+
return "type.googleapis.com/google.spanner.v1.Session".eql? type
|
270
|
+
end
|
271
|
+
false
|
272
|
+
end
|
273
|
+
|
274
|
+
def transaction_not_found? err
|
275
|
+
if err.respond_to?(:metadata) && err.metadata["google.rpc.resourceinfo-bin"]
|
276
|
+
resource_info = Google::Rpc::ResourceInfo.decode err.metadata["google.rpc.resourceinfo-bin"]
|
277
|
+
type = resource_info["resource_type"]
|
278
|
+
return "type.googleapis.com/google.spanner.v1.Transaction".eql? type
|
279
|
+
end
|
280
|
+
false
|
281
|
+
end
|
282
|
+
|
283
|
+
def raise_aborted_err
|
284
|
+
retry_info = Google::Rpc::RetryInfo.new retry_delay: Google::Protobuf::Duration.new(seconds: 0, nanos: 1)
|
285
|
+
begin
|
286
|
+
raise GRPC::BadStatus.new(
|
287
|
+
GRPC::Core::StatusCodes::ABORTED,
|
288
|
+
"Transaction aborted",
|
289
|
+
"google.rpc.retryinfo-bin": Google::Rpc::RetryInfo.encode(retry_info)
|
290
|
+
)
|
291
|
+
rescue GRPC::BadStatus
|
292
|
+
raise Google::Cloud::AbortedError
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
private
|
297
|
+
|
298
|
+
def execute_ddl_statements statements, operation_id, wait_until_done
|
299
|
+
job = database.update statements: statements, operation_id: operation_id
|
300
|
+
job.wait_until_done! if wait_until_done
|
301
|
+
raise Google::Cloud::Error.from_error job.error if job.error?
|
302
|
+
job.done?
|
303
|
+
end
|
304
|
+
|
305
|
+
##
|
306
|
+
# Retrieves the delay value from Google::Cloud::AbortedError or
|
307
|
+
# GRPC::Aborted
|
308
|
+
def delay_from_aborted err
|
309
|
+
return nil if err.nil?
|
310
|
+
if err.respond_to?(:metadata) && err.metadata["google.rpc.retryinfo-bin"]
|
311
|
+
retry_info = Google::Rpc::RetryInfo.decode err.metadata["google.rpc.retryinfo-bin"]
|
312
|
+
seconds = retry_info["retry_delay"].seconds
|
313
|
+
nanos = retry_info["retry_delay"].nanos
|
314
|
+
return seconds if nanos.zero?
|
315
|
+
return seconds + (nanos / 1_000_000_000.0)
|
316
|
+
end
|
317
|
+
# No metadata? Try the inner error
|
318
|
+
delay_from_aborted err.cause
|
319
|
+
rescue StandardError
|
320
|
+
# Any error indicates the backoff should be handled elsewhere
|
321
|
+
nil
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|