activerecord-spanner-adapter 0.1.0 → 0.7.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 (292) 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 +42 -0
  23. data/CODE_OF_CONDUCT.md +40 -0
  24. data/CONTRIBUTING.md +79 -0
  25. data/Gemfile +9 -5
  26. data/LICENSE +6 -6
  27. data/README.md +67 -30
  28. data/Rakefile +74 -2
  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/interleaved-tables/README.md +152 -0
  167. data/examples/snippets/interleaved-tables/Rakefile +13 -0
  168. data/examples/snippets/interleaved-tables/application.rb +109 -0
  169. data/examples/snippets/interleaved-tables/config/database.yml +8 -0
  170. data/examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb +44 -0
  171. data/examples/snippets/interleaved-tables/db/schema.rb +32 -0
  172. data/examples/snippets/interleaved-tables/db/seeds.rb +40 -0
  173. data/examples/snippets/interleaved-tables/models/album.rb +15 -0
  174. data/examples/snippets/interleaved-tables/models/singer.rb +20 -0
  175. data/examples/snippets/interleaved-tables/models/track.rb +25 -0
  176. data/examples/snippets/migrations/README.md +43 -0
  177. data/examples/snippets/migrations/Rakefile +13 -0
  178. data/examples/snippets/migrations/application.rb +26 -0
  179. data/examples/snippets/migrations/config/database.yml +8 -0
  180. data/examples/snippets/migrations/db/migrate/01_create_tables.rb +28 -0
  181. data/examples/snippets/migrations/db/schema.rb +33 -0
  182. data/examples/snippets/migrations/db/seeds.rb +5 -0
  183. data/examples/snippets/migrations/models/album.rb +10 -0
  184. data/examples/snippets/migrations/models/singer.rb +10 -0
  185. data/examples/snippets/migrations/models/track.rb +9 -0
  186. data/examples/snippets/mutations/README.md +34 -0
  187. data/examples/snippets/mutations/Rakefile +13 -0
  188. data/examples/snippets/mutations/application.rb +47 -0
  189. data/examples/snippets/mutations/config/database.yml +8 -0
  190. data/examples/snippets/mutations/db/migrate/01_create_tables.rb +22 -0
  191. data/examples/snippets/mutations/db/schema.rb +27 -0
  192. data/examples/snippets/mutations/db/seeds.rb +25 -0
  193. data/examples/snippets/mutations/models/album.rb +9 -0
  194. data/examples/snippets/mutations/models/singer.rb +9 -0
  195. data/examples/snippets/optimistic-locking/README.md +12 -0
  196. data/examples/snippets/optimistic-locking/Rakefile +13 -0
  197. data/examples/snippets/optimistic-locking/application.rb +48 -0
  198. data/examples/snippets/optimistic-locking/config/database.yml +8 -0
  199. data/examples/snippets/optimistic-locking/db/migrate/01_create_tables.rb +26 -0
  200. data/examples/snippets/optimistic-locking/db/schema.rb +29 -0
  201. data/examples/snippets/optimistic-locking/db/seeds.rb +25 -0
  202. data/examples/snippets/optimistic-locking/models/album.rb +9 -0
  203. data/examples/snippets/optimistic-locking/models/singer.rb +9 -0
  204. data/examples/snippets/partitioned-dml/README.md +16 -0
  205. data/examples/snippets/partitioned-dml/Rakefile +13 -0
  206. data/examples/snippets/partitioned-dml/application.rb +48 -0
  207. data/examples/snippets/partitioned-dml/config/database.yml +8 -0
  208. data/examples/snippets/partitioned-dml/db/migrate/01_create_tables.rb +21 -0
  209. data/examples/snippets/partitioned-dml/db/schema.rb +26 -0
  210. data/examples/snippets/partitioned-dml/db/seeds.rb +29 -0
  211. data/examples/snippets/partitioned-dml/models/album.rb +9 -0
  212. data/examples/snippets/partitioned-dml/models/singer.rb +9 -0
  213. data/examples/snippets/quickstart/README.md +26 -0
  214. data/examples/snippets/quickstart/Rakefile +13 -0
  215. data/examples/snippets/quickstart/application.rb +51 -0
  216. data/examples/snippets/quickstart/config/database.yml +8 -0
  217. data/examples/snippets/quickstart/db/migrate/01_create_tables.rb +21 -0
  218. data/examples/snippets/quickstart/db/schema.rb +26 -0
  219. data/examples/snippets/quickstart/db/seeds.rb +24 -0
  220. data/examples/snippets/quickstart/models/album.rb +9 -0
  221. data/examples/snippets/quickstart/models/singer.rb +9 -0
  222. data/examples/snippets/read-only-transactions/README.md +13 -0
  223. data/examples/snippets/read-only-transactions/Rakefile +13 -0
  224. data/examples/snippets/read-only-transactions/application.rb +77 -0
  225. data/examples/snippets/read-only-transactions/config/database.yml +8 -0
  226. data/examples/snippets/read-only-transactions/db/migrate/01_create_tables.rb +21 -0
  227. data/examples/snippets/read-only-transactions/db/schema.rb +26 -0
  228. data/examples/snippets/read-only-transactions/db/seeds.rb +24 -0
  229. data/examples/snippets/read-only-transactions/models/album.rb +9 -0
  230. data/examples/snippets/read-only-transactions/models/singer.rb +9 -0
  231. data/examples/snippets/read-write-transactions/README.md +12 -0
  232. data/examples/snippets/read-write-transactions/Rakefile +13 -0
  233. data/examples/snippets/read-write-transactions/application.rb +39 -0
  234. data/examples/snippets/read-write-transactions/config/database.yml +8 -0
  235. data/examples/snippets/read-write-transactions/db/migrate/01_create_tables.rb +22 -0
  236. data/examples/snippets/read-write-transactions/db/schema.rb +27 -0
  237. data/examples/snippets/read-write-transactions/db/seeds.rb +25 -0
  238. data/examples/snippets/read-write-transactions/models/album.rb +9 -0
  239. data/examples/snippets/read-write-transactions/models/singer.rb +9 -0
  240. data/examples/snippets/stale-reads/README.md +27 -0
  241. data/examples/snippets/stale-reads/Rakefile +13 -0
  242. data/examples/snippets/stale-reads/application.rb +63 -0
  243. data/examples/snippets/stale-reads/config/database.yml +8 -0
  244. data/examples/snippets/stale-reads/db/migrate/01_create_tables.rb +21 -0
  245. data/examples/snippets/stale-reads/db/schema.rb +26 -0
  246. data/examples/snippets/stale-reads/db/seeds.rb +24 -0
  247. data/examples/snippets/stale-reads/models/album.rb +9 -0
  248. data/examples/snippets/stale-reads/models/singer.rb +9 -0
  249. data/examples/snippets/timestamp-data-type/README.md +17 -0
  250. data/examples/snippets/timestamp-data-type/Rakefile +13 -0
  251. data/examples/snippets/timestamp-data-type/application.rb +42 -0
  252. data/examples/snippets/timestamp-data-type/config/database.yml +8 -0
  253. data/examples/snippets/timestamp-data-type/db/migrate/01_create_tables.rb +21 -0
  254. data/examples/snippets/timestamp-data-type/db/schema.rb +21 -0
  255. data/examples/snippets/timestamp-data-type/db/seeds.rb +6 -0
  256. data/examples/snippets/timestamp-data-type/models/meeting.rb +19 -0
  257. data/examples/solidus/README.md +172 -0
  258. data/lib/active_record/connection_adapters/spanner/database_statements.rb +244 -251
  259. data/lib/active_record/connection_adapters/spanner/quoting.rb +42 -50
  260. data/lib/active_record/connection_adapters/spanner/schema_cache.rb +43 -0
  261. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +129 -7
  262. data/lib/active_record/connection_adapters/spanner/schema_definitions.rb +122 -0
  263. data/lib/active_record/connection_adapters/spanner/schema_dumper.rb +19 -0
  264. data/lib/active_record/connection_adapters/spanner/schema_statements.rb +553 -141
  265. data/lib/active_record/connection_adapters/spanner/type_metadata.rb +37 -0
  266. data/lib/active_record/connection_adapters/spanner_adapter.rb +188 -70
  267. data/lib/active_record/tasks/spanner_database_tasks.rb +74 -0
  268. data/lib/active_record/type/spanner/array.rb +32 -0
  269. data/lib/active_record/type/spanner/bytes.rb +26 -0
  270. data/lib/active_record/type/spanner/spanner_active_record_converter.rb +33 -0
  271. data/lib/active_record/type/spanner/time.rb +37 -0
  272. data/lib/activerecord-spanner-adapter.rb +23 -0
  273. data/lib/activerecord_spanner_adapter/base.rb +238 -0
  274. data/lib/activerecord_spanner_adapter/connection.rb +324 -0
  275. data/lib/activerecord_spanner_adapter/errors.rb +13 -0
  276. data/lib/activerecord_spanner_adapter/foreign_key.rb +29 -0
  277. data/lib/activerecord_spanner_adapter/index/column.rb +38 -0
  278. data/lib/activerecord_spanner_adapter/index.rb +80 -0
  279. data/lib/activerecord_spanner_adapter/information_schema.rb +261 -0
  280. data/lib/activerecord_spanner_adapter/primary_key.rb +31 -0
  281. data/lib/activerecord_spanner_adapter/table/column.rb +59 -0
  282. data/lib/activerecord_spanner_adapter/table.rb +61 -0
  283. data/lib/activerecord_spanner_adapter/transaction.rb +123 -0
  284. data/lib/activerecord_spanner_adapter/version.rb +9 -0
  285. data/lib/arel/visitors/spanner.rb +111 -0
  286. data/lib/spanner_client_ext.rb +103 -0
  287. data/renovate.json +5 -0
  288. metadata +417 -36
  289. data/.gitmodules +0 -3
  290. data/.travis.yml +0 -5
  291. data/lib/active_record/connection_adapters/spanner.rb +0 -10
  292. data/lib/activerecord-spanner-adapter/version.rb +0 -3
@@ -1,320 +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
13
+ # DDL, DML and DQL Statements
6
14
 
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
15
+ def execute sql, name = nil, binds = []
16
+ statement_type = sql_statement_type sql
12
17
 
13
- # A mixin which provides ways to resolve rvalues.
14
- module RightValueResolveable
15
- def visit_Arel_Nodes_BindParam(o)
16
- binds.shift
18
+ if preventing_writes? && [:dml, :ddl].include?(statement_type)
19
+ raise ActiveRecord::ReadOnlyError(
20
+ "Write query attempted while in readonly mode: #{sql}"
21
+ )
17
22
  end
18
23
 
19
- def visit_Array(o)
20
- o.map {|item| accept(item) }
21
- end
22
-
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
+ # 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
29
36
  end
30
- end
31
37
 
32
- private
33
- # To be overridden
34
- def binds
35
- raise NotImplementedError
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
48
+ end
49
+ end
36
50
  end
37
51
  end
38
52
 
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)
53
+ def query sql, name = nil
54
+ exec_query sql, name
55
+ end
70
56
 
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
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
74
63
 
75
- ids = WhereVisitor.new(o.relation, pk, binds).accept(o.wheres[0])
76
- raise NotImplementedError 'UPDATE statements with complex WHERE clause' unless ids
64
+ def exec_mutation mutation
65
+ @connection.current_transaction.buffer mutation
66
+ end
77
67
 
78
- [table, ids, values]
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")
79
72
  end
73
+ return super unless should_use_mutation arel
80
74
 
81
- def visit_Arel_Nodes_DeleteStatement(o)
82
- table = o.relation.name
75
+ raise "Unsupported update for use with mutations: #{arel}" unless arel.is_a? Arel::DeleteManager
83
76
 
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
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
106
97
 
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
98
+ def truncate table_name, name = nil
99
+ Array(table_name).each do |t|
100
+ log "TRUNCATE #{t}", name do
101
+ @connection.truncate t
115
102
  end
116
103
  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
104
  end
126
105
 
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
106
+ def write_query? sql
107
+ sql_statement_type(sql) == :dml
108
+ end
133
109
 
134
- def initialize(relation, pk, binds)
135
- super()
136
- @relation = relation
137
- @pk = pk
138
- @binds = binds
110
+ def execute_ddl statements
111
+ log "MIGRATION", "SCHEMA" do
112
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
113
+ @connection.execute_ddl statements
114
+ end
139
115
  end
116
+ rescue Google::Cloud::Error => error
117
+ raise ActiveRecord::StatementInvalid, error
118
+ end
140
119
 
141
- attr_reader :binds
142
- private :binds
120
+ # Transaction
143
121
 
144
- def visit_Arel_Nodes_And(o)
145
- if o.children.size == 1
146
- accept(o.left)
147
- else
148
- NOT_SIMPLE
149
- end
122
+ def transaction requires_new: nil, isolation: nil, joinable: true
123
+ if !requires_new && current_transaction.joinable?
124
+ return super
150
125
  end
151
126
 
152
- def visit_Arel_Nodes_Equality(o)
153
- if pk_cond?(o)
154
- [accept(o.right)]
155
- else
156
- NOT_SIMPLE
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
157
134
  end
135
+ raise
158
136
  end
137
+ end
159
138
 
160
- def visit_Arel_Nodes_In(o)
161
- return nil unless pk_cond?(o)
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"
150
+ }
151
+ end
162
152
 
163
- if o.kind_of?(Array) and o.empty?
164
- []
165
- else
166
- accept(o.right)
167
- end
153
+ def begin_db_transaction
154
+ log "BEGIN" do
155
+ @connection.begin_transaction
168
156
  end
157
+ end
169
158
 
170
- def unsupported(o)
171
- return NOT_SIMPLE
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
183
+ else
184
+ raise "Unsupported isolation level: #{isolation}" unless \
185
+ [:serializable, :read_only, :buffered_mutations, :pdml].include? isolation
172
186
  end
173
187
 
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
188
+ log "BEGIN #{isolation}" do
189
+ @connection.begin_transaction isolation
192
190
  end
193
191
  end
194
192
 
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
207
- }
208
-
209
- log(fake_sql, name) do
210
- session.commit do |c|
211
- c.insert table, row
212
- end
193
+ def commit_db_transaction
194
+ log "COMMIT" do
195
+ @connection.commit_transaction
213
196
  end
214
-
215
- id_value
216
197
  end
217
198
 
218
- def update(arel, name = nil, binds = [])
219
- raise NotImplementedError, "DELETE in raw SQL is not supported" unless arel.respond_to?(:ast)
220
-
221
- type_casted_binds = binds.map {|attr| type_cast(attr.value_for_database) }
222
- table, target, values = MutationVisitor.new(self, type_casted_binds.dup).accept(arel.ast)
223
-
224
- fake_set = values.map {|col, val|
225
- "#{quote_column_name(col)} = #{quote(val)}"
226
- }.join(', ')
227
-
228
- pk = primary_key(table)
229
- if target.size > 1
230
- fake_sql = "UPDATE #{quote_column_name(table)} SET #{fake_set} WHERE #{pk} IN ?"
231
- else
232
- fake_sql = "UPDATE #{quote_column_name(table)} SET #{fake_set} WHERE #{pk} = ?"
199
+ def rollback_db_transaction
200
+ log "ROLLBACK" do
201
+ @connection.rollback_transaction
233
202
  end
203
+ end
234
204
 
235
- row = values.inject({}) {|r, (col, val)| r[col] = val; r }
236
- rows = target.map {|id| row.merge(pk => id) }
205
+ private
237
206
 
238
- log(fake_sql, name, binds) do
239
- session.commit do |c|
240
- c.update(table, rows)
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)
241
214
  end
242
- end
243
-
244
- true
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]
245
229
  end
246
230
 
247
- def delete(arel, name, binds)
248
- raise NotImplementedError, "DELETE in raw SQL is not supported" unless arel.respond_to?(:ast)
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) \
238
+ end
249
239
 
250
- type_casted_binds = binds.map {|attr| type_cast(attr.value_for_database) }
251
- table, target, wheres = MutationVisitor.new(self, type_casted_binds.dup).accept(arel.ast)
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
252
244
 
253
- # TODO(yugui) Support composite primary key?
254
- pk = primary_key(table)
255
- if target.nil?
256
- where_clause = visitor.accept(wheres, collector).compile(binds.dup, self)
257
- # TODO(yugui) keep consistency with transaction
258
- target = select_values(<<~"SQL", name, binds)
259
- SELECT #{quote_column_name(pk)} FROM #{quote_table_name(table)} WHERE #{where_clause}
260
- SQL
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"
261
248
  end
262
-
263
- if target == :all
264
- keyset = []
265
- fake_sql = "DELETE FROM #{quote_column_name(table)}"
266
- elsif target.size > 1
267
- fake_sql = "DELETE FROM #{quote_column_name(table)} WHERE (primary-key) IN ?"
268
- keyset = target
269
- else
270
- fake_sql = "DELETE FROM #{quote_column_name(table)} WHERE (primary-key) = ?"
271
- keyset = target
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"
272
252
  end
273
-
274
- log(fake_sql, name, binds) do
275
- session.commit do |c|
276
- c.delete(table, keyset)
277
- 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"
278
257
  end
279
258
 
280
- keyset.size
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
+ )
281
265
  end
282
266
 
283
- def execute(stmt)
284
- case stmt
285
- when Spanner::DDL
286
- execute_ddl(stmt)
287
- else
288
- super(stmt)
289
- 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
271
+
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)}/
290
275
  end
291
276
 
292
- def exec_query(sql, name = 'SQL', binds = [], prepare: :ignored)
293
- case
294
- when binds.kind_of?(Hash)
295
- spanner_binds = binds
296
- binds = binds.values
297
- when binds.respond_to?(:to_hash)
298
- spanner_binds = binds.to_hash
299
- binds = spanner_binds.values
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
300
287
  else
301
- spanner_binds = binds.each_with_index.inject({}) {|b, (attr, i)|
302
- b["p#{i+1}"] = type_cast(attr.value_for_database)
303
- b
304
- }
288
+ :dql
305
289
  end
290
+ end
306
291
 
307
- log(sql, name, binds) do
308
- results = session.execute(sql, params: spanner_binds, streaming: false)
309
- columns = results.types.map(&:first)
310
- rows = results.rows.map {|row|
311
- columns.map {|col| row[col] }
312
- }
313
- ActiveRecord::Result.new(columns.map(&:to_s), rows)
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)
314
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
315
309
  end
316
310
  end
317
311
  end
318
312
  end
319
313
  end
320
-