activerecord-spanner-adapter 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. checksums.yaml +5 -5
  2. data/.github/CODEOWNERS +7 -0
  3. data/.github/sync-repo-settings.yaml +16 -0
  4. data/.github/workflows/acceptance-tests-on-emulator.yaml +45 -0
  5. data/.github/workflows/acceptance-tests-on-production.yaml +36 -0
  6. data/.github/workflows/ci.yaml +33 -0
  7. data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +52 -0
  8. data/.github/workflows/nightly-acceptance-tests-on-production.yaml +35 -0
  9. data/.github/workflows/nightly-unit-tests.yaml +40 -0
  10. data/.github/workflows/release-please-label.yml +25 -0
  11. data/.github/workflows/release-please.yml +39 -0
  12. data/.github/workflows/rubocop.yaml +31 -0
  13. data/.gitignore +67 -5
  14. data/.kokoro/populate-secrets.sh +77 -0
  15. data/.kokoro/release.cfg +33 -0
  16. data/.kokoro/release.sh +15 -0
  17. data/.kokoro/trampoline_v2.sh +489 -0
  18. data/.rubocop.yml +46 -0
  19. data/.toys/release.rb +18 -0
  20. data/.trampolinerc +48 -0
  21. data/.yardopts +11 -0
  22. data/CHANGELOG.md +26 -0
  23. data/CODE_OF_CONDUCT.md +40 -0
  24. data/CONTRIBUTING.md +79 -0
  25. data/Gemfile +9 -4
  26. data/LICENSE +6 -6
  27. data/README.md +67 -30
  28. data/Rakefile +79 -3
  29. data/SECURITY.md +7 -0
  30. data/acceptance/cases/associations/has_many_associations_test.rb +119 -0
  31. data/acceptance/cases/associations/has_many_through_associations_test.rb +63 -0
  32. data/acceptance/cases/associations/has_one_associations_test.rb +79 -0
  33. data/acceptance/cases/associations/has_one_through_associations_test.rb +98 -0
  34. data/acceptance/cases/interleaved_associations/has_many_associations_using_interleaved_test.rb +211 -0
  35. data/acceptance/cases/migration/change_schema_test.rb +433 -0
  36. data/acceptance/cases/migration/change_table_test.rb +115 -0
  37. data/acceptance/cases/migration/column_attributes_test.rb +122 -0
  38. data/acceptance/cases/migration/column_positioning_test.rb +48 -0
  39. data/acceptance/cases/migration/columns_test.rb +201 -0
  40. data/acceptance/cases/migration/command_recorder_test.rb +406 -0
  41. data/acceptance/cases/migration/create_join_table_test.rb +216 -0
  42. data/acceptance/cases/migration/ddl_batching_test.rb +80 -0
  43. data/acceptance/cases/migration/foreign_key_test.rb +297 -0
  44. data/acceptance/cases/migration/index_test.rb +211 -0
  45. data/acceptance/cases/migration/references_foreign_key_test.rb +259 -0
  46. data/acceptance/cases/migration/references_index_test.rb +135 -0
  47. data/acceptance/cases/migration/references_statements_test.rb +166 -0
  48. data/acceptance/cases/migration/rename_column_test.rb +96 -0
  49. data/acceptance/cases/models/calculation_query_test.rb +128 -0
  50. data/acceptance/cases/models/generated_column_test.rb +126 -0
  51. data/acceptance/cases/models/mutation_test.rb +122 -0
  52. data/acceptance/cases/models/query_test.rb +147 -0
  53. data/acceptance/cases/sessions/session_not_found_test.rb +121 -0
  54. data/acceptance/cases/transactions/optimistic_locking_test.rb +141 -0
  55. data/acceptance/cases/transactions/read_only_transactions_test.rb +67 -0
  56. data/acceptance/cases/transactions/read_write_transactions_test.rb +248 -0
  57. data/acceptance/cases/type/all_types_test.rb +152 -0
  58. data/acceptance/cases/type/binary_test.rb +59 -0
  59. data/acceptance/cases/type/boolean_test.rb +31 -0
  60. data/acceptance/cases/type/date_test.rb +32 -0
  61. data/acceptance/cases/type/date_time_test.rb +30 -0
  62. data/acceptance/cases/type/float_test.rb +27 -0
  63. data/acceptance/cases/type/integer_test.rb +44 -0
  64. data/acceptance/cases/type/numeric_test.rb +27 -0
  65. data/acceptance/cases/type/string_test.rb +79 -0
  66. data/acceptance/cases/type/text_test.rb +30 -0
  67. data/acceptance/cases/type/time_test.rb +87 -0
  68. data/acceptance/models/account.rb +13 -0
  69. data/acceptance/models/address.rb +9 -0
  70. data/acceptance/models/album.rb +12 -0
  71. data/acceptance/models/all_types.rb +8 -0
  72. data/acceptance/models/author.rb +11 -0
  73. data/acceptance/models/club.rb +12 -0
  74. data/acceptance/models/comment.rb +9 -0
  75. data/acceptance/models/customer.rb +9 -0
  76. data/acceptance/models/department.rb +9 -0
  77. data/acceptance/models/firm.rb +10 -0
  78. data/acceptance/models/member.rb +13 -0
  79. data/acceptance/models/member_type.rb +9 -0
  80. data/acceptance/models/membership.rb +10 -0
  81. data/acceptance/models/organization.rb +9 -0
  82. data/acceptance/models/post.rb +10 -0
  83. data/acceptance/models/singer.rb +10 -0
  84. data/acceptance/models/track.rb +20 -0
  85. data/acceptance/models/transaction.rb +9 -0
  86. data/acceptance/schema/schema.rb +143 -0
  87. data/acceptance/test_helper.rb +260 -0
  88. data/activerecord-spanner-adapter.gemspec +32 -17
  89. data/assets/solidus-db.png +0 -0
  90. data/benchmarks/README.md +17 -0
  91. data/benchmarks/Rakefile +14 -0
  92. data/benchmarks/application.rb +308 -0
  93. data/benchmarks/config/database.yml +8 -0
  94. data/benchmarks/config/environment.rb +12 -0
  95. data/benchmarks/db/migrate/01_create_tables.rb +25 -0
  96. data/benchmarks/db/schema.rb +29 -0
  97. data/benchmarks/models/album.rb +9 -0
  98. data/benchmarks/models/singer.rb +9 -0
  99. data/bin/console +6 -7
  100. data/examples/rails/README.md +262 -0
  101. data/examples/snippets/README.md +29 -0
  102. data/examples/snippets/Rakefile +57 -0
  103. data/examples/snippets/array-data-type/README.md +45 -0
  104. data/examples/snippets/array-data-type/Rakefile +13 -0
  105. data/examples/snippets/array-data-type/application.rb +45 -0
  106. data/examples/snippets/array-data-type/config/database.yml +8 -0
  107. data/examples/snippets/array-data-type/db/migrate/01_create_tables.rb +24 -0
  108. data/examples/snippets/array-data-type/db/schema.rb +26 -0
  109. data/examples/snippets/array-data-type/db/seeds.rb +5 -0
  110. data/examples/snippets/array-data-type/models/entity_with_array_types.rb +18 -0
  111. data/examples/snippets/bin/create_emulator_instance.rb +18 -0
  112. data/examples/snippets/bulk-insert/README.md +21 -0
  113. data/examples/snippets/bulk-insert/Rakefile +13 -0
  114. data/examples/snippets/bulk-insert/application.rb +64 -0
  115. data/examples/snippets/bulk-insert/config/database.yml +8 -0
  116. data/examples/snippets/bulk-insert/db/migrate/01_create_tables.rb +21 -0
  117. data/examples/snippets/bulk-insert/db/schema.rb +26 -0
  118. data/examples/snippets/bulk-insert/db/seeds.rb +5 -0
  119. data/examples/snippets/bulk-insert/models/album.rb +9 -0
  120. data/examples/snippets/bulk-insert/models/singer.rb +9 -0
  121. data/examples/snippets/commit-timestamp/README.md +18 -0
  122. data/examples/snippets/commit-timestamp/Rakefile +13 -0
  123. data/examples/snippets/commit-timestamp/application.rb +53 -0
  124. data/examples/snippets/commit-timestamp/config/database.yml +8 -0
  125. data/examples/snippets/commit-timestamp/db/migrate/01_create_tables.rb +26 -0
  126. data/examples/snippets/commit-timestamp/db/schema.rb +29 -0
  127. data/examples/snippets/commit-timestamp/db/seeds.rb +5 -0
  128. data/examples/snippets/commit-timestamp/models/album.rb +9 -0
  129. data/examples/snippets/commit-timestamp/models/singer.rb +9 -0
  130. data/examples/snippets/config/environment.rb +21 -0
  131. data/examples/snippets/create-records/README.md +12 -0
  132. data/examples/snippets/create-records/Rakefile +13 -0
  133. data/examples/snippets/create-records/application.rb +42 -0
  134. data/examples/snippets/create-records/config/database.yml +8 -0
  135. data/examples/snippets/create-records/db/migrate/01_create_tables.rb +21 -0
  136. data/examples/snippets/create-records/db/schema.rb +26 -0
  137. data/examples/snippets/create-records/db/seeds.rb +5 -0
  138. data/examples/snippets/create-records/models/album.rb +9 -0
  139. data/examples/snippets/create-records/models/singer.rb +9 -0
  140. data/examples/snippets/date-data-type/README.md +19 -0
  141. data/examples/snippets/date-data-type/Rakefile +13 -0
  142. data/examples/snippets/date-data-type/application.rb +35 -0
  143. data/examples/snippets/date-data-type/config/database.yml +8 -0
  144. data/examples/snippets/date-data-type/db/migrate/01_create_tables.rb +20 -0
  145. data/examples/snippets/date-data-type/db/schema.rb +21 -0
  146. data/examples/snippets/date-data-type/db/seeds.rb +16 -0
  147. data/examples/snippets/date-data-type/models/singer.rb +8 -0
  148. data/examples/snippets/generated-column/README.md +41 -0
  149. data/examples/snippets/generated-column/Rakefile +13 -0
  150. data/examples/snippets/generated-column/application.rb +37 -0
  151. data/examples/snippets/generated-column/config/database.yml +8 -0
  152. data/examples/snippets/generated-column/db/migrate/01_create_tables.rb +23 -0
  153. data/examples/snippets/generated-column/db/schema.rb +21 -0
  154. data/examples/snippets/generated-column/db/seeds.rb +18 -0
  155. data/examples/snippets/generated-column/models/singer.rb +8 -0
  156. data/examples/snippets/interleaved-tables/README.md +152 -0
  157. data/examples/snippets/interleaved-tables/Rakefile +13 -0
  158. data/examples/snippets/interleaved-tables/application.rb +109 -0
  159. data/examples/snippets/interleaved-tables/config/database.yml +8 -0
  160. data/examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb +44 -0
  161. data/examples/snippets/interleaved-tables/db/schema.rb +32 -0
  162. data/examples/snippets/interleaved-tables/db/seeds.rb +40 -0
  163. data/examples/snippets/interleaved-tables/models/album.rb +15 -0
  164. data/examples/snippets/interleaved-tables/models/singer.rb +20 -0
  165. data/examples/snippets/interleaved-tables/models/track.rb +25 -0
  166. data/examples/snippets/migrations/README.md +43 -0
  167. data/examples/snippets/migrations/Rakefile +13 -0
  168. data/examples/snippets/migrations/application.rb +26 -0
  169. data/examples/snippets/migrations/config/database.yml +8 -0
  170. data/examples/snippets/migrations/db/migrate/01_create_tables.rb +28 -0
  171. data/examples/snippets/migrations/db/schema.rb +33 -0
  172. data/examples/snippets/migrations/db/seeds.rb +5 -0
  173. data/examples/snippets/migrations/models/album.rb +10 -0
  174. data/examples/snippets/migrations/models/singer.rb +10 -0
  175. data/examples/snippets/migrations/models/track.rb +9 -0
  176. data/examples/snippets/mutations/README.md +34 -0
  177. data/examples/snippets/mutations/Rakefile +13 -0
  178. data/examples/snippets/mutations/application.rb +47 -0
  179. data/examples/snippets/mutations/config/database.yml +8 -0
  180. data/examples/snippets/mutations/db/migrate/01_create_tables.rb +22 -0
  181. data/examples/snippets/mutations/db/schema.rb +27 -0
  182. data/examples/snippets/mutations/db/seeds.rb +25 -0
  183. data/examples/snippets/mutations/models/album.rb +9 -0
  184. data/examples/snippets/mutations/models/singer.rb +9 -0
  185. data/examples/snippets/optimistic-locking/README.md +12 -0
  186. data/examples/snippets/optimistic-locking/Rakefile +13 -0
  187. data/examples/snippets/optimistic-locking/application.rb +48 -0
  188. data/examples/snippets/optimistic-locking/config/database.yml +8 -0
  189. data/examples/snippets/optimistic-locking/db/migrate/01_create_tables.rb +26 -0
  190. data/examples/snippets/optimistic-locking/db/schema.rb +29 -0
  191. data/examples/snippets/optimistic-locking/db/seeds.rb +25 -0
  192. data/examples/snippets/optimistic-locking/models/album.rb +9 -0
  193. data/examples/snippets/optimistic-locking/models/singer.rb +9 -0
  194. data/examples/snippets/quickstart/README.md +26 -0
  195. data/examples/snippets/quickstart/Rakefile +13 -0
  196. data/examples/snippets/quickstart/application.rb +51 -0
  197. data/examples/snippets/quickstart/config/database.yml +8 -0
  198. data/examples/snippets/quickstart/db/migrate/01_create_tables.rb +21 -0
  199. data/examples/snippets/quickstart/db/schema.rb +26 -0
  200. data/examples/snippets/quickstart/db/seeds.rb +24 -0
  201. data/examples/snippets/quickstart/models/album.rb +9 -0
  202. data/examples/snippets/quickstart/models/singer.rb +9 -0
  203. data/examples/snippets/read-only-transactions/README.md +13 -0
  204. data/examples/snippets/read-only-transactions/Rakefile +13 -0
  205. data/examples/snippets/read-only-transactions/application.rb +49 -0
  206. data/examples/snippets/read-only-transactions/config/database.yml +8 -0
  207. data/examples/snippets/read-only-transactions/db/migrate/01_create_tables.rb +21 -0
  208. data/examples/snippets/read-only-transactions/db/schema.rb +26 -0
  209. data/examples/snippets/read-only-transactions/db/seeds.rb +24 -0
  210. data/examples/snippets/read-only-transactions/models/album.rb +9 -0
  211. data/examples/snippets/read-only-transactions/models/singer.rb +9 -0
  212. data/examples/snippets/read-write-transactions/README.md +12 -0
  213. data/examples/snippets/read-write-transactions/Rakefile +13 -0
  214. data/examples/snippets/read-write-transactions/application.rb +39 -0
  215. data/examples/snippets/read-write-transactions/config/database.yml +8 -0
  216. data/examples/snippets/read-write-transactions/db/migrate/01_create_tables.rb +22 -0
  217. data/examples/snippets/read-write-transactions/db/schema.rb +27 -0
  218. data/examples/snippets/read-write-transactions/db/seeds.rb +25 -0
  219. data/examples/snippets/read-write-transactions/models/album.rb +9 -0
  220. data/examples/snippets/read-write-transactions/models/singer.rb +9 -0
  221. data/examples/snippets/timestamp-data-type/README.md +17 -0
  222. data/examples/snippets/timestamp-data-type/Rakefile +13 -0
  223. data/examples/snippets/timestamp-data-type/application.rb +42 -0
  224. data/examples/snippets/timestamp-data-type/config/database.yml +8 -0
  225. data/examples/snippets/timestamp-data-type/db/migrate/01_create_tables.rb +21 -0
  226. data/examples/snippets/timestamp-data-type/db/schema.rb +21 -0
  227. data/examples/snippets/timestamp-data-type/db/seeds.rb +6 -0
  228. data/examples/snippets/timestamp-data-type/models/meeting.rb +19 -0
  229. data/examples/solidus/README.md +172 -0
  230. data/lib/active_record/connection_adapters/spanner/database_statements.rb +224 -269
  231. data/lib/active_record/connection_adapters/spanner/quoting.rb +42 -50
  232. data/lib/active_record/connection_adapters/spanner/schema_cache.rb +43 -0
  233. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +125 -9
  234. data/lib/active_record/connection_adapters/spanner/schema_definitions.rb +122 -0
  235. data/lib/active_record/connection_adapters/spanner/schema_dumper.rb +19 -0
  236. data/lib/active_record/connection_adapters/spanner/schema_statements.rb +553 -139
  237. data/lib/active_record/connection_adapters/spanner/type_metadata.rb +37 -0
  238. data/lib/active_record/connection_adapters/spanner_adapter.rb +182 -78
  239. data/lib/active_record/tasks/spanner_database_tasks.rb +74 -0
  240. data/lib/active_record/type/spanner/array.rb +32 -0
  241. data/lib/active_record/type/spanner/bytes.rb +26 -0
  242. data/lib/active_record/type/spanner/spanner_active_record_converter.rb +32 -0
  243. data/lib/active_record/type/spanner/time.rb +37 -0
  244. data/lib/activerecord-spanner-adapter.rb +23 -0
  245. data/lib/activerecord_spanner_adapter/base.rb +217 -0
  246. data/lib/activerecord_spanner_adapter/connection.rb +324 -0
  247. data/lib/activerecord_spanner_adapter/errors.rb +13 -0
  248. data/lib/activerecord_spanner_adapter/foreign_key.rb +29 -0
  249. data/lib/activerecord_spanner_adapter/index/column.rb +38 -0
  250. data/lib/activerecord_spanner_adapter/index.rb +80 -0
  251. data/lib/activerecord_spanner_adapter/information_schema.rb +261 -0
  252. data/lib/activerecord_spanner_adapter/primary_key.rb +31 -0
  253. data/lib/activerecord_spanner_adapter/table/column.rb +59 -0
  254. data/lib/activerecord_spanner_adapter/table.rb +61 -0
  255. data/lib/activerecord_spanner_adapter/transaction.rb +113 -0
  256. data/lib/activerecord_spanner_adapter/version.rb +9 -0
  257. data/lib/arel/visitors/spanner.rb +35 -0
  258. data/lib/spanner_client_ext.rb +82 -0
  259. data/renovate.json +5 -0
  260. metadata +387 -34
  261. data/.travis.yml +0 -5
  262. data/lib/active_record/connection_adapters/spanner/client.rb +0 -190
  263. data/lib/active_record/connection_adapters/spanner.rb +0 -10
  264. data/lib/activerecord-spanner-adapter/version.rb +0 -3
@@ -0,0 +1,147 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ # frozen_string_literal: true
8
+
9
+ require "test_helper"
10
+ require "models/author"
11
+ require "models/post"
12
+ require "models/comment"
13
+ require "models/address"
14
+
15
+ module ActiveRecord
16
+ module Model
17
+ class QueryTest < SpannerAdapter::TestCase
18
+ include SpannerAdapter::Associations::TestHelper
19
+
20
+ attr_accessor :author, :post, :comment
21
+
22
+ def setup
23
+ super
24
+
25
+ @author = Author.create name: "David"
26
+ @post = Post.create title: "Title - 1", author: author
27
+ @comment = Comment.create comment: "Comment - 1", post: post
28
+ end
29
+
30
+ def teardown
31
+ super
32
+ Comment.destroy_all
33
+ Post.destroy_all
34
+ Author.destroy_all
35
+ end
36
+
37
+ def test_in_clause_is_correctly_sliced
38
+ author2 = Author.create name: "John"
39
+
40
+ assert_equal 2, Author.count
41
+ assert_equal [author], Author.where(name: "David", id: [author.id, author2.id])
42
+ end
43
+
44
+ def test_type_casting_nested_joins
45
+ assert_equal [comment], Comment.joins(post: :author).where(authors: { id: author.id })
46
+ end
47
+
48
+ def test_where_copies_bind_params
49
+ posts = author.posts.where("posts.id = #{post.id}")
50
+ joined = Post.where(id: posts)
51
+
52
+ assert_operator joined.length, :>, 0
53
+
54
+ joined.each { |j_post|
55
+ assert_equal author, j_post.author
56
+ assert_equal post.id, j_post.id
57
+ }
58
+ end
59
+
60
+ def test_where_or_with_relation
61
+ post2 = Post.create title: "Title - 2", author: author
62
+ expected = Post.where("id = #{post.id} or id = #{post2.id}").to_a
63
+ assert_equal expected, Post.where("id = #{post.id}").or(Post.where("id = #{post2.id}")).to_a
64
+ end
65
+
66
+ def test_joins_and_preload
67
+ assert_nothing_raised do
68
+ Post.includes(:author).or(Post.includes(:author))
69
+ Post.eager_load(:author).or(Post.eager_load(:author))
70
+ Post.preload(:author).or(Post.preload(:author))
71
+ Post.group(:author_id).or(Post.group(:author_id))
72
+ Post.joins(:author).or(Post.joins(:author))
73
+ Post.left_outer_joins(:author).or(Post.left_outer_joins(:author))
74
+ Post.from("posts")
75
+ end
76
+ end
77
+
78
+ def test_not_inverts_where_clause
79
+ relation = Post.where.not(title: "hello")
80
+ expected_where_clause = Post.where(title: "hello").where_clause.invert
81
+
82
+ assert_equal expected_where_clause, relation.where_clause
83
+ end
84
+
85
+ def test_range
86
+ post2 = Post.create title: "Title - 1", author: author
87
+ comment2 = Comment.create comment: "Comment - 2", post: post2
88
+
89
+ assert_equal 2, Post.where(comments_count: 1..3).count
90
+ end
91
+
92
+ def test_with_infinite_upper_bound_range
93
+ assert_equal 1, Post.where(comments_count: 1..Float::INFINITY).count
94
+ end
95
+
96
+ def test_offset_and_limit
97
+ post = Post.create title: "Title - 1", author: author
98
+ 5.times.each do |i|
99
+ Comment.create comment: "Comment - #{i+1}", post: post
100
+ end
101
+
102
+ assert_equal 5, post.comments.count
103
+ assert_equal 3, Comment.offset(1).limit(3).count
104
+ end
105
+
106
+ def test_select
107
+ post = Post.select(:title).to_a.first
108
+
109
+ assert_nil post.id
110
+ assert_equal "Title - 1", post.title
111
+ end
112
+
113
+ def test_order_and_pluck
114
+ post = Post.create title: "Title - 2", author: author
115
+ titles = Post.order("title").pluck("posts.title")
116
+
117
+ assert_equal ["Title - 1", "Title - 2"], titles
118
+ end
119
+
120
+ def test_time_value
121
+ time_value = Time.new(2016, 05, 11, 19, 0, 0)
122
+ post = Post.create(published_time: time_value)
123
+ assert_equal post, Post.find_by(published_time: time_value)
124
+ end
125
+
126
+ def test_timestamp_value
127
+ timestamp_value = Time.now
128
+ post = Post.create(published_time: timestamp_value)
129
+ assert_equal post, Post.find_by(published_time: timestamp_value)
130
+ end
131
+
132
+ def test_date_value
133
+ date = Date.new(2016, 05, 11)
134
+ post = Post.create(post_date: date)
135
+ assert_equal post, Post.find_by(post_date: date)
136
+ end
137
+
138
+ def test_relation_merging
139
+ post.comments << Comment.new(comment: "Comment - 2")
140
+
141
+ posts = Post.where("comments_count >= 0").merge(Post.limit(2)).merge(Post.order("id ASC"))
142
+
143
+ assert_equal [post], posts.to_a
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,121 @@
1
+ # Copyright 2021 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ # frozen_string_literal: true
8
+
9
+ require "test_helper"
10
+ require "models/author"
11
+ require "models/post"
12
+ require "models/comment"
13
+ require "models/organization"
14
+
15
+ module ActiveRecord
16
+ module Sessions
17
+
18
+ # Verifies that the adapter can handle a Session not found error in all (common) scenarios.
19
+ class SessionNotFoundTest < SpannerAdapter::TestCase
20
+ include SpannerAdapter::Associations::TestHelper
21
+
22
+ attr_accessor :organization
23
+
24
+ def setup
25
+ super
26
+
27
+ @organization = Organization.create name: "Organization 1"
28
+ end
29
+
30
+ def teardown
31
+ super
32
+
33
+ Organization.destroy_all
34
+ end
35
+
36
+ def client
37
+ @client ||= Google::Cloud::Spanner::V1::Spanner::Client.new do |config|
38
+ config.credentials = ENV["SPANNER_EMULATOR_HOST"] \
39
+ ? :this_channel_is_insecure \
40
+ : ENV["SPANNER_TEST_KEYFILE"] || ENV["GCLOUD_TEST_KEYFILE"]
41
+ config.endpoint = ENV["SPANNER_EMULATOR_HOST"] if ENV["SPANNER_EMULATOR_HOST"]
42
+ end
43
+ end
44
+
45
+ def delete_all_sessions
46
+ sessions = client.list_sessions(
47
+ Google::Cloud::Spanner::V1::ListSessionsRequest.new(
48
+ database: "projects/#{ENV["SPANNER_TEST_PROJECT"]}/instances/#{ENV["SPANNER_TEST_INSTANCE"]}/databases/#{$spanner_test_database}"
49
+ )
50
+ )
51
+ sessions.each do |session|
52
+ client.delete_session Google::Cloud::Spanner::V1::DeleteSessionRequest.new name: session.name
53
+ end
54
+ end
55
+
56
+ def test_single_read
57
+ delete_all_sessions
58
+ organization = Organization.find_by id: @organization.id
59
+ refute_nil organization
60
+ end
61
+
62
+ def test_single_mutation
63
+ delete_all_sessions
64
+ id = Organization.create name: "Organization 2"
65
+ assert_equal "Organization 2", Organization.find_by(id: id).name
66
+ end
67
+
68
+ def test_batch_mutation
69
+ delete_all_sessions
70
+ Organization.create([{name: "Organization 2"}, {name: "Organization 3"}])
71
+ assert_equal 3, Organization.count
72
+ end
73
+
74
+ def test_begin_transaction
75
+ delete_all_sessions
76
+ Organization.transaction do
77
+ organization = Organization.find_by id: @organization.id
78
+ refute_nil organization
79
+ end
80
+ end
81
+
82
+ def test_read_in_transaction
83
+ attempts = 0
84
+ Organization.transaction do
85
+ attempts += 1
86
+ delete_all_sessions if attempts == 1
87
+ organization = Organization.find_by id: @organization.id
88
+ refute_nil organization
89
+ end
90
+ # The transaction could also be aborted by the backend, hence the > 1.
91
+ assert attempts > 1, "Should retry at least once"
92
+ end
93
+
94
+ def test_dml_in_transaction
95
+ id = nil
96
+ attempts = 0
97
+ Organization.transaction do
98
+ attempts += 1
99
+ delete_all_sessions if attempts == 1
100
+ id = Organization.create name: "Organization 2"
101
+ end
102
+ assert attempts > 1, "Should retry at least once"
103
+ assert_equal "Organization 2", Organization.find_by(id: id).name
104
+ end
105
+
106
+ def test_commit
107
+ attempts = 0
108
+ Organization.transaction do
109
+ Organization.find_by id: @organization.id
110
+ attempts += 1
111
+ # The following is a trick for the emulator only. If a session on the emulator has an active transaction,
112
+ # and that session is deleted, the emulator still thinks that the transaction is active.
113
+ # See https://github.com/GoogleCloudPlatform/cloud-spanner-emulator/issues/30
114
+ Base.connection.current_spanner_transaction.shoot_and_forget_rollback if ENV["SPANNER_EMULATOR_HOST"] && attempts == 1
115
+ delete_all_sessions if attempts == 1
116
+ end
117
+ assert attempts > 1, "Should retry at least once"
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,141 @@
1
+ # Copyright 2021 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ # frozen_string_literal: true
8
+
9
+ require "test_helper"
10
+ require "models/singer"
11
+ require "models/album"
12
+ require "models/track"
13
+
14
+ module ActiveRecord
15
+ module Transactions
16
+ class OptimisticLockingTest < SpannerAdapter::TestCase
17
+ include SpannerAdapter::Associations::TestHelper
18
+
19
+ def setup
20
+ super
21
+
22
+ singer = Singer.create first_name: "Pete", last_name: "Allison"
23
+ album = Album.create title: "Musical Jeans", singer: singer
24
+ Track.create title: "Increased Headline", album: album, singer: singer
25
+ end
26
+
27
+ def teardown
28
+ super
29
+
30
+ Track.delete_all
31
+ Album.delete_all
32
+ Singer.delete_all
33
+ end
34
+
35
+ # Runs the given block in a transaction with the given isolation level, or without a transaction if isolation is
36
+ # nil.
37
+ def run_in_transaction isolation
38
+ if isolation
39
+ Base.transaction isolation: isolation do
40
+ yield
41
+ end
42
+ else
43
+ yield
44
+ end
45
+ end
46
+
47
+ def test_update_single_record_increases_version_number
48
+ [nil, :serializable, :buffered_mutations].each do |isolation|
49
+ singer = Singer.all.sample
50
+ original_version = singer.lock_version
51
+
52
+ run_in_transaction isolation do
53
+ singer.update last_name: "Peterson-#{singer.last_name}"
54
+ end
55
+
56
+ singer.reload
57
+ assert_equal original_version + 1, singer.lock_version
58
+ end
59
+ end
60
+
61
+ def test_update_multiple_records_increases_version_numbers
62
+ singer = Singer.all.sample
63
+ album = Album.all.sample
64
+ track = Track.all.sample
65
+ [nil, :serializable, :buffered_mutations].each do |isolation|
66
+ original_singer_version = singer.reload.lock_version
67
+ original_album_version = album.reload.lock_version
68
+ original_track_version = track.reload.lock_version
69
+
70
+ run_in_transaction isolation do
71
+ singer.update last_name: "Peterson-#{singer.last_name}"
72
+ singer.albums.each { |album| album.update title: "Updated: #{album.title}" }
73
+ singer.tracks.each { |track| track.update title: "Updated: #{track.title}" }
74
+ end
75
+
76
+ singer.reload
77
+ assert_equal original_singer_version + 1, singer.lock_version
78
+ singer.albums.each { |album| assert_equal original_album_version + 1, album.lock_version }
79
+ singer.tracks.each { |track| assert_equal original_track_version + 1, track.lock_version }
80
+ end
81
+ end
82
+
83
+ def test_concurrent_update_single_record_fails
84
+ [nil, :serializable, :buffered_mutations].each do |isolation|
85
+ singer = Singer.all.sample
86
+ original_version = singer.lock_version
87
+
88
+ # Update the singer in a separate thread to simulate a concurrent update.
89
+ t = Thread.new do
90
+ singer2 = Singer.find singer.id
91
+ singer2.update last_name: "Henderson-#{singer2.last_name}"
92
+ end
93
+ t.join
94
+
95
+ run_in_transaction isolation do
96
+ assert_raises ActiveRecord::StaleObjectError do
97
+ singer.update last_name: "Peterson-#{singer.last_name}"
98
+ end
99
+ end
100
+
101
+ singer.reload
102
+ assert_equal original_version + 1, singer.lock_version
103
+ assert singer.last_name.start_with?("Henderson-")
104
+ end
105
+ end
106
+
107
+ def test_concurrent_update_multiple_records_fails
108
+ singer = Singer.all.sample
109
+ album = Album.all.sample
110
+ track = Track.all.sample
111
+ [nil, :serializable, :buffered_mutations].each do |isolation|
112
+ original_singer_version = singer.reload.lock_version
113
+ original_album_version = album.reload.lock_version
114
+ original_track_version = track.reload.lock_version
115
+
116
+ # Update the singer in a separate thread to simulate a concurrent update.
117
+ t = Thread.new do
118
+ singer2 = Singer.find singer.id
119
+ singer2.update last_name: "Henderson-#{singer2.last_name}"
120
+ end
121
+ t.join
122
+
123
+ run_in_transaction isolation do
124
+ assert_raises ActiveRecord::StaleObjectError do
125
+ singer.update last_name: "Peterson-#{singer.last_name}"
126
+ end
127
+ singer.albums.each { |album| album.update title: "Updated: #{album.title}" }
128
+ singer.tracks.each { |track| track.update title: "Updated: #{track.title}" }
129
+ end
130
+
131
+ singer.reload
132
+ # The singer should be updated, but only by the separate thread.
133
+ assert_equal original_singer_version + 1, singer.lock_version
134
+ assert singer.last_name.start_with? "Henderson-"
135
+ singer.albums.each { |album| assert_equal original_album_version + 1, album.lock_version }
136
+ singer.tracks.each { |track| assert_equal original_track_version + 1, track.lock_version }
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,67 @@
1
+ # Copyright 2021 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ # frozen_string_literal: true
8
+
9
+ require "test_helper"
10
+ require "models/organization"
11
+
12
+ module ActiveRecord
13
+ module Transactions
14
+ class ReadOnlyTransactionsTest < SpannerAdapter::TestCase
15
+ include SpannerAdapter::Associations::TestHelper
16
+
17
+ attr_accessor :organization
18
+
19
+ def setup
20
+ super
21
+
22
+ @organization = Organization.create name: "Organization 1"
23
+ end
24
+
25
+ def teardown
26
+ super
27
+
28
+ Organization.destroy_all
29
+ end
30
+
31
+ def test_read_in_snapshot
32
+ Base.transaction isolation: :read_only do
33
+ org = Organization.find @organization.id
34
+ assert_equal "Organization 1", org.name
35
+ end
36
+ end
37
+
38
+ def test_snapshot_does_not_see_new_changes
39
+ Base.transaction isolation: :read_only do
40
+ org = Organization.find @organization.id
41
+ assert_equal "Organization 1", org.name
42
+
43
+ # Update the name of the organization using a separate thread (and separate transaction).
44
+ t = Thread.new { organization.update(name: "New name") }
45
+ t.join
46
+
47
+ # Reload the record using the current snapshot. The change will not be visible.
48
+ org.reload
49
+ assert_equal "Organization 1", org.name
50
+ end
51
+
52
+ # Now read the record outside of the snapshot. The new value should be visible.
53
+ org = Organization.find @organization.id
54
+ assert_equal "New name", org.name
55
+ end
56
+
57
+ def test_write_in_snapshot
58
+ Base.transaction isolation: :read_only do
59
+ err = assert_raises ActiveRecord::StatementInvalid do
60
+ Organization.create(name: "Created in read-only transaction")
61
+ end
62
+ assert err.cause.is_a?(Google::Cloud::InvalidArgumentError)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end