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
@@ -0,0 +1,238 @@
1
+ # Copyright 2021 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ module ActiveRecord
8
+ class TableMetadata # :nodoc:
9
+ # This attr_reader is private in ActiveRecord 6.0.x and public in 6.1.x. This makes sure it is always available in
10
+ # the Spanner adapter.
11
+ attr_reader :arel_table
12
+ end
13
+
14
+ class Base
15
+ # Creates an object (or multiple objects) and saves it to the database. This method will use mutations instead
16
+ # of DML if there is no active transaction, or if the active transaction has been created with the option
17
+ # isolation: :buffered_mutations.
18
+ def self.create! attributes = nil, &block
19
+ return super unless spanner_adapter?
20
+ return super if active_transaction?
21
+
22
+ transaction isolation: :buffered_mutations do
23
+ return super
24
+ end
25
+ end
26
+
27
+ def self.create attributes = nil, &block
28
+ return super unless spanner_adapter?
29
+ return super if active_transaction?
30
+
31
+ transaction isolation: :buffered_mutations do
32
+ return super
33
+ end
34
+ end
35
+
36
+ def self.spanner_adapter?
37
+ connection.adapter_name == "spanner"
38
+ end
39
+
40
+ def self.buffered_mutations?
41
+ spanner_adapter? && connection&.current_spanner_transaction&.isolation == :buffered_mutations
42
+ end
43
+
44
+ def self._insert_record values
45
+ return super unless buffered_mutations?
46
+
47
+ primary_key = self.primary_key
48
+ primary_key_value = nil
49
+
50
+ if primary_key && values.is_a?(Hash)
51
+ primary_key_value = values[primary_key]
52
+
53
+ if !primary_key_value && prefetch_primary_key?
54
+ primary_key_value = next_sequence_value
55
+ values[primary_key] = primary_key_value
56
+ end
57
+ end
58
+
59
+ metadata = TableMetadata.new self, arel_table
60
+ columns, grpc_values = _create_grpc_values_for_insert metadata, values
61
+
62
+ mutation = Google::Cloud::Spanner::V1::Mutation.new(
63
+ insert: Google::Cloud::Spanner::V1::Mutation::Write.new(
64
+ table: arel_table.name,
65
+ columns: columns,
66
+ values: [grpc_values.list_value]
67
+ )
68
+ )
69
+ connection.current_spanner_transaction.buffer mutation
70
+
71
+ primary_key_value
72
+ end
73
+
74
+ # Deletes all records of this class. This method will use mutations instead of DML if there is no active
75
+ # transaction, or if the active transaction has been created with the option isolation: :buffered_mutations.
76
+ def self.delete_all
77
+ return super unless spanner_adapter?
78
+ return super if active_transaction?
79
+
80
+ transaction isolation: :buffered_mutations do
81
+ return super
82
+ end
83
+ end
84
+
85
+ def self.active_transaction?
86
+ current_transaction = connection.current_transaction
87
+ !(current_transaction.nil? || current_transaction.is_a?(ConnectionAdapters::NullTransaction))
88
+ end
89
+
90
+ # Updates the given attributes of the object in the database. This method will use mutations instead
91
+ # of DML if there is no active transaction, or if the active transaction has been created with the option
92
+ # isolation: :buffered_mutations.
93
+ def update attributes
94
+ return super unless self.class.spanner_adapter?
95
+ return super if self.class.active_transaction?
96
+
97
+ transaction isolation: :buffered_mutations do
98
+ return super
99
+ end
100
+ end
101
+
102
+ # Deletes the object in the database. This method will use mutations instead
103
+ # of DML if there is no active transaction, or if the active transaction has been created with the option
104
+ # isolation: :buffered_mutations.
105
+ def destroy
106
+ return super unless self.class.spanner_adapter?
107
+ return super if self.class.active_transaction?
108
+
109
+ transaction isolation: :buffered_mutations do
110
+ return super
111
+ end
112
+ end
113
+
114
+ private
115
+
116
+ def self._create_grpc_values_for_insert metadata, values
117
+ serialized_values = []
118
+ columns = []
119
+ values.each_pair do |k, v|
120
+ type = metadata.type k
121
+ serialized_values << (type.method(:serialize).arity < 0 ? type.serialize(v, :mutation) : type.serialize(v))
122
+ columns << metadata.arel_table[k].name
123
+ end
124
+ [columns, Google::Protobuf::Value.new(list_value:
125
+ Google::Protobuf::ListValue.new(
126
+ values: serialized_values.map do |value|
127
+ Google::Cloud::Spanner::Convert.object_to_grpc_value value
128
+ end
129
+ ))]
130
+ end
131
+ private_class_method :_create_grpc_values_for_insert
132
+
133
+ def _update_row attribute_names, attempted_action = "update"
134
+ return super unless self.class.buffered_mutations?
135
+
136
+ if locking_enabled?
137
+ _execute_version_check attempted_action
138
+ attribute_names << self.class.locking_column
139
+ self[self.class.locking_column] += 1
140
+ end
141
+
142
+ metadata = TableMetadata.new self.class, self.class.arel_table
143
+ values = attributes_with_values attribute_names
144
+ columns, grpc_values = _create_grpc_values_for_update metadata, values
145
+
146
+ mutation = Google::Cloud::Spanner::V1::Mutation.new(
147
+ update: Google::Cloud::Spanner::V1::Mutation::Write.new(
148
+ table: self.class.arel_table.name,
149
+ columns: columns,
150
+ values: [grpc_values.list_value]
151
+ )
152
+ )
153
+ self.class.connection.current_spanner_transaction.buffer mutation
154
+ 1 # Affected rows
155
+ end
156
+
157
+ def _create_grpc_values_for_update metadata, values
158
+ constraints = {}
159
+ keys = self.class.primary_and_parent_key
160
+ keys.each do |key|
161
+ constraints[key] = attribute_in_database key
162
+ end
163
+
164
+ # Use both the where values + the values that are actually set.
165
+ all_values = [constraints, values]
166
+ all_serialized_values = []
167
+ all_columns = []
168
+ all_values.each do |h|
169
+ h.each_pair do |k, v|
170
+ type = metadata.type k
171
+ has_serialize_options = type.method(:serialize).arity < 0
172
+ all_serialized_values << (has_serialize_options ? type.serialize(v, :mutation) : type.serialize(v))
173
+ all_columns << metadata.arel_table[k].name
174
+ end
175
+ end
176
+ [all_columns, Google::Protobuf::Value.new(list_value:
177
+ Google::Protobuf::ListValue.new(
178
+ values: all_serialized_values.map do |value|
179
+ Google::Cloud::Spanner::Convert.object_to_grpc_value value
180
+ end
181
+ ))]
182
+ end
183
+
184
+ def destroy_row
185
+ return super unless self.class.buffered_mutations?
186
+
187
+ _delete_row
188
+ end
189
+
190
+ def _delete_row
191
+ return super unless self.class.buffered_mutations?
192
+ if locking_enabled?
193
+ _execute_version_check "destroy"
194
+ end
195
+
196
+ metadata = TableMetadata.new self.class, self.class.arel_table
197
+ keys = self.class.primary_and_parent_key
198
+ serialized_values = serialize_keys metadata, keys
199
+ list_value = Google::Protobuf::ListValue.new(
200
+ values: serialized_values.map do |value|
201
+ Google::Cloud::Spanner::Convert.object_to_grpc_value value
202
+ end
203
+ )
204
+ mutation = Google::Cloud::Spanner::V1::Mutation.new(
205
+ delete: Google::Cloud::Spanner::V1::Mutation::Delete.new(
206
+ table: self.class.arel_table.name,
207
+ key_set: { keys: [list_value] }
208
+ )
209
+ )
210
+ self.class.connection.current_spanner_transaction.buffer mutation
211
+ 1 # Affected rows
212
+ end
213
+
214
+ def serialize_keys metadata, keys
215
+ serialized_values = []
216
+ keys.each do |key|
217
+ type = metadata.type key
218
+ has_serialize_options = type.method(:serialize).arity < 0
219
+ serialized_values << type.serialize(attribute_in_database(key), :mutation) if has_serialize_options
220
+ serialized_values << type.serialize(attribute_in_database(key)) unless has_serialize_options
221
+ end
222
+ serialized_values
223
+ end
224
+
225
+ def _execute_version_check attempted_action
226
+ locking_column = self.class.locking_column
227
+ previous_lock_value = read_attribute_before_type_cast locking_column
228
+
229
+ # We need to check the version using a SELECT query, as a mutation cannot include a WHERE clause.
230
+ sql = "SELECT 1 FROM `#{self.class.arel_table.name}` " \
231
+ "WHERE `#{self.class.primary_key}` = @id AND `#{locking_column}` = @lock_version"
232
+ params = { "id" => id_in_database, "lock_version" => previous_lock_value }
233
+ param_types = { "id" => :INT64, "lock_version" => :INT64 }
234
+ locked_row = self.class.connection.raw_connection.execute_query sql, params: params, types: param_types
235
+ raise ActiveRecord::StaleObjectError.new(self, attempted_action) unless locked_row.rows.any?
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,350 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ require "google/cloud/spanner"
8
+ require "spanner_client_ext"
9
+ require "activerecord_spanner_adapter/information_schema"
10
+
11
+ module ActiveRecordSpannerAdapter
12
+ class Connection
13
+ attr_reader :instance_id, :database_id, :spanner
14
+ attr_accessor :current_transaction
15
+
16
+ def initialize config
17
+ @instance_id = config[:instance]
18
+ @database_id = config[:database]
19
+ @spanner = self.class.spanners config
20
+ end
21
+
22
+ def self.spanners config
23
+ config = config.symbolize_keys
24
+ @spanners ||= {}
25
+ @mutex ||= Mutex.new
26
+ @mutex.synchronize do
27
+ @spanners[database_path(config)] ||= Google::Cloud::Spanner.new(
28
+ project_id: config[:project],
29
+ credentials: config[:credentials],
30
+ emulator_host: config[:emulator_host],
31
+ scope: config[:scope],
32
+ timeout: config[:timeout],
33
+ lib_name: "spanner-activerecord-adapter",
34
+ lib_version: ActiveRecordSpannerAdapter::VERSION
35
+ )
36
+ end
37
+ end
38
+
39
+ def self.information_schema config
40
+ @information_schemas ||= {}
41
+ @information_schemas[database_path(config)] ||= \
42
+ ActiveRecordSpannerAdapter::InformationSchema.new new(config)
43
+ end
44
+
45
+ def session
46
+ @last_used = Time.current
47
+ @session ||= spanner.create_session instance_id, database_id
48
+ end
49
+ alias connect! session
50
+
51
+ def active?
52
+ # This method should not initialize a session.
53
+ unless @session
54
+ return false
55
+ end
56
+ # Assume that it is still active if it has been used in the past 50 minutes.
57
+ if ((Time.current - @last_used) / 60).round < 50
58
+ return true
59
+ end
60
+ session.execute_query "SELECT 1"
61
+ true
62
+ rescue StandardError
63
+ false
64
+ end
65
+
66
+ def disconnect!
67
+ session.release!
68
+ true
69
+ ensure
70
+ @session = nil
71
+ end
72
+
73
+ def reset!
74
+ disconnect!
75
+ session
76
+ true
77
+ end
78
+
79
+ # Database Operations
80
+
81
+ def create_database
82
+ job = spanner.create_database instance_id, database_id
83
+ job.wait_until_done!
84
+ raise Google::Cloud::Error.from_error job.error if job.error?
85
+ job.database
86
+ end
87
+
88
+ def database
89
+ @database ||= begin
90
+ database = spanner.database instance_id, database_id
91
+ unless database
92
+ raise ActiveRecord::NoDatabaseError(
93
+ "#{spanner.project}/#{instance_id}/#{database_id}"
94
+ )
95
+ end
96
+ database
97
+ end
98
+ end
99
+
100
+ # DDL Statements
101
+
102
+ # @params [Array<String>, String] sql Single or list of statements
103
+ def execute_ddl statements, operation_id: nil, wait_until_done: true
104
+ raise "DDL cannot be executed during a transaction" if current_transaction&.active?
105
+ self.current_transaction = nil
106
+
107
+ statements = Array statements
108
+ return unless statements.any?
109
+
110
+ # If a DDL batch is active we only buffer the statements on the connection until the batch is run.
111
+ if @ddl_batch
112
+ @ddl_batch.push(*statements)
113
+ return true
114
+ end
115
+
116
+ execute_ddl_statements statements, operation_id, wait_until_done
117
+ end
118
+
119
+ # DDL Batching
120
+
121
+ ##
122
+ # Executes a set of DDL statements as one batch. This method raises an error if no block is given.
123
+ #
124
+ # @example
125
+ # connection.ddl_batch do
126
+ # connection.execute_ddl "CREATE TABLE `Users` (Id INT64, Name STRING(MAX)) PRIMARY KEY (Id)"
127
+ # connection.execute_ddl "CREATE INDEX Idx_Users_Name ON `Users` (Name)"
128
+ # end
129
+ def ddl_batch
130
+ raise Google::Cloud::FailedPreconditionError, "No block given for the DDL batch" unless block_given?
131
+ begin
132
+ start_batch_ddl
133
+ yield
134
+ run_batch
135
+ rescue StandardError
136
+ abort_batch
137
+ raise
138
+ ensure
139
+ @ddl_batch = nil
140
+ end
141
+ end
142
+
143
+ ##
144
+ # Returns true if this connection is currently executing a DDL batch, and otherwise false.
145
+ def ddl_batch?
146
+ return true if @ddl_batch
147
+ false
148
+ end
149
+
150
+ ##
151
+ # Starts a manual DDL batch. The batch must be ended by calling either run_batch or abort_batch.
152
+ #
153
+ # @example
154
+ # begin
155
+ # connection.start_batch_ddl
156
+ # connection.execute_ddl "CREATE TABLE `Users` (Id INT64, Name STRING(MAX)) PRIMARY KEY (Id)"
157
+ # connection.execute_ddl "CREATE INDEX Idx_Users_Name ON `Users` (Name)"
158
+ # connection.run_batch
159
+ # rescue StandardError
160
+ # connection.abort_batch
161
+ # raise
162
+ # end
163
+ def start_batch_ddl
164
+ if @ddl_batch
165
+ raise Google::Cloud::FailedPreconditionError, "A DDL batch is already active on this connection"
166
+ end
167
+ @ddl_batch = []
168
+ end
169
+
170
+ ##
171
+ # Aborts the current batch on this connection. This is a no-op if there is no batch on this connection.
172
+ #
173
+ # @see start_batch_ddl
174
+ def abort_batch
175
+ @ddl_batch = nil
176
+ end
177
+
178
+ ##
179
+ # Runs the current batch on this connection. This will raise a FailedPreconditionError if there is no
180
+ # active batch on this connection.
181
+ #
182
+ # @see start_batch_ddl
183
+ def run_batch
184
+ unless @ddl_batch
185
+ raise Google::Cloud::FailedPreconditionError, "There is no batch active on this connection"
186
+ end
187
+ # Just return if the batch is empty.
188
+ return true if @ddl_batch.empty?
189
+ begin
190
+ execute_ddl_statements @ddl_batch, nil, true
191
+ ensure
192
+ @ddl_batch = nil
193
+ end
194
+ end
195
+
196
+ # DQL, DML Statements
197
+
198
+ def execute_query sql, params: nil, types: nil, single_use_selector: nil
199
+ if params
200
+ converted_params, types = \
201
+ Google::Cloud::Spanner::Convert.to_input_params_and_types(
202
+ params, types
203
+ )
204
+ end
205
+
206
+ # Clear the transaction from the previous statement.
207
+ unless current_transaction&.active?
208
+ self.current_transaction = nil
209
+ end
210
+
211
+ selector = transaction_selector || single_use_selector
212
+ execute_sql_request sql, converted_params, types, selector
213
+ end
214
+
215
+ def execute_sql_request sql, converted_params, types, selector
216
+ res = session.execute_query \
217
+ sql,
218
+ params: converted_params,
219
+ types: types,
220
+ transaction: selector,
221
+ seqno: (current_transaction&.next_sequence_number)
222
+ current_transaction.grpc_transaction = res.metadata.transaction \
223
+ if current_transaction && res&.metadata&.transaction
224
+ res
225
+ rescue Google::Cloud::AbortedError
226
+ # Mark the current transaction as aborted to prevent any unnecessary further requests on the transaction.
227
+ current_transaction&.mark_aborted
228
+ raise
229
+ rescue Google::Cloud::NotFoundError => e
230
+ if session_not_found?(e) || transaction_not_found?(e)
231
+ reset!
232
+ # Force a retry of the entire transaction if this statement was executed as part of a transaction.
233
+ # Otherwise, just retry the statement itself.
234
+ raise_aborted_err if current_transaction&.active?
235
+ retry
236
+ end
237
+ raise
238
+ rescue Google::Cloud::Error => e
239
+ # Check if it was the first statement in a transaction that included a BeginTransaction
240
+ # option in the request. If so, execute an explicit BeginTransaction and then retry the
241
+ # request without the BeginTransaction option.
242
+ if current_transaction && selector&.begin&.read_write
243
+ selector = create_transaction_after_failed_first_statement e
244
+ retry
245
+ end
246
+ # It was not the first statement, so propagate the error.
247
+ raise
248
+ end
249
+
250
+ # Creates a transaction using a BeginTransaction RPC. This is used if the first statement of a
251
+ # transaction fails, as that also means that no transaction id was returned.
252
+ def create_transaction_after_failed_first_statement original_error
253
+ transaction = current_transaction.force_begin_read_write
254
+ Google::Spanner::V1::TransactionSelector.new id: transaction.transaction_id
255
+ rescue Google::Cloud::Error
256
+ # Raise the original error if the BeginTransaction RPC also fails.
257
+ raise original_error
258
+ end
259
+
260
+ # Transactions
261
+
262
+ def begin_transaction isolation = nil
263
+ raise "Nested transactions are not allowed" if current_transaction&.active?
264
+ self.current_transaction = Transaction.new self, isolation
265
+ current_transaction.begin
266
+ current_transaction
267
+ end
268
+
269
+ def commit_transaction
270
+ raise "This connection does not have a transaction" unless current_transaction
271
+ current_transaction.commit
272
+ end
273
+
274
+ def rollback_transaction
275
+ raise "This connection does not have a transaction" unless current_transaction
276
+ current_transaction.rollback
277
+ end
278
+
279
+ def transaction_selector
280
+ return current_transaction&.transaction_selector if current_transaction&.active?
281
+ end
282
+
283
+ def truncate table_name
284
+ session.delete table_name
285
+ end
286
+
287
+ def self.database_path config
288
+ "#{config[:emulator_host]}/#{config[:project]}/#{config[:instance]}/#{config[:database]}"
289
+ end
290
+
291
+ def session_not_found? err
292
+ if err.respond_to?(:metadata) && err.metadata["google.rpc.resourceinfo-bin"]
293
+ resource_info = Google::Rpc::ResourceInfo.decode err.metadata["google.rpc.resourceinfo-bin"]
294
+ type = resource_info["resource_type"]
295
+ return "type.googleapis.com/google.spanner.v1.Session".eql? type
296
+ end
297
+ false
298
+ end
299
+
300
+ def transaction_not_found? err
301
+ if err.respond_to?(:metadata) && err.metadata["google.rpc.resourceinfo-bin"]
302
+ resource_info = Google::Rpc::ResourceInfo.decode err.metadata["google.rpc.resourceinfo-bin"]
303
+ type = resource_info["resource_type"]
304
+ return "type.googleapis.com/google.spanner.v1.Transaction".eql? type
305
+ end
306
+ false
307
+ end
308
+
309
+ def raise_aborted_err
310
+ retry_info = Google::Rpc::RetryInfo.new retry_delay: Google::Protobuf::Duration.new(seconds: 0, nanos: 1)
311
+ begin
312
+ raise GRPC::BadStatus.new(
313
+ GRPC::Core::StatusCodes::ABORTED,
314
+ "Transaction aborted",
315
+ "google.rpc.retryinfo-bin": Google::Rpc::RetryInfo.encode(retry_info)
316
+ )
317
+ rescue GRPC::BadStatus
318
+ raise Google::Cloud::AbortedError
319
+ end
320
+ end
321
+
322
+ private
323
+
324
+ def execute_ddl_statements statements, operation_id, wait_until_done
325
+ job = database.update statements: statements, operation_id: operation_id
326
+ job.wait_until_done! if wait_until_done
327
+ raise Google::Cloud::Error.from_error job.error if job.error?
328
+ job.done?
329
+ end
330
+
331
+ ##
332
+ # Retrieves the delay value from Google::Cloud::AbortedError or
333
+ # GRPC::Aborted
334
+ def delay_from_aborted err
335
+ return nil if err.nil?
336
+ if err.respond_to?(:metadata) && err.metadata["google.rpc.retryinfo-bin"]
337
+ retry_info = Google::Rpc::RetryInfo.decode err.metadata["google.rpc.retryinfo-bin"]
338
+ seconds = retry_info["retry_delay"].seconds
339
+ nanos = retry_info["retry_delay"].nanos
340
+ return seconds if nanos.zero?
341
+ return seconds + (nanos / 1_000_000_000.0)
342
+ end
343
+ # No metadata? Try the inner error
344
+ delay_from_aborted err.cause
345
+ rescue StandardError
346
+ # Any error indicates the backoff should be handled elsewhere
347
+ nil
348
+ end
349
+ end
350
+ end
@@ -0,0 +1,13 @@
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
+ module ActiveRecordSpannerAdapter
8
+ class Error < StandardError
9
+ end
10
+
11
+ class NotSupportedError < Error
12
+ end
13
+ end
@@ -0,0 +1,29 @@
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
+ module ActiveRecordSpannerAdapter
8
+ class ForeignKey
9
+ attr_accessor :table_name, :name, :columns, :ref_table, :ref_columns,
10
+ :on_delete, :on_update
11
+
12
+ def initialize \
13
+ table_name,
14
+ name,
15
+ columns,
16
+ ref_table,
17
+ ref_columns,
18
+ on_delete: nil,
19
+ on_update: nil
20
+ @table_name = table_name
21
+ @name = name
22
+ @columns = Array(columns)
23
+ @ref_table = ref_table
24
+ @ref_columns = Array(ref_columns)
25
+ @on_delete = on_delete unless on_delete == "NO ACTION"
26
+ @on_update = on_update unless on_update == "NO ACTION"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,38 @@
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
+ module ActiveRecordSpannerAdapter
8
+ class Index
9
+ class Column
10
+ attr_accessor :table_name, :index_name, :name, :order, :ordinal_position
11
+
12
+ def initialize \
13
+ table_name,
14
+ index_name,
15
+ name,
16
+ order: nil,
17
+ ordinal_position: nil
18
+ @table_name = table_name.to_s
19
+ @index_name = index_name.to_s
20
+ @name = name.to_s
21
+ @order = order.to_s.upcase if order
22
+ @ordinal_position = ordinal_position
23
+ end
24
+
25
+ def storing?
26
+ @ordinal_position.nil?
27
+ end
28
+
29
+ def desc?
30
+ @order == "DESC"
31
+ end
32
+
33
+ def desc!
34
+ @order = "DESC"
35
+ end
36
+ end
37
+ end
38
+ end