activerecord-spanner-adapter 0.3.0 → 0.5.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 +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,42 @@
|
|
|
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
|
+
require "io/console"
|
|
8
|
+
require_relative "../config/environment"
|
|
9
|
+
require_relative "models/meeting"
|
|
10
|
+
|
|
11
|
+
class Application
|
|
12
|
+
def self.run
|
|
13
|
+
# Set the default local timezone.
|
|
14
|
+
Time.zone = "Europe/Lisbon"
|
|
15
|
+
|
|
16
|
+
# Create a meeting using the local timezone. The timezone information will not be stored in the `meeting_time`
|
|
17
|
+
# column in the database, which is why we also include a separate column where we can store the timezone name.
|
|
18
|
+
meeting_time = Time.zone.local 2021, 7, 1, 10, 30, 0
|
|
19
|
+
meeting = Meeting.create title: "Standup", meeting_time: meeting_time, meeting_timezone: Time.zone.name
|
|
20
|
+
|
|
21
|
+
# The meeting_time is saved in UTC in Cloud Spanner. Reloading it will therefore lose the timezone information in
|
|
22
|
+
# the meeting_time attribute. It is however stored in the separate meeting_timezone attribute, and that can be used
|
|
23
|
+
# to reconstruct the meeting_time in the timezone where the meeting was planned.
|
|
24
|
+
# The Meeting model class also contains two helper methods:
|
|
25
|
+
# 1. `local_meeting_time`: Returns the meeting_time in the local timezone.
|
|
26
|
+
# 2. `meeting_time_in_planned_zone`: Returns the meeting_time in the timezone where it is planned.
|
|
27
|
+
meeting.reload
|
|
28
|
+
puts ""
|
|
29
|
+
puts "#{'Meeting time in UTC:'.ljust 60} #{meeting.meeting_time}"
|
|
30
|
+
puts "#{'Meeting time in the timezone where it was planned:'.ljust 60} #{meeting.meeting_time_in_planned_zone}"
|
|
31
|
+
|
|
32
|
+
# Simulate that the application is now running in the timezone America/Los_Angeles.
|
|
33
|
+
Time.zone = "America/Los_Angeles"
|
|
34
|
+
puts "#{'Meeting time in the local timezone (America/Los_Angeles):'.ljust 60} #{meeting.local_meeting_time}"
|
|
35
|
+
|
|
36
|
+
puts ""
|
|
37
|
+
puts "Press any key to end the application"
|
|
38
|
+
STDIN.getch
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
Application.run
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
class CreateTables < ActiveRecord::Migration[6.0]
|
|
8
|
+
def change
|
|
9
|
+
connection.ddl_batch do
|
|
10
|
+
create_table :meetings do |t|
|
|
11
|
+
t.string :title
|
|
12
|
+
# A `TIMESTAMP` column in Cloud Spanner contains a date/time value that designates a specific point in time. The
|
|
13
|
+
# value is always stored in UTC. If you specify a date/time value in a different timezone, the value is
|
|
14
|
+
# converted to UTC when saving it to the database. You can use a separate column to store the timezone of the
|
|
15
|
+
# timestamp if that is vital for your application, and use that information when the timestamp is read back.
|
|
16
|
+
t.datetime :meeting_time
|
|
17
|
+
t.string :meeting_timezone
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
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 `rails
|
|
6
|
+
# db:schema:load`. When creating a new database, `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.define(version: 1) do
|
|
14
|
+
|
|
15
|
+
create_table "meetings", force: :cascade do |t|
|
|
16
|
+
t.string "title"
|
|
17
|
+
t.time "meeting_time"
|
|
18
|
+
t.string "meeting_timezone"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
class Meeting < ActiveRecord::Base
|
|
8
|
+
# Returns the meeting time in the local timezone.
|
|
9
|
+
def local_meeting_time
|
|
10
|
+
return unless meeting_time && Time.zone
|
|
11
|
+
meeting_time.in_time_zone Time.zone
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Returns the time of the meeting in the timezone where the meeting is planned.
|
|
15
|
+
def meeting_time_in_planned_zone
|
|
16
|
+
return unless meeting_time && meeting_timezone
|
|
17
|
+
meeting_time.in_time_zone meeting_timezone
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
## activerecord-spanner-adapter on Solidus
|
|
2
|
+
|
|
3
|
+
This example shows how to use activerecord-spanner-adapter for Cloud Spanner as a backend database for https://solidus.io.
|
|
4
|
+
|
|
5
|
+
#### Table of contents
|
|
6
|
+
- [Ensure you have a Cloud Spanner database already created](#ensure-you-have-a-Cloud-Spanner-database-already-created)
|
|
7
|
+
- [Create a Rails application and install Solidus](#create-a-rails-application-and-install-solidus)
|
|
8
|
+
- [Update database.yml](#update-database.yml)
|
|
9
|
+
- [Create application database](#create-application-database)
|
|
10
|
+
- [Apply the migrations](#apply-the-migrations)
|
|
11
|
+
- [Go view the results on Cloud Spanner UI](#go-view-the-results-on-Cloud-Spanner-UI)
|
|
12
|
+
- [Run the application](#run-the-application)
|
|
13
|
+
- [References](#references)
|
|
14
|
+
|
|
15
|
+
### Ensure you have a Cloud Spanner database already created
|
|
16
|
+
If you haven't already, please follow the steps to install [Cloud Spanner](https://cloud.google.com/spanner/docs/getting-started/set-up),
|
|
17
|
+
or visit this [codelab](https://opencensus.io/codelabs/spanner/#0)
|
|
18
|
+
|
|
19
|
+
**You'll need to ensure that your Google Application Default Credentials are properly downloaded and saved in your environment.**
|
|
20
|
+
|
|
21
|
+
### Create a Rails application and install Solidus
|
|
22
|
+
|
|
23
|
+
Create a Rails application.
|
|
24
|
+
```shell
|
|
25
|
+
rails new e-store
|
|
26
|
+
cd e-store
|
|
27
|
+
```
|
|
28
|
+
Add `solidus` and `activerecord-spanner-adapter` gem to Gemfile.
|
|
29
|
+
|
|
30
|
+
Gemfile
|
|
31
|
+
```ruby
|
|
32
|
+
gem 'solidus'
|
|
33
|
+
gem 'activerecord-spanner-adapter', git: 'https://github.com/orijtech/activerecord-spanner-adapter.git'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```shell
|
|
37
|
+
bundle install
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Use Cloud Spanner adapter in Gemfile
|
|
41
|
+
Edit Gemfile file and add `activerecord-spanner-adapter` gem.
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
gem 'activerecord-spanner-adapter', git: 'https://github.com/orijtech/activerecord-spanner-adapter.git'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Install gems.
|
|
48
|
+
|
|
49
|
+
```shell
|
|
50
|
+
bundle install
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
After installing gems, you'll have to run the generator to create necessary configuration files and migrations.
|
|
54
|
+
|
|
55
|
+
```shell
|
|
56
|
+
bin/rails g spree:install
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Update database.yml
|
|
60
|
+
|
|
61
|
+
Update `db/database.yml` to use `activerecord-spanner-adapter`
|
|
62
|
+
|
|
63
|
+
After we have a Cloud Spanner database created, we'll need a few variables:
|
|
64
|
+
* Project id
|
|
65
|
+
* Instance id
|
|
66
|
+
* Database name
|
|
67
|
+
* Credential: Credential keyfile path or Export `GOOGLE_CLOUD_KEYFILE`environment variable.
|
|
68
|
+
|
|
69
|
+
Once in, please edit the file `config/database.yml` and make the section `DATABASES` into the following:
|
|
70
|
+
|
|
71
|
+
```yml
|
|
72
|
+
default: &default
|
|
73
|
+
adapter: "spanner"
|
|
74
|
+
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 10 } %>
|
|
75
|
+
project: PROJECT_ID
|
|
76
|
+
instance: SPANNER_INSTANCE_ID
|
|
77
|
+
credentials: SPANNER_PROJECT_KEYFILE
|
|
78
|
+
|
|
79
|
+
development:
|
|
80
|
+
<<: *default
|
|
81
|
+
database: e-store-dev
|
|
82
|
+
|
|
83
|
+
test:
|
|
84
|
+
<<: *default
|
|
85
|
+
database: e-store-test
|
|
86
|
+
|
|
87
|
+
production:
|
|
88
|
+
<<: *default
|
|
89
|
+
database: e-store
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
and for example here is a filled in database where:
|
|
93
|
+
|
|
94
|
+
* `PROJECT_ID`: spanner-appdev
|
|
95
|
+
* `SPANNER_INSTANCE_ID`: instance
|
|
96
|
+
* `SPANNER_PROJECT_KEYFILE`: credentials key file path. i.e "/app/keyfile.json".
|
|
97
|
+
|
|
98
|
+
### Create application database
|
|
99
|
+
|
|
100
|
+
Please run:
|
|
101
|
+
```shell
|
|
102
|
+
$ ./bin/rails db:create
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```shell
|
|
106
|
+
Created database e-store-dev
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Apply the migrations
|
|
110
|
+
Please run:
|
|
111
|
+
```shell
|
|
112
|
+
$ ./bin/rails db:migrate
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
and that'll take a while running, it will look like the following
|
|
116
|
+
|
|
117
|
+
<details>
|
|
118
|
+
|
|
119
|
+
```shell
|
|
120
|
+
$ ./bin/rails db:migrate
|
|
121
|
+
== 20200410055855 CreateActiveStorageTables: migrating ========================
|
|
122
|
+
-- create_table(:active_storage_blobs, {})
|
|
123
|
+
-> 0.0011s
|
|
124
|
+
-- create_table(:active_storage_attachments, {})
|
|
125
|
+
-> 0.0010s
|
|
126
|
+
== 20200410055855 CreateActiveStorageTables: migrated (0.0023s) ===============
|
|
127
|
+
|
|
128
|
+
..... MANY MORE MIGRATION LOG LINES
|
|
129
|
+
```
|
|
130
|
+
</details>
|
|
131
|
+
|
|
132
|
+
### Go view the results on Cloud Spanner UI
|
|
133
|
+
|
|
134
|
+
To double check that the respective tables and migrations were performed, please go visit the page with your database on Cloud Spanner's UI. For example it should look like this
|
|
135
|
+
|
|
136
|
+

|
|
137
|
+
|
|
138
|
+
### Run the application
|
|
139
|
+
After those migrations run application server.
|
|
140
|
+
|
|
141
|
+
```shell
|
|
142
|
+
./bin/rails s
|
|
143
|
+
```
|
|
144
|
+
<details>
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
./bin/rails s
|
|
148
|
+
Puma starting in single mode...
|
|
149
|
+
* Version 4.3.3 (ruby 2.6.3-p62), codename: Mysterious Traveller
|
|
150
|
+
* Min threads: 5, max threads: 5
|
|
151
|
+
* Environment: development
|
|
152
|
+
* Listening on tcp://127.0.0.1:3000
|
|
153
|
+
* Listening on tcp://[::1]:3000
|
|
154
|
+
Use Ctrl-C to stop
|
|
155
|
+
Started GET "/" for ::1 at 2020-04-10 11:42:00 +0530
|
|
156
|
+
(563.0ms) SELECT `schema_migrations`.`version` FROM `schema_migrations` ORDER BY `schema_migrations`.`version` ASC
|
|
157
|
+
Processing by Spree::HomeController#index as HTML
|
|
158
|
+
Spree::Store Load (507.9ms) SELECT `spree_stores`.* FROM `spree_stores` WHERE (`spree_stores`.`url` = 'localhost' OR `spree_stores`.`default` = TRUE) ORDER BY `spree_stores`.`default` ASC LIMIT 1
|
|
159
|
+
Rendering /Users/jiren/work/spanner_orm/solidus/frontend/app/views/spree/home/index.html.erb within spree/layouts/spree_application
|
|
160
|
+
Spree::Taxonomy Load (571.6ms) SELECT `spree_taxonomies`.* FROM `spree_taxonomies` ORDER BY `spree_taxonomies`.`position` ASC
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
</details>
|
|
164
|
+
|
|
165
|
+
### References
|
|
166
|
+
|
|
167
|
+
Resource|URL
|
|
168
|
+
---|---
|
|
169
|
+
Solidus application|https://solidus.io/
|
|
170
|
+
Solidus application source code|https://github.com/solidusio/solidus
|
|
171
|
+
Cloud Spanner homepage|https://cloud.google.com/spanner/
|
|
172
|
+
activerecord-spanner-adapter project's source code|https://github.com/orijtech/activerecord-spanner-adapter
|
|
@@ -1,335 +1,290 @@
|
|
|
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
|
+
|
|
1
9
|
module ActiveRecord
|
|
2
10
|
module ConnectionAdapters
|
|
3
11
|
module Spanner
|
|
4
12
|
module DatabaseStatements
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class QueryVisitor < ::Arel::Visitors::ToSql
|
|
8
|
-
def visit_Arel_Nodes_BindParam(o, collector)
|
|
9
|
-
collector.add_bind(o) {|bind_idx| "@p#{bind_idx}" }
|
|
10
|
-
end
|
|
11
|
-
end
|
|
13
|
+
# DDL, DML and DQL Statements
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def visit_Arel_Nodes_BindParam(o)
|
|
16
|
-
binds.shift
|
|
17
|
-
end
|
|
15
|
+
def execute sql, name = nil, binds = []
|
|
16
|
+
statement_type = sql_statement_type sql
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
if preventing_writes? && [:dml, :ddl].include?(statement_type)
|
|
19
|
+
raise ActiveRecord::ReadOnlyError(
|
|
20
|
+
"Write query attempted while in readonly mode: #{sql}"
|
|
21
|
+
)
|
|
21
22
|
end
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
if statement_type == :ddl
|
|
25
|
+
execute_ddl sql
|
|
26
|
+
else
|
|
27
|
+
transaction_required = statement_type == :dml
|
|
28
|
+
materialize_transactions
|
|
29
|
+
|
|
30
|
+
log sql, name do
|
|
31
|
+
types, params = to_types_and_params binds
|
|
32
|
+
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
|
|
33
|
+
if transaction_required
|
|
34
|
+
transaction do
|
|
35
|
+
@connection.execute_query sql, params: params, types: types
|
|
36
|
+
end
|
|
37
|
+
else
|
|
38
|
+
@connection.execute_query sql, params: params, types: types
|
|
39
|
+
end
|
|
40
|
+
end
|
|
29
41
|
end
|
|
30
42
|
end
|
|
31
|
-
|
|
32
|
-
private
|
|
33
|
-
# To be overridden
|
|
34
|
-
def binds
|
|
35
|
-
raise NotImplementedError
|
|
36
|
-
end
|
|
37
43
|
end
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
include RightValueResolveable
|
|
43
|
-
|
|
44
|
-
def initialize(schema_reader, binds)
|
|
45
|
-
super()
|
|
46
|
-
@schema_reader = schema_reader
|
|
47
|
-
@binds = binds
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
attr_reader :binds
|
|
51
|
-
private :binds
|
|
52
|
-
|
|
53
|
-
def visit_Arel_Nodes_InsertStatement(o)
|
|
54
|
-
raise NotImplementedError, 'INSERT INTO SELECT statement is not supported' if o.select
|
|
55
|
-
table = o.relation.name
|
|
56
|
-
columns = if o.columns.any?
|
|
57
|
-
o.columns.map(&:name)
|
|
58
|
-
else
|
|
59
|
-
columns(table).map(&:name)
|
|
60
|
-
end
|
|
61
|
-
values = o.values ? accept(o.values) : []
|
|
62
|
-
|
|
63
|
-
[table, columns, values]
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def visit_Arel_Nodes_UpdateStatement(o)
|
|
67
|
-
table = o.relation.name
|
|
68
|
-
values = accept(o.values)
|
|
69
|
-
pk = @schema_reader.primary_key(table)
|
|
45
|
+
def query sql, name = nil
|
|
46
|
+
exec_query sql, name
|
|
47
|
+
end
|
|
70
48
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
49
|
+
def exec_query sql, name = "SQL", binds = [], prepare: false # rubocop:disable Lint/UnusedMethodArgument
|
|
50
|
+
result = execute sql, name, binds
|
|
51
|
+
ActiveRecord::Result.new(
|
|
52
|
+
result.fields.keys.map(&:to_s), result.rows.map(&:values)
|
|
53
|
+
)
|
|
54
|
+
end
|
|
74
55
|
|
|
75
|
-
|
|
76
|
-
|
|
56
|
+
def exec_mutation mutation
|
|
57
|
+
@connection.current_transaction.buffer mutation
|
|
58
|
+
end
|
|
77
59
|
|
|
78
|
-
|
|
60
|
+
def update arel, name = nil, binds = []
|
|
61
|
+
# Add a `WHERE TRUE` if it is an update_all or delete_all call that uses DML.
|
|
62
|
+
if !should_use_mutation(arel) && arel.respond_to?(:ast) && arel.ast.wheres.empty?
|
|
63
|
+
arel.ast.wheres << Arel::Nodes::SqlLiteral.new("TRUE")
|
|
79
64
|
end
|
|
65
|
+
return super unless should_use_mutation arel
|
|
80
66
|
|
|
81
|
-
|
|
82
|
-
table = o.relation.name
|
|
67
|
+
raise "Unsupported update for use with mutations: #{arel}" unless arel.is_a? Arel::DeleteManager
|
|
83
68
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
end
|
|
105
|
-
end
|
|
69
|
+
exec_mutation create_delete_all_mutation arel if arel.is_a? Arel::DeleteManager
|
|
70
|
+
0 # Affected rows (unknown)
|
|
71
|
+
end
|
|
72
|
+
alias delete update
|
|
73
|
+
|
|
74
|
+
def exec_update sql, name = "SQL", binds = []
|
|
75
|
+
result = execute sql, name, binds
|
|
76
|
+
# Make sure that we consume the entire result stream before trying to get the stats.
|
|
77
|
+
# This is required because the ExecuteStreamingSql RPC is also used for (Partitioned) DML,
|
|
78
|
+
# and this RPC can return multiple partial result sets for DML as well. Only the last partial
|
|
79
|
+
# result set will contain the statistics. Although there will never be any rows, this makes
|
|
80
|
+
# sure that the stream is fully consumed.
|
|
81
|
+
result.rows.each { |_| }
|
|
82
|
+
return result.row_count if result.row_count
|
|
83
|
+
|
|
84
|
+
raise ActiveRecord::StatementInvalid.new(
|
|
85
|
+
"DML statement is invalid.", sql: sql
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
alias exec_delete exec_update
|
|
106
89
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
raise NotImplementedError, "mutation with SQL literal is not supported"
|
|
112
|
-
else
|
|
113
|
-
accept(value)
|
|
114
|
-
end
|
|
90
|
+
def truncate table_name, name = nil
|
|
91
|
+
Array(table_name).each do |t|
|
|
92
|
+
log "TRUNCATE #{t}", name do
|
|
93
|
+
@connection.truncate t
|
|
115
94
|
end
|
|
116
95
|
end
|
|
117
|
-
|
|
118
|
-
def visit_Arel_Nodes_Assignment(o)
|
|
119
|
-
[accept(o.left), accept(o.right)]
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def visit_Arel_Nodes_UnqualifiedColumn(o)
|
|
123
|
-
o.name
|
|
124
|
-
end
|
|
125
96
|
end
|
|
126
97
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
include RightValueResolveable
|
|
131
|
-
|
|
132
|
-
NOT_SIMPLE = nil
|
|
133
|
-
|
|
134
|
-
def initialize(relation, pk, binds)
|
|
135
|
-
super()
|
|
136
|
-
@relation = relation
|
|
137
|
-
@pk = pk
|
|
138
|
-
@binds = binds
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
attr_reader :binds
|
|
142
|
-
private :binds
|
|
143
|
-
|
|
144
|
-
def visit_Arel_Nodes_And(o)
|
|
145
|
-
if o.children.size == 1
|
|
146
|
-
accept(o.left)
|
|
147
|
-
else
|
|
148
|
-
NOT_SIMPLE
|
|
149
|
-
end
|
|
150
|
-
end
|
|
98
|
+
def write_query? sql
|
|
99
|
+
sql_statement_type(sql) == :dml
|
|
100
|
+
end
|
|
151
101
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
NOT_SIMPLE
|
|
102
|
+
def execute_ddl statements
|
|
103
|
+
log "MIGRATION", "SCHEMA" do
|
|
104
|
+
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
|
|
105
|
+
@connection.execute_ddl statements
|
|
157
106
|
end
|
|
158
107
|
end
|
|
108
|
+
rescue Google::Cloud::Error => error
|
|
109
|
+
raise ActiveRecord::StatementInvalid, error
|
|
110
|
+
end
|
|
159
111
|
|
|
160
|
-
|
|
161
|
-
return nil unless pk_cond?(o)
|
|
162
|
-
|
|
163
|
-
if o.kind_of?(Array) and o.empty?
|
|
164
|
-
[]
|
|
165
|
-
else
|
|
166
|
-
accept(o.right)
|
|
167
|
-
end
|
|
168
|
-
end
|
|
112
|
+
# Transaction
|
|
169
113
|
|
|
170
|
-
|
|
171
|
-
|
|
114
|
+
def transaction requires_new: nil, isolation: nil, joinable: true
|
|
115
|
+
if !requires_new && current_transaction.joinable?
|
|
116
|
+
return super
|
|
172
117
|
end
|
|
173
118
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
alias visit_Arel_Nodes_LessThan unsupported
|
|
184
|
-
alias visit_Arel_Nodes_Matches unsupported
|
|
185
|
-
alias visit_Arel_Nodes_DoesNotMatch unsupported
|
|
186
|
-
|
|
187
|
-
private
|
|
188
|
-
def pk_cond?(o)
|
|
189
|
-
o.left.kind_of?(Arel::Attributes::Attribute) &&
|
|
190
|
-
o.left.relation == @relation &&
|
|
191
|
-
o.left.name == @pk
|
|
119
|
+
backoff = 0.2
|
|
120
|
+
begin
|
|
121
|
+
super
|
|
122
|
+
rescue ActiveRecord::StatementInvalid => err
|
|
123
|
+
if err.cause.is_a? Google::Cloud::AbortedError
|
|
124
|
+
sleep(delay_from_aborted(err) || backoff *= 1.3)
|
|
125
|
+
retry
|
|
126
|
+
end
|
|
127
|
+
raise
|
|
192
128
|
end
|
|
193
129
|
end
|
|
194
130
|
|
|
195
|
-
def
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
out
|
|
131
|
+
def transaction_isolation_levels
|
|
132
|
+
{
|
|
133
|
+
read_uncommitted: "READ UNCOMMITTED",
|
|
134
|
+
read_committed: "READ COMMITTED",
|
|
135
|
+
repeatable_read: "REPEATABLE READ",
|
|
136
|
+
serializable: "SERIALIZABLE",
|
|
137
|
+
|
|
138
|
+
# These are not really isolation levels, but it is the only (best) way to pass in additional
|
|
139
|
+
# transaction options to the connection.
|
|
140
|
+
read_only: "READ_ONLY",
|
|
141
|
+
buffered_mutations: "BUFFERED_MUTATIONS"
|
|
207
142
|
}
|
|
143
|
+
end
|
|
208
144
|
|
|
209
|
-
|
|
210
|
-
|
|
145
|
+
def begin_db_transaction
|
|
146
|
+
log "BEGIN" do
|
|
147
|
+
@connection.begin_transaction
|
|
211
148
|
end
|
|
212
|
-
|
|
213
|
-
id_value
|
|
214
149
|
end
|
|
215
150
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
151
|
+
# Begins a transaction on the database with the specified isolation level. Cloud Spanner only supports
|
|
152
|
+
# isolation level :serializable, but also defines two additional 'isolation levels' that can be used
|
|
153
|
+
# to start specific types of Spanner transactions:
|
|
154
|
+
# * :read_only: Starts a read-only snapshot transaction using a strong timestamp bound.
|
|
155
|
+
# * :buffered_mutations: Starts a read/write transaction that will use mutations instead of DML for single-row
|
|
156
|
+
# inserts/updates/deletes. Mutations are buffered locally until the transaction is
|
|
157
|
+
# committed, and any changes during a transaction cannot be read by the application.
|
|
158
|
+
# * :pdml: Starts a Partitioned DML transaction. Executing multiple DML statements in one PDML transaction
|
|
159
|
+
# block is NOT supported A PDML transaction is not guaranteed to be atomic.
|
|
160
|
+
# See https://cloud.google.com/spanner/docs/dml-partitioned for more information.
|
|
161
|
+
def begin_isolated_db_transaction isolation
|
|
162
|
+
raise "Unsupported isolation level: #{isolation}" unless \
|
|
163
|
+
[:serializable, :read_only, :buffered_mutations, :pdml].include? isolation
|
|
164
|
+
|
|
165
|
+
log "BEGIN #{isolation}" do
|
|
166
|
+
@connection.begin_transaction isolation
|
|
231
167
|
end
|
|
168
|
+
end
|
|
232
169
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
log(fake_sql, name, binds) do
|
|
237
|
-
with_phase_transition {|client| client.update(table, rows) }
|
|
170
|
+
def commit_db_transaction
|
|
171
|
+
log "COMMIT" do
|
|
172
|
+
@connection.commit_transaction
|
|
238
173
|
end
|
|
239
|
-
target.size
|
|
240
174
|
end
|
|
241
175
|
|
|
242
|
-
def
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
type_casted_binds = binds.map {|attr| type_cast(attr.value_for_database) }
|
|
246
|
-
table, target, wheres = MutationVisitor.new(self, type_casted_binds.dup).accept(arel.ast)
|
|
247
|
-
|
|
248
|
-
# TODO(yugui) Support composite primary key?
|
|
249
|
-
pk = primary_key(table)
|
|
250
|
-
if target.nil?
|
|
251
|
-
where_clause = visitor.accept(wheres, collector).compile(binds.dup, self)
|
|
252
|
-
# TODO(yugui) keep consistency with transaction
|
|
253
|
-
target = select_values(<<~"SQL", name, binds)
|
|
254
|
-
SELECT #{quote_column_name(pk)} FROM #{quote_table_name(table)} WHERE #{where_clause}
|
|
255
|
-
SQL
|
|
176
|
+
def rollback_db_transaction
|
|
177
|
+
log "ROLLBACK" do
|
|
178
|
+
@connection.rollback_transaction
|
|
256
179
|
end
|
|
180
|
+
end
|
|
257
181
|
|
|
258
|
-
|
|
259
|
-
keyset = []
|
|
260
|
-
fake_sql = "DELETE FROM #{quote_column_name(table)}"
|
|
261
|
-
elsif target.size > 1
|
|
262
|
-
fake_sql = "DELETE FROM #{quote_column_name(table)} WHERE (primary-key) IN ?"
|
|
263
|
-
keyset = target
|
|
264
|
-
else
|
|
265
|
-
fake_sql = "DELETE FROM #{quote_column_name(table)} WHERE (primary-key) = ?"
|
|
266
|
-
keyset = target
|
|
267
|
-
end
|
|
182
|
+
private
|
|
268
183
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
184
|
+
# Translates binds to Spanner types and params.
|
|
185
|
+
def to_types_and_params binds
|
|
186
|
+
types = binds.enum_for(:each_with_index).map do |bind, i|
|
|
187
|
+
type = :INT64
|
|
188
|
+
if bind.respond_to? :type
|
|
189
|
+
type = ActiveRecord::Type::Spanner::SpannerActiveRecordConverter
|
|
190
|
+
.convert_active_model_type_to_spanner(bind.type)
|
|
191
|
+
end
|
|
192
|
+
[
|
|
193
|
+
# Generates binds for named parameters in the format `@p1, @p2, ...`
|
|
194
|
+
"p#{i + 1}", type
|
|
195
|
+
]
|
|
196
|
+
end.to_h
|
|
197
|
+
params = binds.enum_for(:each_with_index).map do |bind, i|
|
|
198
|
+
type = bind.respond_to?(:type) ? bind.type : ActiveModel::Type::Integer
|
|
199
|
+
value = bind
|
|
200
|
+
value = type.serialize bind.value, :dml if type.respond_to?(:serialize) && type.method(:serialize).arity < 0
|
|
201
|
+
value = type.serialize bind.value if type.respond_to?(:serialize) && type.method(:serialize).arity >= 0
|
|
202
|
+
|
|
203
|
+
["p#{i + 1}", value]
|
|
204
|
+
end.to_h
|
|
205
|
+
[types, params]
|
|
206
|
+
end
|
|
272
207
|
|
|
273
|
-
|
|
208
|
+
# An insert/update/delete statement could use mutations in some specific circumstances.
|
|
209
|
+
# This method returns an indication whether a specific operation should use mutations instead of DML
|
|
210
|
+
# based on the operation itself, and the current transaction.
|
|
211
|
+
def should_use_mutation arel
|
|
212
|
+
!@connection.current_transaction.nil? \
|
|
213
|
+
&& @connection.current_transaction.isolation == :buffered_mutations \
|
|
214
|
+
&& can_use_mutation(arel) \
|
|
274
215
|
end
|
|
275
216
|
|
|
276
|
-
def
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
execute_ddl(stmt)
|
|
280
|
-
else
|
|
281
|
-
super(stmt)
|
|
282
|
-
end
|
|
217
|
+
def can_use_mutation arel
|
|
218
|
+
return true if arel.is_a?(Arel::DeleteManager) && arel.respond_to?(:ast) && arel.ast.wheres.empty?
|
|
219
|
+
false
|
|
283
220
|
end
|
|
284
221
|
|
|
285
|
-
def
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
spanner_binds = binds
|
|
289
|
-
binds = binds.values
|
|
290
|
-
when binds.respond_to?(:to_hash)
|
|
291
|
-
spanner_binds = binds.to_hash
|
|
292
|
-
binds = spanner_binds.values
|
|
293
|
-
else
|
|
294
|
-
spanner_binds = binds.each_with_index.inject({}) {|b, (attr, i)|
|
|
295
|
-
b["p#{i+1}"] = type_cast(attr.value_for_database)
|
|
296
|
-
b
|
|
297
|
-
}
|
|
222
|
+
def create_delete_all_mutation arel
|
|
223
|
+
unless arel.is_a? Arel::DeleteManager
|
|
224
|
+
raise "A delete mutation can only be created from a DeleteManager"
|
|
298
225
|
end
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
226
|
+
# Check if it is a delete_all operation.
|
|
227
|
+
unless arel.ast.wheres.empty?
|
|
228
|
+
raise "A delete mutation can only be created without a WHERE clause"
|
|
229
|
+
end
|
|
230
|
+
table_name = arel.ast.relation.name if arel.ast.relation.is_a? Arel::Table
|
|
231
|
+
table_name = arel.ast.relation.left.name if arel.ast.relation.is_a? Arel::Nodes::JoinSource
|
|
232
|
+
unless table_name
|
|
233
|
+
raise "Could not find table for delete mutation"
|
|
307
234
|
end
|
|
308
|
-
end
|
|
309
235
|
|
|
310
|
-
|
|
311
|
-
|
|
236
|
+
Google::Cloud::Spanner::V1::Mutation.new(
|
|
237
|
+
delete: Google::Cloud::Spanner::V1::Mutation::Delete.new(
|
|
238
|
+
table: table_name,
|
|
239
|
+
key_set: { all: true }
|
|
240
|
+
)
|
|
241
|
+
)
|
|
312
242
|
end
|
|
313
243
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
244
|
+
COMMENT_REGEX = %r{(?:--.*\n)*|/\*(?:[^*]|\*[^/])*\*/}m.freeze \
|
|
245
|
+
unless defined? ActiveRecord::ConnectionAdapters::AbstractAdapter::COMMENT_REGEX
|
|
246
|
+
COMMENT_REGEX = ActiveRecord::ConnectionAdapters::AbstractAdapter::COMMENT_REGEX \
|
|
247
|
+
if defined? ActiveRecord::ConnectionAdapters::AbstractAdapter::COMMENT_REGEX
|
|
317
248
|
|
|
318
|
-
def
|
|
319
|
-
|
|
249
|
+
private_class_method def self.build_sql_statement_regexp *parts # :nodoc:
|
|
250
|
+
parts = parts.map { |part| /#{part}/i }
|
|
251
|
+
/\A(?:[\(\s]|#{COMMENT_REGEX})*#{Regexp.union(*parts)}/
|
|
320
252
|
end
|
|
321
253
|
|
|
322
|
-
|
|
323
|
-
|
|
254
|
+
DDL_REGX = build_sql_statement_regexp(:create, :alter, :drop).freeze
|
|
255
|
+
|
|
256
|
+
DML_REGX = build_sql_statement_regexp(:insert, :delete, :update).freeze
|
|
257
|
+
|
|
258
|
+
def sql_statement_type sql
|
|
259
|
+
case sql
|
|
260
|
+
when DDL_REGX
|
|
261
|
+
:ddl
|
|
262
|
+
when DML_REGX
|
|
263
|
+
:dml
|
|
264
|
+
else
|
|
265
|
+
:dql
|
|
266
|
+
end
|
|
324
267
|
end
|
|
325
268
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
269
|
+
##
|
|
270
|
+
# Retrieves the delay value from Google::Cloud::AbortedError or
|
|
271
|
+
# GRPC::Aborted
|
|
272
|
+
def delay_from_aborted err
|
|
273
|
+
return nil if err.nil?
|
|
274
|
+
if err.respond_to?(:metadata) && err.metadata["google.rpc.retryinfo-bin"]
|
|
275
|
+
retry_info = Google::Rpc::RetryInfo.decode err.metadata["google.rpc.retryinfo-bin"]
|
|
276
|
+
seconds = retry_info["retry_delay"].seconds
|
|
277
|
+
nanos = retry_info["retry_delay"].nanos
|
|
278
|
+
return seconds if nanos.zero?
|
|
279
|
+
return seconds + (nanos / 1_000_000_000.0)
|
|
280
|
+
end
|
|
281
|
+
# No metadata? Try the inner error
|
|
282
|
+
delay_from_aborted err.cause
|
|
283
|
+
rescue StandardError
|
|
284
|
+
# Any error indicates the backoff should be handled elsewhere
|
|
285
|
+
nil
|
|
330
286
|
end
|
|
331
287
|
end
|
|
332
288
|
end
|
|
333
289
|
end
|
|
334
290
|
end
|
|
335
|
-
|