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,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,8 @@
1
+ development:
2
+ adapter: spanner
3
+ emulator_host: localhost:9010
4
+ project: test-project
5
+ instance: test-instance
6
+ database: testdb
7
+ pool: 5
8
+ timeout: 5000
@@ -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,6 @@
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
+ #
@@ -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
+ ![](/assets/solidus-db.png)
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
- include ConnectionAdapters::DatabaseStatements
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
- # A mixin which provides ways to resolve rvalues.
14
- module RightValueResolveable
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
- def visit_Array(o)
20
- o.map {|item| accept(item) }
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
- def visit_Arel_Nodes_Casted(o)
24
- a = o.attribute
25
- if a.able_to_type_cast?
26
- a.type_cast_for_database(o.val)
27
- else
28
- o.val
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
- # 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)
45
+ def query sql, name = nil
46
+ exec_query sql, name
47
+ end
70
48
 
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
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
- ids = WhereVisitor.new(o.relation, pk, binds).accept(o.wheres[0])
76
- raise NotImplementedError 'UPDATE statements with complex WHERE clause' unless ids
56
+ def exec_mutation mutation
57
+ @connection.current_transaction.buffer mutation
58
+ end
77
59
 
78
- [table, ids, values]
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
- def visit_Arel_Nodes_DeleteStatement(o)
82
- table = o.relation.name
67
+ raise "Unsupported update for use with mutations: #{arel}" unless arel.is_a? Arel::DeleteManager
83
68
 
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
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
- def visit_Arel_Nodes_Values o
108
- o.expressions.map.with_index do |value|
109
- case value
110
- when ::Arel::Nodes::SqlLiteral
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
- # Tries to convert where clause into a set of ids if it is simple enough.
128
- # Returns nil if the clause is not simple.
129
- class WhereVisitor < ::Arel::Visitors::Visitor
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
- def visit_Arel_Nodes_Equality(o)
153
- if pk_cond?(o)
154
- [accept(o.right)]
155
- else
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
- def visit_Arel_Nodes_In(o)
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
- def unsupported(o)
171
- return NOT_SIMPLE
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
- alias visit_Arel_Nodes_Grouping unsupported
175
- alias visit_Arel_Nodes_NotIn unsupported
176
- alias visit_Arel_Nodes_Or unsupported
177
- alias visit_Arel_Nodes_NotEqual unsupported
178
- alias visit_Arel_Nodes_Case unsupported
179
- alias visit_Arel_Nodes_Between unsupported
180
- alias visit_Arel_Nodes_GreaterThanOrEqual unsupported
181
- alias visit_Arel_Nodes_GreaterThan unsupported
182
- alias visit_Arel_Nodes_LessThanOrEqual unsupported
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 insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
196
- raise NotImplementedError, "INSERT in raw SQL is not supported" unless arel.respond_to?(:ast)
197
-
198
- type_casted_binds = binds.map {|attr| type_cast(attr.value_for_database) }
199
- table, columns, values = MutationVisitor.new(self, type_casted_binds).accept(arel.ast)
200
- fake_sql = <<~"SQL"
201
- INSERT INTO #{table}(#{columns.join(", ")}) VALUES (#{values.join(", ")})
202
- SQL
203
-
204
- row = columns.zip(values).inject({}) {|out, (col, value)|
205
- out[col] = value
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
- log(fake_sql, name) do
210
- with_phase_transition {|client| client.insert(table, row) }
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
- def update(arel, name = nil, binds = [])
217
- raise NotImplementedError, "UPDATE statement in raw SQL is not supported" unless arel.respond_to?(:ast)
218
-
219
- type_casted_binds = binds.map {|attr| type_cast(attr.value_for_database) }
220
- table, target, values = MutationVisitor.new(self, type_casted_binds.dup).accept(arel.ast)
221
-
222
- fake_set = values.map {|col, val|
223
- "#{quote_column_name(col)} = #{quote(val)}"
224
- }.join(', ')
225
-
226
- pk = primary_key(table)
227
- if target.size > 1
228
- fake_sql = "UPDATE #{quote_column_name(table)} SET #{fake_set} WHERE #{pk} IN ?"
229
- else
230
- fake_sql = "UPDATE #{quote_column_name(table)} SET #{fake_set} WHERE #{pk} = ?"
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
- row = values.inject({}) {|r, (col, val)| r[col] = val; r }
234
- rows = target.map {|id| row.merge(pk => id) }
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 delete(arel, name, binds)
243
- raise NotImplementedError, "DELETE statement in raw SQL is not supported" unless arel.respond_to?(:ast)
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
- if target == :all
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
- log(fake_sql, name, binds) do
270
- with_phase_transition {|client| client.delete(table, keyset) }
271
- end
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
- keyset.size
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 execute(stmt)
277
- case stmt
278
- when Spanner::DDL
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 exec_query(sql, name = 'SQL', binds = [], prepare: :ignored)
286
- case
287
- when binds.kind_of?(Hash)
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
- log(sql, name, binds) do
301
- executor = client
302
- executor = raw_client if name == 'SCHEMA'
303
- results = executor.execute(sql, params: spanner_binds)
304
- columns = results.fields.keys
305
- rows = results.rows.map(&:to_a)
306
- ActiveRecord::Result.new(columns.map(&:to_s), rows)
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
- def begin_db_transaction
311
- with_phase_transition {|client| client.begin_transaction }
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
- def begin_isolated_db_transaction(isolation)
315
- with_phase_transition {|client| client.begin_snapshot(**isolation) }
316
- end
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 commit_db_transaction
319
- with_phase_transition {|client| client.commit }
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
- def exec_rollback_db_transaction
323
- with_phase_transition {|client| client.rollback }
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
- def with_phase_transition
327
- result = yield client
328
- @client = client.next
329
- result
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
-