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,171 @@
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
+
146
+ def test_statement_hint
147
+ post = Post.optimizer_hints("statement_hint: @{USE_ADDITIONAL_PARALLELISM=TRUE}")
148
+ .select(:title).to_a.first
149
+
150
+ assert_nil post.id
151
+ assert_equal "Title - 1", post.title
152
+ end
153
+
154
+ def test_table_hint
155
+ post = Post.optimizer_hints("table_hint: posts@{FORCE_INDEX=_BASE_TABLE}")
156
+ .select(:title).to_a.first
157
+
158
+ assert_nil post.id
159
+ assert_equal "Title - 1", post.title
160
+ end
161
+
162
+ def test_join_hint
163
+ post = Post.joins("inner join @{JOIN_TYPE=HASH_JOIN} comments on posts.id=comments.post_id")
164
+ .select(:title).to_a.first
165
+
166
+ assert_nil post.id
167
+ assert_equal "Title - 1", post.title
168
+ end
169
+ end
170
+ end
171
+ 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,130 @@
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_read_in_snapshot_at_timestamp
39
+ # Get a valid timestamp from the server to use for the transaction.
40
+ timestamp = ActiveRecord::Base.connection.select_all("SELECT CURRENT_TIMESTAMP AS ts")[0]["ts"]
41
+ Base.transaction isolation: { timestamp: timestamp } do
42
+ org = Organization.find @organization.id
43
+ assert_equal "Organization 1", org.name
44
+ end
45
+ end
46
+
47
+ def test_read_in_snapshot_with_staleness
48
+ Base.transaction isolation: { staleness: 1 } do
49
+ begin
50
+ # It could be that the record or even the table cannot be found, as the read timestamp could be
51
+ # before either of them were created, but the record could also be found, all depending on the execution
52
+ # speed of the test. All those scenarios are valid.
53
+ org = Organization.find @organization.id
54
+ assert_equal "Organization 1", org.name
55
+ rescue => e
56
+ assert e.message.include?("Table not found") || e.message.include?("Couldn't find Organization"), e.message
57
+ end
58
+ end
59
+ end
60
+
61
+ def test_single_read_at_timestamp
62
+ # Get a valid timestamp from the server to use for the transaction.
63
+ timestamp = ActiveRecord::Base.connection.select_all("SELECT CURRENT_TIMESTAMP AS ts")[0]["ts"]
64
+
65
+ org = Organization.optimizer_hints("read_timestamp:#{timestamp.xmlschema(9)}").find @organization.id
66
+ assert_equal "Organization 1", org.name
67
+ end
68
+
69
+ def test_single_read_at_min_read_timestamp
70
+ # Get a valid timestamp from the server to use for the transaction.
71
+ timestamp = ActiveRecord::Base.connection.select_all("SELECT CURRENT_TIMESTAMP AS ts")[0]["ts"]
72
+
73
+ org = Organization.optimizer_hints("min_read_timestamp:#{timestamp.xmlschema(9)}").find @organization.id
74
+ assert_equal "Organization 1", org.name
75
+ end
76
+
77
+ def test_single_read_with_max_staleness
78
+ begin
79
+ # It could be that the record or even the table cannot be found, as the read timestamp could be
80
+ # before either of them were created, but the record could also be found, all depending on the execution
81
+ # speed of the test. All those scenarios are valid.
82
+ org = Organization.optimizer_hints("max_staleness: 1").find @organization.id
83
+ assert_equal "Organization 1", org.name
84
+ rescue => e
85
+ assert e.message.include?("Table not found") || e.message.include?("Couldn't find Organization"), e.message
86
+ end
87
+ end
88
+
89
+ def test_single_read_with_exact_staleness
90
+ begin
91
+ # It could be that the record or even the table cannot be found, as the read timestamp could be
92
+ # before either of them were created, but the record could also be found, all depending on the execution
93
+ # speed of the test. All those scenarios are valid.
94
+ org = Organization.optimizer_hints("exact_staleness: 1").find @organization.id
95
+ assert_equal "Organization 1", org.name
96
+ rescue => e
97
+ assert e.message.include?("Table not found") || e.message.include?("Couldn't find Organization"), e.message
98
+ end
99
+ end
100
+
101
+ def test_snapshot_does_not_see_new_changes
102
+ Base.transaction isolation: :read_only do
103
+ org = Organization.find @organization.id
104
+ assert_equal "Organization 1", org.name
105
+
106
+ # Update the name of the organization using a separate thread (and separate transaction).
107
+ t = Thread.new { organization.update(name: "New name") }
108
+ t.join
109
+
110
+ # Reload the record using the current snapshot. The change will not be visible.
111
+ org.reload
112
+ assert_equal "Organization 1", org.name
113
+ end
114
+
115
+ # Now read the record outside of the snapshot. The new value should be visible.
116
+ org = Organization.find @organization.id
117
+ assert_equal "New name", org.name
118
+ end
119
+
120
+ def test_write_in_snapshot
121
+ Base.transaction isolation: :read_only do
122
+ err = assert_raises ActiveRecord::StatementInvalid do
123
+ Organization.create(name: "Created in read-only transaction")
124
+ end
125
+ assert err.cause.is_a?(Google::Cloud::InvalidArgumentError)
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end