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.
Files changed (282) 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 +49 -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 +55 -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 +66 -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 +171 -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 +130 -0
  56. data/acceptance/cases/transactions/read_write_transactions_test.rb +248 -0
  57. data/acceptance/cases/type/all_types_test.rb +172 -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/json_test.rb +34 -0
  65. data/acceptance/cases/type/numeric_test.rb +27 -0
  66. data/acceptance/cases/type/string_test.rb +79 -0
  67. data/acceptance/cases/type/text_test.rb +30 -0
  68. data/acceptance/cases/type/time_test.rb +87 -0
  69. data/acceptance/models/account.rb +13 -0
  70. data/acceptance/models/address.rb +9 -0
  71. data/acceptance/models/album.rb +12 -0
  72. data/acceptance/models/all_types.rb +8 -0
  73. data/acceptance/models/author.rb +11 -0
  74. data/acceptance/models/club.rb +12 -0
  75. data/acceptance/models/comment.rb +9 -0
  76. data/acceptance/models/customer.rb +9 -0
  77. data/acceptance/models/department.rb +9 -0
  78. data/acceptance/models/firm.rb +10 -0
  79. data/acceptance/models/member.rb +13 -0
  80. data/acceptance/models/member_type.rb +9 -0
  81. data/acceptance/models/membership.rb +10 -0
  82. data/acceptance/models/organization.rb +9 -0
  83. data/acceptance/models/post.rb +10 -0
  84. data/acceptance/models/singer.rb +10 -0
  85. data/acceptance/models/track.rb +20 -0
  86. data/acceptance/models/transaction.rb +9 -0
  87. data/acceptance/schema/schema.rb +147 -0
  88. data/acceptance/test_helper.rb +261 -0
  89. data/activerecord-spanner-adapter.gemspec +32 -17
  90. data/assets/solidus-db.png +0 -0
  91. data/benchmarks/README.md +17 -0
  92. data/benchmarks/Rakefile +14 -0
  93. data/benchmarks/application.rb +308 -0
  94. data/benchmarks/config/database.yml +8 -0
  95. data/benchmarks/config/environment.rb +12 -0
  96. data/benchmarks/db/migrate/01_create_tables.rb +25 -0
  97. data/benchmarks/db/schema.rb +29 -0
  98. data/benchmarks/models/album.rb +9 -0
  99. data/benchmarks/models/singer.rb +9 -0
  100. data/bin/console +6 -7
  101. data/examples/rails/README.md +262 -0
  102. data/examples/snippets/README.md +29 -0
  103. data/examples/snippets/Rakefile +57 -0
  104. data/examples/snippets/array-data-type/README.md +45 -0
  105. data/examples/snippets/array-data-type/Rakefile +13 -0
  106. data/examples/snippets/array-data-type/application.rb +45 -0
  107. data/examples/snippets/array-data-type/config/database.yml +8 -0
  108. data/examples/snippets/array-data-type/db/migrate/01_create_tables.rb +24 -0
  109. data/examples/snippets/array-data-type/db/schema.rb +26 -0
  110. data/examples/snippets/array-data-type/db/seeds.rb +5 -0
  111. data/examples/snippets/array-data-type/models/entity_with_array_types.rb +18 -0
  112. data/examples/snippets/bin/create_emulator_instance.rb +18 -0
  113. data/examples/snippets/bulk-insert/README.md +21 -0
  114. data/examples/snippets/bulk-insert/Rakefile +13 -0
  115. data/examples/snippets/bulk-insert/application.rb +64 -0
  116. data/examples/snippets/bulk-insert/config/database.yml +8 -0
  117. data/examples/snippets/bulk-insert/db/migrate/01_create_tables.rb +21 -0
  118. data/examples/snippets/bulk-insert/db/schema.rb +26 -0
  119. data/examples/snippets/bulk-insert/db/seeds.rb +5 -0
  120. data/examples/snippets/bulk-insert/models/album.rb +9 -0
  121. data/examples/snippets/bulk-insert/models/singer.rb +9 -0
  122. data/examples/snippets/commit-timestamp/README.md +18 -0
  123. data/examples/snippets/commit-timestamp/Rakefile +13 -0
  124. data/examples/snippets/commit-timestamp/application.rb +53 -0
  125. data/examples/snippets/commit-timestamp/config/database.yml +8 -0
  126. data/examples/snippets/commit-timestamp/db/migrate/01_create_tables.rb +26 -0
  127. data/examples/snippets/commit-timestamp/db/schema.rb +29 -0
  128. data/examples/snippets/commit-timestamp/db/seeds.rb +5 -0
  129. data/examples/snippets/commit-timestamp/models/album.rb +9 -0
  130. data/examples/snippets/commit-timestamp/models/singer.rb +9 -0
  131. data/examples/snippets/config/environment.rb +21 -0
  132. data/examples/snippets/create-records/README.md +12 -0
  133. data/examples/snippets/create-records/Rakefile +13 -0
  134. data/examples/snippets/create-records/application.rb +42 -0
  135. data/examples/snippets/create-records/config/database.yml +8 -0
  136. data/examples/snippets/create-records/db/migrate/01_create_tables.rb +21 -0
  137. data/examples/snippets/create-records/db/schema.rb +26 -0
  138. data/examples/snippets/create-records/db/seeds.rb +5 -0
  139. data/examples/snippets/create-records/models/album.rb +9 -0
  140. data/examples/snippets/create-records/models/singer.rb +9 -0
  141. data/examples/snippets/date-data-type/README.md +19 -0
  142. data/examples/snippets/date-data-type/Rakefile +13 -0
  143. data/examples/snippets/date-data-type/application.rb +35 -0
  144. data/examples/snippets/date-data-type/config/database.yml +8 -0
  145. data/examples/snippets/date-data-type/db/migrate/01_create_tables.rb +20 -0
  146. data/examples/snippets/date-data-type/db/schema.rb +21 -0
  147. data/examples/snippets/date-data-type/db/seeds.rb +16 -0
  148. data/examples/snippets/date-data-type/models/singer.rb +8 -0
  149. data/examples/snippets/generated-column/README.md +41 -0
  150. data/examples/snippets/generated-column/Rakefile +13 -0
  151. data/examples/snippets/generated-column/application.rb +37 -0
  152. data/examples/snippets/generated-column/config/database.yml +8 -0
  153. data/examples/snippets/generated-column/db/migrate/01_create_tables.rb +23 -0
  154. data/examples/snippets/generated-column/db/schema.rb +21 -0
  155. data/examples/snippets/generated-column/db/seeds.rb +18 -0
  156. data/examples/snippets/generated-column/models/singer.rb +8 -0
  157. data/examples/snippets/hints/README.md +19 -0
  158. data/examples/snippets/hints/Rakefile +13 -0
  159. data/examples/snippets/hints/application.rb +47 -0
  160. data/examples/snippets/hints/config/database.yml +8 -0
  161. data/examples/snippets/hints/db/migrate/01_create_tables.rb +23 -0
  162. data/examples/snippets/hints/db/schema.rb +28 -0
  163. data/examples/snippets/hints/db/seeds.rb +29 -0
  164. data/examples/snippets/hints/models/album.rb +9 -0
  165. data/examples/snippets/hints/models/singer.rb +9 -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/partitioned-dml/README.md +16 -0
  195. data/examples/snippets/partitioned-dml/Rakefile +13 -0
  196. data/examples/snippets/partitioned-dml/application.rb +48 -0
  197. data/examples/snippets/partitioned-dml/config/database.yml +8 -0
  198. data/examples/snippets/partitioned-dml/db/migrate/01_create_tables.rb +21 -0
  199. data/examples/snippets/partitioned-dml/db/schema.rb +26 -0
  200. data/examples/snippets/partitioned-dml/db/seeds.rb +29 -0
  201. data/examples/snippets/partitioned-dml/models/album.rb +9 -0
  202. data/examples/snippets/partitioned-dml/models/singer.rb +9 -0
  203. data/examples/snippets/quickstart/README.md +26 -0
  204. data/examples/snippets/quickstart/Rakefile +13 -0
  205. data/examples/snippets/quickstart/application.rb +51 -0
  206. data/examples/snippets/quickstart/config/database.yml +8 -0
  207. data/examples/snippets/quickstart/db/migrate/01_create_tables.rb +21 -0
  208. data/examples/snippets/quickstart/db/schema.rb +26 -0
  209. data/examples/snippets/quickstart/db/seeds.rb +24 -0
  210. data/examples/snippets/quickstart/models/album.rb +9 -0
  211. data/examples/snippets/quickstart/models/singer.rb +9 -0
  212. data/examples/snippets/read-only-transactions/README.md +13 -0
  213. data/examples/snippets/read-only-transactions/Rakefile +13 -0
  214. data/examples/snippets/read-only-transactions/application.rb +77 -0
  215. data/examples/snippets/read-only-transactions/config/database.yml +8 -0
  216. data/examples/snippets/read-only-transactions/db/migrate/01_create_tables.rb +21 -0
  217. data/examples/snippets/read-only-transactions/db/schema.rb +26 -0
  218. data/examples/snippets/read-only-transactions/db/seeds.rb +24 -0
  219. data/examples/snippets/read-only-transactions/models/album.rb +9 -0
  220. data/examples/snippets/read-only-transactions/models/singer.rb +9 -0
  221. data/examples/snippets/read-write-transactions/README.md +12 -0
  222. data/examples/snippets/read-write-transactions/Rakefile +13 -0
  223. data/examples/snippets/read-write-transactions/application.rb +39 -0
  224. data/examples/snippets/read-write-transactions/config/database.yml +8 -0
  225. data/examples/snippets/read-write-transactions/db/migrate/01_create_tables.rb +22 -0
  226. data/examples/snippets/read-write-transactions/db/schema.rb +27 -0
  227. data/examples/snippets/read-write-transactions/db/seeds.rb +25 -0
  228. data/examples/snippets/read-write-transactions/models/album.rb +9 -0
  229. data/examples/snippets/read-write-transactions/models/singer.rb +9 -0
  230. data/examples/snippets/stale-reads/README.md +27 -0
  231. data/examples/snippets/stale-reads/Rakefile +13 -0
  232. data/examples/snippets/stale-reads/application.rb +63 -0
  233. data/examples/snippets/stale-reads/config/database.yml +8 -0
  234. data/examples/snippets/stale-reads/db/migrate/01_create_tables.rb +21 -0
  235. data/examples/snippets/stale-reads/db/schema.rb +26 -0
  236. data/examples/snippets/stale-reads/db/seeds.rb +24 -0
  237. data/examples/snippets/stale-reads/models/album.rb +9 -0
  238. data/examples/snippets/stale-reads/models/singer.rb +9 -0
  239. data/examples/snippets/timestamp-data-type/README.md +17 -0
  240. data/examples/snippets/timestamp-data-type/Rakefile +13 -0
  241. data/examples/snippets/timestamp-data-type/application.rb +42 -0
  242. data/examples/snippets/timestamp-data-type/config/database.yml +8 -0
  243. data/examples/snippets/timestamp-data-type/db/migrate/01_create_tables.rb +21 -0
  244. data/examples/snippets/timestamp-data-type/db/schema.rb +21 -0
  245. data/examples/snippets/timestamp-data-type/db/seeds.rb +6 -0
  246. data/examples/snippets/timestamp-data-type/models/meeting.rb +19 -0
  247. data/examples/solidus/README.md +172 -0
  248. data/lib/active_record/connection_adapters/spanner/database_statements.rb +244 -266
  249. data/lib/active_record/connection_adapters/spanner/quoting.rb +42 -50
  250. data/lib/active_record/connection_adapters/spanner/schema_cache.rb +43 -0
  251. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +125 -9
  252. data/lib/active_record/connection_adapters/spanner/schema_definitions.rb +122 -0
  253. data/lib/active_record/connection_adapters/spanner/schema_dumper.rb +19 -0
  254. data/lib/active_record/connection_adapters/spanner/schema_statements.rb +553 -139
  255. data/lib/active_record/connection_adapters/spanner/type_metadata.rb +37 -0
  256. data/lib/active_record/connection_adapters/spanner_adapter.rb +185 -78
  257. data/lib/active_record/tasks/spanner_database_tasks.rb +74 -0
  258. data/lib/active_record/type/spanner/array.rb +32 -0
  259. data/lib/active_record/type/spanner/bytes.rb +26 -0
  260. data/lib/active_record/type/spanner/spanner_active_record_converter.rb +33 -0
  261. data/lib/active_record/type/spanner/time.rb +37 -0
  262. data/lib/activerecord-spanner-adapter.rb +23 -0
  263. data/lib/activerecord_spanner_adapter/base.rb +238 -0
  264. data/lib/activerecord_spanner_adapter/connection.rb +350 -0
  265. data/lib/activerecord_spanner_adapter/errors.rb +13 -0
  266. data/lib/activerecord_spanner_adapter/foreign_key.rb +29 -0
  267. data/lib/activerecord_spanner_adapter/index/column.rb +38 -0
  268. data/lib/activerecord_spanner_adapter/index.rb +80 -0
  269. data/lib/activerecord_spanner_adapter/information_schema.rb +262 -0
  270. data/lib/activerecord_spanner_adapter/primary_key.rb +31 -0
  271. data/lib/activerecord_spanner_adapter/table/column.rb +59 -0
  272. data/lib/activerecord_spanner_adapter/table.rb +61 -0
  273. data/lib/activerecord_spanner_adapter/transaction.rb +154 -0
  274. data/lib/activerecord_spanner_adapter/version.rb +9 -0
  275. data/lib/arel/visitors/spanner.rb +111 -0
  276. data/lib/spanner_client_ext.rb +107 -0
  277. data/renovate.json +5 -0
  278. metadata +405 -34
  279. data/.travis.yml +0 -5
  280. data/lib/active_record/connection_adapters/spanner/client.rb +0 -190
  281. data/lib/active_record/connection_adapters/spanner.rb +0 -10
  282. 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
- 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
29
- end
30
- end
31
-
32
- private
33
- # To be overridden
34
- def binds
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
- 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)
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
- 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
53
+ def query sql, name = nil
54
+ exec_query sql, name
125
55
  end
126
56
 
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
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
- NOT_SIMPLE = nil
64
+ def exec_mutation mutation
65
+ @connection.current_transaction.buffer mutation
66
+ end
133
67
 
134
- def initialize(relation, pk, binds)
135
- super()
136
- @relation = relation
137
- @pk = pk
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
- attr_reader :binds
142
- private :binds
75
+ raise "Unsupported update for use with mutations: #{arel}" unless arel.is_a? Arel::DeleteManager
143
76
 
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
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
- def visit_Arel_Nodes_Equality(o)
153
- if pk_cond?(o)
154
- [accept(o.right)]
155
- else
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
- def visit_Arel_Nodes_In(o)
161
- return nil unless pk_cond?(o)
106
+ def write_query? sql
107
+ sql_statement_type(sql) == :dml
108
+ end
162
109
 
163
- if o.kind_of?(Array) and o.empty?
164
- []
165
- else
166
- accept(o.right)
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
- def unsupported(o)
171
- return NOT_SIMPLE
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
- 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
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 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
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
- log(fake_sql, name) do
210
- with_phase_transition {|client| client.insert(table, row) }
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
- 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 ?"
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
- fake_sql = "UPDATE #{quote_column_name(table)} SET #{fake_set} WHERE #{pk} = ?"
184
+ raise "Unsupported isolation level: #{isolation}" unless \
185
+ [:serializable, :read_only, :buffered_mutations, :pdml].include? isolation
231
186
  end
232
187
 
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) }
188
+ log "BEGIN #{isolation}" do
189
+ @connection.begin_transaction isolation
238
190
  end
239
- target.size
240
191
  end
241
192
 
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
193
+ def commit_db_transaction
194
+ log "COMMIT" do
195
+ @connection.commit_transaction
256
196
  end
197
+ end
257
198
 
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
199
+ def rollback_db_transaction
200
+ log "ROLLBACK" do
201
+ @connection.rollback_transaction
267
202
  end
203
+ end
268
204
 
269
- log(fake_sql, name, binds) do
270
- with_phase_transition {|client| client.delete(table, keyset) }
271
- end
205
+ private
272
206
 
273
- keyset.size
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
- def execute(stmt)
277
- case stmt
278
- when Spanner::DDL
279
- execute_ddl(stmt)
280
- else
281
- super(stmt)
282
- end
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 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
- }
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
- 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)
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
- def begin_db_transaction
311
- with_phase_transition {|client| client.begin_transaction }
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
- def begin_isolated_db_transaction(isolation)
315
- with_phase_transition {|client| client.begin_snapshot(**isolation) }
316
- end
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 commit_db_transaction
319
- with_phase_transition {|client| client.commit }
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
- def exec_rollback_db_transaction
323
- with_phase_transition {|client| client.rollback }
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
- def with_phase_transition
327
- result = yield client
328
- @client = client.next
329
- result
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
-