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.
Files changed (264) hide show
  1. checksums.yaml +5 -5
  2. data/.github/CODEOWNERS +7 -0
  3. data/.github/sync-repo-settings.yaml +16 -0
  4. data/.github/workflows/acceptance-tests-on-emulator.yaml +45 -0
  5. data/.github/workflows/acceptance-tests-on-production.yaml +36 -0
  6. data/.github/workflows/ci.yaml +33 -0
  7. data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +52 -0
  8. data/.github/workflows/nightly-acceptance-tests-on-production.yaml +35 -0
  9. data/.github/workflows/nightly-unit-tests.yaml +40 -0
  10. data/.github/workflows/release-please-label.yml +25 -0
  11. data/.github/workflows/release-please.yml +39 -0
  12. data/.github/workflows/rubocop.yaml +31 -0
  13. data/.gitignore +67 -5
  14. data/.kokoro/populate-secrets.sh +77 -0
  15. data/.kokoro/release.cfg +33 -0
  16. data/.kokoro/release.sh +15 -0
  17. data/.kokoro/trampoline_v2.sh +489 -0
  18. data/.rubocop.yml +46 -0
  19. data/.toys/release.rb +18 -0
  20. data/.trampolinerc +48 -0
  21. data/.yardopts +11 -0
  22. data/CHANGELOG.md +26 -0
  23. data/CODE_OF_CONDUCT.md +40 -0
  24. data/CONTRIBUTING.md +79 -0
  25. data/Gemfile +9 -4
  26. data/LICENSE +6 -6
  27. data/README.md +67 -30
  28. data/Rakefile +79 -3
  29. data/SECURITY.md +7 -0
  30. data/acceptance/cases/associations/has_many_associations_test.rb +119 -0
  31. data/acceptance/cases/associations/has_many_through_associations_test.rb +63 -0
  32. data/acceptance/cases/associations/has_one_associations_test.rb +79 -0
  33. data/acceptance/cases/associations/has_one_through_associations_test.rb +98 -0
  34. data/acceptance/cases/interleaved_associations/has_many_associations_using_interleaved_test.rb +211 -0
  35. data/acceptance/cases/migration/change_schema_test.rb +433 -0
  36. data/acceptance/cases/migration/change_table_test.rb +115 -0
  37. data/acceptance/cases/migration/column_attributes_test.rb +122 -0
  38. data/acceptance/cases/migration/column_positioning_test.rb +48 -0
  39. data/acceptance/cases/migration/columns_test.rb +201 -0
  40. data/acceptance/cases/migration/command_recorder_test.rb +406 -0
  41. data/acceptance/cases/migration/create_join_table_test.rb +216 -0
  42. data/acceptance/cases/migration/ddl_batching_test.rb +80 -0
  43. data/acceptance/cases/migration/foreign_key_test.rb +297 -0
  44. data/acceptance/cases/migration/index_test.rb +211 -0
  45. data/acceptance/cases/migration/references_foreign_key_test.rb +259 -0
  46. data/acceptance/cases/migration/references_index_test.rb +135 -0
  47. data/acceptance/cases/migration/references_statements_test.rb +166 -0
  48. data/acceptance/cases/migration/rename_column_test.rb +96 -0
  49. data/acceptance/cases/models/calculation_query_test.rb +128 -0
  50. data/acceptance/cases/models/generated_column_test.rb +126 -0
  51. data/acceptance/cases/models/mutation_test.rb +122 -0
  52. data/acceptance/cases/models/query_test.rb +147 -0
  53. data/acceptance/cases/sessions/session_not_found_test.rb +121 -0
  54. data/acceptance/cases/transactions/optimistic_locking_test.rb +141 -0
  55. data/acceptance/cases/transactions/read_only_transactions_test.rb +67 -0
  56. data/acceptance/cases/transactions/read_write_transactions_test.rb +248 -0
  57. data/acceptance/cases/type/all_types_test.rb +152 -0
  58. data/acceptance/cases/type/binary_test.rb +59 -0
  59. data/acceptance/cases/type/boolean_test.rb +31 -0
  60. data/acceptance/cases/type/date_test.rb +32 -0
  61. data/acceptance/cases/type/date_time_test.rb +30 -0
  62. data/acceptance/cases/type/float_test.rb +27 -0
  63. data/acceptance/cases/type/integer_test.rb +44 -0
  64. data/acceptance/cases/type/numeric_test.rb +27 -0
  65. data/acceptance/cases/type/string_test.rb +79 -0
  66. data/acceptance/cases/type/text_test.rb +30 -0
  67. data/acceptance/cases/type/time_test.rb +87 -0
  68. data/acceptance/models/account.rb +13 -0
  69. data/acceptance/models/address.rb +9 -0
  70. data/acceptance/models/album.rb +12 -0
  71. data/acceptance/models/all_types.rb +8 -0
  72. data/acceptance/models/author.rb +11 -0
  73. data/acceptance/models/club.rb +12 -0
  74. data/acceptance/models/comment.rb +9 -0
  75. data/acceptance/models/customer.rb +9 -0
  76. data/acceptance/models/department.rb +9 -0
  77. data/acceptance/models/firm.rb +10 -0
  78. data/acceptance/models/member.rb +13 -0
  79. data/acceptance/models/member_type.rb +9 -0
  80. data/acceptance/models/membership.rb +10 -0
  81. data/acceptance/models/organization.rb +9 -0
  82. data/acceptance/models/post.rb +10 -0
  83. data/acceptance/models/singer.rb +10 -0
  84. data/acceptance/models/track.rb +20 -0
  85. data/acceptance/models/transaction.rb +9 -0
  86. data/acceptance/schema/schema.rb +143 -0
  87. data/acceptance/test_helper.rb +260 -0
  88. data/activerecord-spanner-adapter.gemspec +32 -17
  89. data/assets/solidus-db.png +0 -0
  90. data/benchmarks/README.md +17 -0
  91. data/benchmarks/Rakefile +14 -0
  92. data/benchmarks/application.rb +308 -0
  93. data/benchmarks/config/database.yml +8 -0
  94. data/benchmarks/config/environment.rb +12 -0
  95. data/benchmarks/db/migrate/01_create_tables.rb +25 -0
  96. data/benchmarks/db/schema.rb +29 -0
  97. data/benchmarks/models/album.rb +9 -0
  98. data/benchmarks/models/singer.rb +9 -0
  99. data/bin/console +6 -7
  100. data/examples/rails/README.md +262 -0
  101. data/examples/snippets/README.md +29 -0
  102. data/examples/snippets/Rakefile +57 -0
  103. data/examples/snippets/array-data-type/README.md +45 -0
  104. data/examples/snippets/array-data-type/Rakefile +13 -0
  105. data/examples/snippets/array-data-type/application.rb +45 -0
  106. data/examples/snippets/array-data-type/config/database.yml +8 -0
  107. data/examples/snippets/array-data-type/db/migrate/01_create_tables.rb +24 -0
  108. data/examples/snippets/array-data-type/db/schema.rb +26 -0
  109. data/examples/snippets/array-data-type/db/seeds.rb +5 -0
  110. data/examples/snippets/array-data-type/models/entity_with_array_types.rb +18 -0
  111. data/examples/snippets/bin/create_emulator_instance.rb +18 -0
  112. data/examples/snippets/bulk-insert/README.md +21 -0
  113. data/examples/snippets/bulk-insert/Rakefile +13 -0
  114. data/examples/snippets/bulk-insert/application.rb +64 -0
  115. data/examples/snippets/bulk-insert/config/database.yml +8 -0
  116. data/examples/snippets/bulk-insert/db/migrate/01_create_tables.rb +21 -0
  117. data/examples/snippets/bulk-insert/db/schema.rb +26 -0
  118. data/examples/snippets/bulk-insert/db/seeds.rb +5 -0
  119. data/examples/snippets/bulk-insert/models/album.rb +9 -0
  120. data/examples/snippets/bulk-insert/models/singer.rb +9 -0
  121. data/examples/snippets/commit-timestamp/README.md +18 -0
  122. data/examples/snippets/commit-timestamp/Rakefile +13 -0
  123. data/examples/snippets/commit-timestamp/application.rb +53 -0
  124. data/examples/snippets/commit-timestamp/config/database.yml +8 -0
  125. data/examples/snippets/commit-timestamp/db/migrate/01_create_tables.rb +26 -0
  126. data/examples/snippets/commit-timestamp/db/schema.rb +29 -0
  127. data/examples/snippets/commit-timestamp/db/seeds.rb +5 -0
  128. data/examples/snippets/commit-timestamp/models/album.rb +9 -0
  129. data/examples/snippets/commit-timestamp/models/singer.rb +9 -0
  130. data/examples/snippets/config/environment.rb +21 -0
  131. data/examples/snippets/create-records/README.md +12 -0
  132. data/examples/snippets/create-records/Rakefile +13 -0
  133. data/examples/snippets/create-records/application.rb +42 -0
  134. data/examples/snippets/create-records/config/database.yml +8 -0
  135. data/examples/snippets/create-records/db/migrate/01_create_tables.rb +21 -0
  136. data/examples/snippets/create-records/db/schema.rb +26 -0
  137. data/examples/snippets/create-records/db/seeds.rb +5 -0
  138. data/examples/snippets/create-records/models/album.rb +9 -0
  139. data/examples/snippets/create-records/models/singer.rb +9 -0
  140. data/examples/snippets/date-data-type/README.md +19 -0
  141. data/examples/snippets/date-data-type/Rakefile +13 -0
  142. data/examples/snippets/date-data-type/application.rb +35 -0
  143. data/examples/snippets/date-data-type/config/database.yml +8 -0
  144. data/examples/snippets/date-data-type/db/migrate/01_create_tables.rb +20 -0
  145. data/examples/snippets/date-data-type/db/schema.rb +21 -0
  146. data/examples/snippets/date-data-type/db/seeds.rb +16 -0
  147. data/examples/snippets/date-data-type/models/singer.rb +8 -0
  148. data/examples/snippets/generated-column/README.md +41 -0
  149. data/examples/snippets/generated-column/Rakefile +13 -0
  150. data/examples/snippets/generated-column/application.rb +37 -0
  151. data/examples/snippets/generated-column/config/database.yml +8 -0
  152. data/examples/snippets/generated-column/db/migrate/01_create_tables.rb +23 -0
  153. data/examples/snippets/generated-column/db/schema.rb +21 -0
  154. data/examples/snippets/generated-column/db/seeds.rb +18 -0
  155. data/examples/snippets/generated-column/models/singer.rb +8 -0
  156. data/examples/snippets/interleaved-tables/README.md +152 -0
  157. data/examples/snippets/interleaved-tables/Rakefile +13 -0
  158. data/examples/snippets/interleaved-tables/application.rb +109 -0
  159. data/examples/snippets/interleaved-tables/config/database.yml +8 -0
  160. data/examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb +44 -0
  161. data/examples/snippets/interleaved-tables/db/schema.rb +32 -0
  162. data/examples/snippets/interleaved-tables/db/seeds.rb +40 -0
  163. data/examples/snippets/interleaved-tables/models/album.rb +15 -0
  164. data/examples/snippets/interleaved-tables/models/singer.rb +20 -0
  165. data/examples/snippets/interleaved-tables/models/track.rb +25 -0
  166. data/examples/snippets/migrations/README.md +43 -0
  167. data/examples/snippets/migrations/Rakefile +13 -0
  168. data/examples/snippets/migrations/application.rb +26 -0
  169. data/examples/snippets/migrations/config/database.yml +8 -0
  170. data/examples/snippets/migrations/db/migrate/01_create_tables.rb +28 -0
  171. data/examples/snippets/migrations/db/schema.rb +33 -0
  172. data/examples/snippets/migrations/db/seeds.rb +5 -0
  173. data/examples/snippets/migrations/models/album.rb +10 -0
  174. data/examples/snippets/migrations/models/singer.rb +10 -0
  175. data/examples/snippets/migrations/models/track.rb +9 -0
  176. data/examples/snippets/mutations/README.md +34 -0
  177. data/examples/snippets/mutations/Rakefile +13 -0
  178. data/examples/snippets/mutations/application.rb +47 -0
  179. data/examples/snippets/mutations/config/database.yml +8 -0
  180. data/examples/snippets/mutations/db/migrate/01_create_tables.rb +22 -0
  181. data/examples/snippets/mutations/db/schema.rb +27 -0
  182. data/examples/snippets/mutations/db/seeds.rb +25 -0
  183. data/examples/snippets/mutations/models/album.rb +9 -0
  184. data/examples/snippets/mutations/models/singer.rb +9 -0
  185. data/examples/snippets/optimistic-locking/README.md +12 -0
  186. data/examples/snippets/optimistic-locking/Rakefile +13 -0
  187. data/examples/snippets/optimistic-locking/application.rb +48 -0
  188. data/examples/snippets/optimistic-locking/config/database.yml +8 -0
  189. data/examples/snippets/optimistic-locking/db/migrate/01_create_tables.rb +26 -0
  190. data/examples/snippets/optimistic-locking/db/schema.rb +29 -0
  191. data/examples/snippets/optimistic-locking/db/seeds.rb +25 -0
  192. data/examples/snippets/optimistic-locking/models/album.rb +9 -0
  193. data/examples/snippets/optimistic-locking/models/singer.rb +9 -0
  194. data/examples/snippets/quickstart/README.md +26 -0
  195. data/examples/snippets/quickstart/Rakefile +13 -0
  196. data/examples/snippets/quickstart/application.rb +51 -0
  197. data/examples/snippets/quickstart/config/database.yml +8 -0
  198. data/examples/snippets/quickstart/db/migrate/01_create_tables.rb +21 -0
  199. data/examples/snippets/quickstart/db/schema.rb +26 -0
  200. data/examples/snippets/quickstart/db/seeds.rb +24 -0
  201. data/examples/snippets/quickstart/models/album.rb +9 -0
  202. data/examples/snippets/quickstart/models/singer.rb +9 -0
  203. data/examples/snippets/read-only-transactions/README.md +13 -0
  204. data/examples/snippets/read-only-transactions/Rakefile +13 -0
  205. data/examples/snippets/read-only-transactions/application.rb +49 -0
  206. data/examples/snippets/read-only-transactions/config/database.yml +8 -0
  207. data/examples/snippets/read-only-transactions/db/migrate/01_create_tables.rb +21 -0
  208. data/examples/snippets/read-only-transactions/db/schema.rb +26 -0
  209. data/examples/snippets/read-only-transactions/db/seeds.rb +24 -0
  210. data/examples/snippets/read-only-transactions/models/album.rb +9 -0
  211. data/examples/snippets/read-only-transactions/models/singer.rb +9 -0
  212. data/examples/snippets/read-write-transactions/README.md +12 -0
  213. data/examples/snippets/read-write-transactions/Rakefile +13 -0
  214. data/examples/snippets/read-write-transactions/application.rb +39 -0
  215. data/examples/snippets/read-write-transactions/config/database.yml +8 -0
  216. data/examples/snippets/read-write-transactions/db/migrate/01_create_tables.rb +22 -0
  217. data/examples/snippets/read-write-transactions/db/schema.rb +27 -0
  218. data/examples/snippets/read-write-transactions/db/seeds.rb +25 -0
  219. data/examples/snippets/read-write-transactions/models/album.rb +9 -0
  220. data/examples/snippets/read-write-transactions/models/singer.rb +9 -0
  221. data/examples/snippets/timestamp-data-type/README.md +17 -0
  222. data/examples/snippets/timestamp-data-type/Rakefile +13 -0
  223. data/examples/snippets/timestamp-data-type/application.rb +42 -0
  224. data/examples/snippets/timestamp-data-type/config/database.yml +8 -0
  225. data/examples/snippets/timestamp-data-type/db/migrate/01_create_tables.rb +21 -0
  226. data/examples/snippets/timestamp-data-type/db/schema.rb +21 -0
  227. data/examples/snippets/timestamp-data-type/db/seeds.rb +6 -0
  228. data/examples/snippets/timestamp-data-type/models/meeting.rb +19 -0
  229. data/examples/solidus/README.md +172 -0
  230. data/lib/active_record/connection_adapters/spanner/database_statements.rb +224 -269
  231. data/lib/active_record/connection_adapters/spanner/quoting.rb +42 -50
  232. data/lib/active_record/connection_adapters/spanner/schema_cache.rb +43 -0
  233. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +125 -9
  234. data/lib/active_record/connection_adapters/spanner/schema_definitions.rb +122 -0
  235. data/lib/active_record/connection_adapters/spanner/schema_dumper.rb +19 -0
  236. data/lib/active_record/connection_adapters/spanner/schema_statements.rb +553 -139
  237. data/lib/active_record/connection_adapters/spanner/type_metadata.rb +37 -0
  238. data/lib/active_record/connection_adapters/spanner_adapter.rb +182 -78
  239. data/lib/active_record/tasks/spanner_database_tasks.rb +74 -0
  240. data/lib/active_record/type/spanner/array.rb +32 -0
  241. data/lib/active_record/type/spanner/bytes.rb +26 -0
  242. data/lib/active_record/type/spanner/spanner_active_record_converter.rb +32 -0
  243. data/lib/active_record/type/spanner/time.rb +37 -0
  244. data/lib/activerecord-spanner-adapter.rb +23 -0
  245. data/lib/activerecord_spanner_adapter/base.rb +217 -0
  246. data/lib/activerecord_spanner_adapter/connection.rb +324 -0
  247. data/lib/activerecord_spanner_adapter/errors.rb +13 -0
  248. data/lib/activerecord_spanner_adapter/foreign_key.rb +29 -0
  249. data/lib/activerecord_spanner_adapter/index/column.rb +38 -0
  250. data/lib/activerecord_spanner_adapter/index.rb +80 -0
  251. data/lib/activerecord_spanner_adapter/information_schema.rb +261 -0
  252. data/lib/activerecord_spanner_adapter/primary_key.rb +31 -0
  253. data/lib/activerecord_spanner_adapter/table/column.rb +59 -0
  254. data/lib/activerecord_spanner_adapter/table.rb +61 -0
  255. data/lib/activerecord_spanner_adapter/transaction.rb +113 -0
  256. data/lib/activerecord_spanner_adapter/version.rb +9 -0
  257. data/lib/arel/visitors/spanner.rb +35 -0
  258. data/lib/spanner_client_ext.rb +82 -0
  259. data/renovate.json +5 -0
  260. metadata +387 -34
  261. data/.travis.yml +0 -5
  262. data/lib/active_record/connection_adapters/spanner/client.rb +0 -190
  263. data/lib/active_record/connection_adapters/spanner.rb +0 -10
  264. 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