activerecord-spanner-adapter 0.3.0 → 1.0.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 +49 -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 +55 -0
- data/CODE_OF_CONDUCT.md +40 -0
- data/CONTRIBUTING.md +79 -0
- data/Gemfile +9 -4
- data/LICENSE +6 -6
- data/README.md +66 -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 +171 -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 +130 -0
- data/acceptance/cases/transactions/read_write_transactions_test.rb +248 -0
- data/acceptance/cases/type/all_types_test.rb +172 -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/json_test.rb +34 -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 +147 -0
- data/acceptance/test_helper.rb +261 -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/hints/README.md +19 -0
- data/examples/snippets/hints/Rakefile +13 -0
- data/examples/snippets/hints/application.rb +47 -0
- data/examples/snippets/hints/config/database.yml +8 -0
- data/examples/snippets/hints/db/migrate/01_create_tables.rb +23 -0
- data/examples/snippets/hints/db/schema.rb +28 -0
- data/examples/snippets/hints/db/seeds.rb +29 -0
- data/examples/snippets/hints/models/album.rb +9 -0
- data/examples/snippets/hints/models/singer.rb +9 -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/partitioned-dml/README.md +16 -0
- data/examples/snippets/partitioned-dml/Rakefile +13 -0
- data/examples/snippets/partitioned-dml/application.rb +48 -0
- data/examples/snippets/partitioned-dml/config/database.yml +8 -0
- data/examples/snippets/partitioned-dml/db/migrate/01_create_tables.rb +21 -0
- data/examples/snippets/partitioned-dml/db/schema.rb +26 -0
- data/examples/snippets/partitioned-dml/db/seeds.rb +29 -0
- data/examples/snippets/partitioned-dml/models/album.rb +9 -0
- data/examples/snippets/partitioned-dml/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 +77 -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/stale-reads/README.md +27 -0
- data/examples/snippets/stale-reads/Rakefile +13 -0
- data/examples/snippets/stale-reads/application.rb +63 -0
- data/examples/snippets/stale-reads/config/database.yml +8 -0
- data/examples/snippets/stale-reads/db/migrate/01_create_tables.rb +21 -0
- data/examples/snippets/stale-reads/db/schema.rb +26 -0
- data/examples/snippets/stale-reads/db/seeds.rb +24 -0
- data/examples/snippets/stale-reads/models/album.rb +9 -0
- data/examples/snippets/stale-reads/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 +244 -266
- 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 +185 -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 +33 -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 +238 -0
- data/lib/activerecord_spanner_adapter/connection.rb +350 -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 +262 -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 +154 -0
- data/lib/activerecord_spanner_adapter/version.rb +9 -0
- data/lib/arel/visitors/spanner.rb +111 -0
- data/lib/spanner_client_ext.rb +107 -0
- data/renovate.json +5 -0
- metadata +405 -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
|
@@ -1,335 +1,313 @@
|
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
raise NotImplementedError
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Converts ASTs of INSERT, UPDATE or DELETE statements into forms
|
|
40
|
-
# convenient for DatabaseStatements#insert, #update and #delete.
|
|
41
|
-
class MutationVisitor < ::Arel::Visitors::Visitor
|
|
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)
|
|
70
|
-
|
|
71
|
-
unless o.orders.empty? and o.limit.nil? and o.wheres.size == 1
|
|
72
|
-
raise NotImplementedError, 'UPDATE statements with ORDER, LIMIT or complex WHERE clause'
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
ids = WhereVisitor.new(o.relation, pk, binds).accept(o.wheres[0])
|
|
76
|
-
raise NotImplementedError 'UPDATE statements with complex WHERE clause' unless ids
|
|
77
|
-
|
|
78
|
-
[table, ids, values]
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def visit_Arel_Nodes_DeleteStatement(o)
|
|
82
|
-
table = o.relation.name
|
|
83
|
-
|
|
84
|
-
# fallback_result lets the caller query the target id set at first and then
|
|
85
|
-
# delete the ids.
|
|
86
|
-
fallback_result = [table, nil, o.wheres]
|
|
87
|
-
|
|
88
|
-
case o.wheres.size
|
|
89
|
-
when 0
|
|
90
|
-
return [table, :all]
|
|
91
|
-
when 1
|
|
92
|
-
# it might be a simple "id = ?". Let's check later
|
|
93
|
-
else
|
|
94
|
-
return fallback_result
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
pk = @schema_reader.primary_key(table)
|
|
98
|
-
ids = WhereVisitor.new(o.relation, pk, binds).accept(o.wheres[0])
|
|
99
|
-
|
|
100
|
-
if ids
|
|
101
|
-
return [table, ids]
|
|
102
|
-
else
|
|
103
|
-
return fallback_result
|
|
24
|
+
if statement_type == :ddl
|
|
25
|
+
execute_ddl sql
|
|
26
|
+
else
|
|
27
|
+
transaction_required = statement_type == :dml
|
|
28
|
+
materialize_transactions
|
|
29
|
+
|
|
30
|
+
# First process and remove any hints in the binds that indicate that
|
|
31
|
+
# a different read staleness should be used than the default.
|
|
32
|
+
staleness_hint = binds.find { |b| b.is_a? Arel::Visitors::StalenessHint }
|
|
33
|
+
if staleness_hint
|
|
34
|
+
selector = Google::Cloud::Spanner::Session.single_use_transaction staleness_hint.value
|
|
35
|
+
binds.delete staleness_hint
|
|
104
36
|
end
|
|
105
|
-
end
|
|
106
37
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
38
|
+
log sql, name do
|
|
39
|
+
types, params = to_types_and_params binds
|
|
40
|
+
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
|
|
41
|
+
if transaction_required
|
|
42
|
+
transaction do
|
|
43
|
+
@connection.execute_query sql, params: params, types: types
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
@connection.execute_query sql, params: params, types: types, single_use_selector: selector
|
|
47
|
+
end
|
|
114
48
|
end
|
|
115
49
|
end
|
|
116
50
|
end
|
|
51
|
+
end
|
|
117
52
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def visit_Arel_Nodes_UnqualifiedColumn(o)
|
|
123
|
-
o.name
|
|
124
|
-
end
|
|
53
|
+
def query sql, name = nil
|
|
54
|
+
exec_query sql, name
|
|
125
55
|
end
|
|
126
56
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
57
|
+
def exec_query sql, name = "SQL", binds = [], prepare: false # rubocop:disable Lint/UnusedMethodArgument
|
|
58
|
+
result = execute sql, name, binds
|
|
59
|
+
ActiveRecord::Result.new(
|
|
60
|
+
result.fields.keys.map(&:to_s), result.rows.map(&:values)
|
|
61
|
+
)
|
|
62
|
+
end
|
|
131
63
|
|
|
132
|
-
|
|
64
|
+
def exec_mutation mutation
|
|
65
|
+
@connection.current_transaction.buffer mutation
|
|
66
|
+
end
|
|
133
67
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
@binds = binds
|
|
68
|
+
def update arel, name = nil, binds = []
|
|
69
|
+
# Add a `WHERE TRUE` if it is an update_all or delete_all call that uses DML.
|
|
70
|
+
if !should_use_mutation(arel) && arel.respond_to?(:ast) && arel.ast.wheres.empty?
|
|
71
|
+
arel.ast.wheres << Arel::Nodes::SqlLiteral.new("TRUE")
|
|
139
72
|
end
|
|
73
|
+
return super unless should_use_mutation arel
|
|
140
74
|
|
|
141
|
-
|
|
142
|
-
private :binds
|
|
75
|
+
raise "Unsupported update for use with mutations: #{arel}" unless arel.is_a? Arel::DeleteManager
|
|
143
76
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
77
|
+
exec_mutation create_delete_all_mutation arel if arel.is_a? Arel::DeleteManager
|
|
78
|
+
0 # Affected rows (unknown)
|
|
79
|
+
end
|
|
80
|
+
alias delete update
|
|
81
|
+
|
|
82
|
+
def exec_update sql, name = "SQL", binds = []
|
|
83
|
+
result = execute sql, name, binds
|
|
84
|
+
# Make sure that we consume the entire result stream before trying to get the stats.
|
|
85
|
+
# This is required because the ExecuteStreamingSql RPC is also used for (Partitioned) DML,
|
|
86
|
+
# and this RPC can return multiple partial result sets for DML as well. Only the last partial
|
|
87
|
+
# result set will contain the statistics. Although there will never be any rows, this makes
|
|
88
|
+
# sure that the stream is fully consumed.
|
|
89
|
+
result.rows.each { |_| }
|
|
90
|
+
return result.row_count if result.row_count
|
|
91
|
+
|
|
92
|
+
raise ActiveRecord::StatementInvalid.new(
|
|
93
|
+
"DML statement is invalid.", sql: sql
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
alias exec_delete exec_update
|
|
151
97
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
NOT_SIMPLE
|
|
98
|
+
def truncate table_name, name = nil
|
|
99
|
+
Array(table_name).each do |t|
|
|
100
|
+
log "TRUNCATE #{t}", name do
|
|
101
|
+
@connection.truncate t
|
|
157
102
|
end
|
|
158
103
|
end
|
|
104
|
+
end
|
|
159
105
|
|
|
160
|
-
|
|
161
|
-
|
|
106
|
+
def write_query? sql
|
|
107
|
+
sql_statement_type(sql) == :dml
|
|
108
|
+
end
|
|
162
109
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
110
|
+
def execute_ddl statements
|
|
111
|
+
log "MIGRATION", "SCHEMA" do
|
|
112
|
+
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
|
|
113
|
+
@connection.execute_ddl statements
|
|
167
114
|
end
|
|
168
115
|
end
|
|
116
|
+
rescue Google::Cloud::Error => error
|
|
117
|
+
raise ActiveRecord::StatementInvalid, error
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Transaction
|
|
169
121
|
|
|
170
|
-
|
|
171
|
-
|
|
122
|
+
def transaction requires_new: nil, isolation: nil, joinable: true
|
|
123
|
+
if !requires_new && current_transaction.joinable?
|
|
124
|
+
return super
|
|
172
125
|
end
|
|
173
126
|
|
|
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
|
|
127
|
+
backoff = 0.2
|
|
128
|
+
begin
|
|
129
|
+
super
|
|
130
|
+
rescue ActiveRecord::StatementInvalid => err
|
|
131
|
+
if err.cause.is_a? Google::Cloud::AbortedError
|
|
132
|
+
sleep(delay_from_aborted(err) || backoff *= 1.3)
|
|
133
|
+
retry
|
|
134
|
+
end
|
|
135
|
+
raise
|
|
192
136
|
end
|
|
193
137
|
end
|
|
194
138
|
|
|
195
|
-
def
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
out
|
|
139
|
+
def transaction_isolation_levels
|
|
140
|
+
{
|
|
141
|
+
read_uncommitted: "READ UNCOMMITTED",
|
|
142
|
+
read_committed: "READ COMMITTED",
|
|
143
|
+
repeatable_read: "REPEATABLE READ",
|
|
144
|
+
serializable: "SERIALIZABLE",
|
|
145
|
+
|
|
146
|
+
# These are not really isolation levels, but it is the only (best) way to pass in additional
|
|
147
|
+
# transaction options to the connection.
|
|
148
|
+
read_only: "READ_ONLY",
|
|
149
|
+
buffered_mutations: "BUFFERED_MUTATIONS"
|
|
207
150
|
}
|
|
151
|
+
end
|
|
208
152
|
|
|
209
|
-
|
|
210
|
-
|
|
153
|
+
def begin_db_transaction
|
|
154
|
+
log "BEGIN" do
|
|
155
|
+
@connection.begin_transaction
|
|
211
156
|
end
|
|
212
|
-
|
|
213
|
-
id_value
|
|
214
157
|
end
|
|
215
158
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
159
|
+
# Begins a transaction on the database with the specified isolation level. Cloud Spanner only supports
|
|
160
|
+
# isolation level :serializable, but also defines three additional 'isolation levels' that can be used
|
|
161
|
+
# to start specific types of Spanner transactions:
|
|
162
|
+
# * :read_only: Starts a read-only snapshot transaction using a strong timestamp bound.
|
|
163
|
+
# * :buffered_mutations: Starts a read/write transaction that will use mutations instead of DML for single-row
|
|
164
|
+
# inserts/updates/deletes. Mutations are buffered locally until the transaction is
|
|
165
|
+
# committed, and any changes during a transaction cannot be read by the application.
|
|
166
|
+
# * :pdml: Starts a Partitioned DML transaction. Executing multiple DML statements in one PDML transaction
|
|
167
|
+
# block is NOT supported A PDML transaction is not guaranteed to be atomic.
|
|
168
|
+
# See https://cloud.google.com/spanner/docs/dml-partitioned for more information.
|
|
169
|
+
#
|
|
170
|
+
# In addition to the above, a Hash containing read-only snapshot options may be used to start a specific
|
|
171
|
+
# read-only snapshot:
|
|
172
|
+
# * { timestamp: Time } Starts a read-only snapshot at the given timestamp.
|
|
173
|
+
# * { staleness: Integer } Starts a read-only snapshot with the given staleness in seconds.
|
|
174
|
+
# * { strong: <any value>} Starts a read-only snapshot with strong timestamp bound
|
|
175
|
+
# (this is the same as :read_only)
|
|
176
|
+
#
|
|
177
|
+
def begin_isolated_db_transaction isolation
|
|
178
|
+
if isolation.is_a? Hash
|
|
179
|
+
raise "Unsupported isolation level: #{isolation}" unless \
|
|
180
|
+
isolation[:timestamp] || isolation[:staleness] || isolation[:strong]
|
|
181
|
+
raise "Only one option is supported. It must be one of `timestamp`, `staleness` or `strong`." \
|
|
182
|
+
if isolation.count != 1
|
|
229
183
|
else
|
|
230
|
-
|
|
184
|
+
raise "Unsupported isolation level: #{isolation}" unless \
|
|
185
|
+
[:serializable, :read_only, :buffered_mutations, :pdml].include? isolation
|
|
231
186
|
end
|
|
232
187
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
log(fake_sql, name, binds) do
|
|
237
|
-
with_phase_transition {|client| client.update(table, rows) }
|
|
188
|
+
log "BEGIN #{isolation}" do
|
|
189
|
+
@connection.begin_transaction isolation
|
|
238
190
|
end
|
|
239
|
-
target.size
|
|
240
191
|
end
|
|
241
192
|
|
|
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
|
|
193
|
+
def commit_db_transaction
|
|
194
|
+
log "COMMIT" do
|
|
195
|
+
@connection.commit_transaction
|
|
256
196
|
end
|
|
197
|
+
end
|
|
257
198
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
199
|
+
def rollback_db_transaction
|
|
200
|
+
log "ROLLBACK" do
|
|
201
|
+
@connection.rollback_transaction
|
|
267
202
|
end
|
|
203
|
+
end
|
|
268
204
|
|
|
269
|
-
|
|
270
|
-
with_phase_transition {|client| client.delete(table, keyset) }
|
|
271
|
-
end
|
|
205
|
+
private
|
|
272
206
|
|
|
273
|
-
|
|
207
|
+
# Translates binds to Spanner types and params.
|
|
208
|
+
def to_types_and_params binds
|
|
209
|
+
types = binds.enum_for(:each_with_index).map do |bind, i|
|
|
210
|
+
type = :INT64
|
|
211
|
+
if bind.respond_to? :type
|
|
212
|
+
type = ActiveRecord::Type::Spanner::SpannerActiveRecordConverter
|
|
213
|
+
.convert_active_model_type_to_spanner(bind.type)
|
|
214
|
+
end
|
|
215
|
+
[
|
|
216
|
+
# Generates binds for named parameters in the format `@p1, @p2, ...`
|
|
217
|
+
"p#{i + 1}", type
|
|
218
|
+
]
|
|
219
|
+
end.to_h
|
|
220
|
+
params = binds.enum_for(:each_with_index).map do |bind, i|
|
|
221
|
+
type = bind.respond_to?(:type) ? bind.type : ActiveModel::Type::Integer
|
|
222
|
+
value = bind
|
|
223
|
+
value = type.serialize bind.value, :dml if type.respond_to?(:serialize) && type.method(:serialize).arity < 0
|
|
224
|
+
value = type.serialize bind.value if type.respond_to?(:serialize) && type.method(:serialize).arity >= 0
|
|
225
|
+
|
|
226
|
+
["p#{i + 1}", value]
|
|
227
|
+
end.to_h
|
|
228
|
+
[types, params]
|
|
274
229
|
end
|
|
275
230
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
231
|
+
# An insert/update/delete statement could use mutations in some specific circumstances.
|
|
232
|
+
# This method returns an indication whether a specific operation should use mutations instead of DML
|
|
233
|
+
# based on the operation itself, and the current transaction.
|
|
234
|
+
def should_use_mutation arel
|
|
235
|
+
!@connection.current_transaction.nil? \
|
|
236
|
+
&& @connection.current_transaction.isolation == :buffered_mutations \
|
|
237
|
+
&& can_use_mutation(arel) \
|
|
283
238
|
end
|
|
284
239
|
|
|
285
|
-
def
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
}
|
|
298
|
-
end
|
|
240
|
+
def can_use_mutation arel
|
|
241
|
+
return true if arel.is_a?(Arel::DeleteManager) && arel.respond_to?(:ast) && arel.ast.wheres.empty?
|
|
242
|
+
false
|
|
243
|
+
end
|
|
299
244
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
245
|
+
def create_delete_all_mutation arel
|
|
246
|
+
unless arel.is_a? Arel::DeleteManager
|
|
247
|
+
raise "A delete mutation can only be created from a DeleteManager"
|
|
248
|
+
end
|
|
249
|
+
# Check if it is a delete_all operation.
|
|
250
|
+
unless arel.ast.wheres.empty?
|
|
251
|
+
raise "A delete mutation can only be created without a WHERE clause"
|
|
252
|
+
end
|
|
253
|
+
table_name = arel.ast.relation.name if arel.ast.relation.is_a? Arel::Table
|
|
254
|
+
table_name = arel.ast.relation.left.name if arel.ast.relation.is_a? Arel::Nodes::JoinSource
|
|
255
|
+
unless table_name
|
|
256
|
+
raise "Could not find table for delete mutation"
|
|
307
257
|
end
|
|
308
|
-
end
|
|
309
258
|
|
|
310
|
-
|
|
311
|
-
|
|
259
|
+
Google::Cloud::Spanner::V1::Mutation.new(
|
|
260
|
+
delete: Google::Cloud::Spanner::V1::Mutation::Delete.new(
|
|
261
|
+
table: table_name,
|
|
262
|
+
key_set: { all: true }
|
|
263
|
+
)
|
|
264
|
+
)
|
|
312
265
|
end
|
|
313
266
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
267
|
+
COMMENT_REGEX = %r{(?:--.*\n)*|/\*(?:[^*]|\*[^/])*\*/}m.freeze \
|
|
268
|
+
unless defined? ActiveRecord::ConnectionAdapters::AbstractAdapter::COMMENT_REGEX
|
|
269
|
+
COMMENT_REGEX = ActiveRecord::ConnectionAdapters::AbstractAdapter::COMMENT_REGEX \
|
|
270
|
+
if defined? ActiveRecord::ConnectionAdapters::AbstractAdapter::COMMENT_REGEX
|
|
317
271
|
|
|
318
|
-
def
|
|
319
|
-
|
|
272
|
+
private_class_method def self.build_sql_statement_regexp *parts # :nodoc:
|
|
273
|
+
parts = parts.map { |part| /#{part}/i }
|
|
274
|
+
/\A(?:[\(\s]|#{COMMENT_REGEX})*#{Regexp.union(*parts)}/
|
|
320
275
|
end
|
|
321
276
|
|
|
322
|
-
|
|
323
|
-
|
|
277
|
+
DDL_REGX = build_sql_statement_regexp(:create, :alter, :drop).freeze
|
|
278
|
+
|
|
279
|
+
DML_REGX = build_sql_statement_regexp(:insert, :delete, :update).freeze
|
|
280
|
+
|
|
281
|
+
def sql_statement_type sql
|
|
282
|
+
case sql
|
|
283
|
+
when DDL_REGX
|
|
284
|
+
:ddl
|
|
285
|
+
when DML_REGX
|
|
286
|
+
:dml
|
|
287
|
+
else
|
|
288
|
+
:dql
|
|
289
|
+
end
|
|
324
290
|
end
|
|
325
291
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
292
|
+
##
|
|
293
|
+
# Retrieves the delay value from Google::Cloud::AbortedError or
|
|
294
|
+
# GRPC::Aborted
|
|
295
|
+
def delay_from_aborted err
|
|
296
|
+
return nil if err.nil?
|
|
297
|
+
if err.respond_to?(:metadata) && err.metadata["google.rpc.retryinfo-bin"]
|
|
298
|
+
retry_info = Google::Rpc::RetryInfo.decode err.metadata["google.rpc.retryinfo-bin"]
|
|
299
|
+
seconds = retry_info["retry_delay"].seconds
|
|
300
|
+
nanos = retry_info["retry_delay"].nanos
|
|
301
|
+
return seconds if nanos.zero?
|
|
302
|
+
return seconds + (nanos / 1_000_000_000.0)
|
|
303
|
+
end
|
|
304
|
+
# No metadata? Try the inner error
|
|
305
|
+
delay_from_aborted err.cause
|
|
306
|
+
rescue StandardError
|
|
307
|
+
# Any error indicates the backoff should be handled elsewhere
|
|
308
|
+
nil
|
|
330
309
|
end
|
|
331
310
|
end
|
|
332
311
|
end
|
|
333
312
|
end
|
|
334
313
|
end
|
|
335
|
-
|