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,261 @@
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
+ gem "minitest"
8
+ require "minitest/autorun"
9
+ require "minitest/focus"
10
+ require "minitest/rg"
11
+ require "active_support"
12
+ require "google/cloud/spanner"
13
+ require "active_record"
14
+ require "active_support/testing/stream"
15
+ require "activerecord-spanner-adapter"
16
+ require "active_record/connection_adapters/spanner_adapter"
17
+ require "securerandom"
18
+
19
+ # rubocop:disable Style/GlobalVars
20
+
21
+ $spanner_test_database = "ar-test-#{SecureRandom.hex 4}"
22
+
23
+ def connector_config
24
+ {
25
+ "adapter" => "spanner",
26
+ "emulator_host" => ENV["SPANNER_EMULATOR_HOST"],
27
+ "project" => ENV["SPANNER_TEST_PROJECT"],
28
+ "instance" => ENV["SPANNER_TEST_INSTANCE"],
29
+ "credentials" => ENV["SPANNER_TEST_KEYFILE"],
30
+ "database" => $spanner_test_database
31
+ }
32
+ end
33
+
34
+ def spanner
35
+ $spanner ||= Google::Cloud::Spanner.new(
36
+ project_id: ENV["SPANNER_TEST_PROJECT"],
37
+ credentials: ENV["SPANNER_TEST_KEYFILE"]
38
+ )
39
+ end
40
+
41
+ def spanner_instance
42
+ unless spanner.instance ENV["SPANNER_TEST_INSTANCE"]
43
+ config = ENV["SPANNER_EMULATOR_HOST"] ? "emulator-config" : "regional-us-central1"
44
+ puts "Creating test instance #{ENV["SPANNER_TEST_INSTANCE"]} with config #{config}"
45
+ job = spanner.create_instance ENV["SPANNER_TEST_INSTANCE"],
46
+ name: "ActiveRecord Test Instance",
47
+ config: config,
48
+ nodes: 1
49
+ job.wait_until_done!
50
+ $owned_test_instance = true
51
+ end
52
+ $spanner_instance ||= spanner.instance ENV["SPANNER_TEST_INSTANCE"]
53
+ end
54
+
55
+ def create_test_database
56
+ job = spanner_instance.create_database $spanner_test_database
57
+ job.wait_until_done!
58
+ if job.error?
59
+ raise "Error in creating database. Error code#{job.error.message}"
60
+ end
61
+
62
+ puts "'#{$spanner_test_database}' test db created."
63
+
64
+ puts "Loading test schema..."
65
+ ActiveRecord::Base.establish_connection connector_config
66
+ require_relative "schema/schema"
67
+ end
68
+
69
+ def drop_test_database
70
+ ActiveRecord::Base.connection_pool.disconnect!
71
+ spanner_instance&.delete if $owned_test_instance
72
+ spanner_instance.database($spanner_test_database)&.drop unless $owned_test_instance
73
+
74
+ puts "Test instance #{spanner_instance} deleted" if $owned_test_instance
75
+ puts "#{$spanner_test_database} database deleted" unless $owned_test_instance
76
+ end
77
+
78
+ def current_adapter? *names
79
+ names.include? :SpannerAdapter
80
+ end
81
+
82
+ def load_test_schema
83
+ ActiveRecord::Base.establish_connection connector_config
84
+
85
+ require_relative "schema/schema"
86
+ end
87
+
88
+ module SpannerAdapter
89
+ class TestCase < ActiveSupport::TestCase
90
+ def assert_column(model, column_name, msg = nil)
91
+ assert has_column?(model, column_name), msg
92
+ end
93
+
94
+ def assert_no_column(model, column_name, msg = nil)
95
+ assert_not has_column?(model, column_name), msg
96
+ end
97
+
98
+ def has_column?(model, column_name)
99
+ model.reset_column_information
100
+ model.column_names.include?(column_name.to_s)
101
+ end
102
+
103
+ def capture_sql
104
+ ActiveRecord::Base.connection.materialize_transactions
105
+ SQLCounter.clear_log
106
+ yield
107
+ SQLCounter.log.dup
108
+ end
109
+
110
+ def assert_queries(num = 1, options = {})
111
+ ignore_none = options.fetch(:ignore_none) { num == :any }
112
+ ActiveRecord::Base.connection.materialize_transactions
113
+ SQLCounter.clear_log
114
+ x = yield
115
+ the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log
116
+ if num == :any
117
+ assert_operator the_log.size, :>=, 1, "1 or more queries expected, but none were executed."
118
+ else
119
+ mesg = "#{the_log.size} instead of #{num} queries were executed.#{the_log.size == 0 ? '' : "\nQueries:\n#{the_log.join("\n")}"}"
120
+ assert_equal num, the_log.size, mesg
121
+ end
122
+ x
123
+ end
124
+
125
+ def assert_no_queries(options = {}, &block)
126
+ options.reverse_merge! ignore_none: true
127
+ assert_queries(0, options, &block)
128
+ end
129
+ end
130
+
131
+ module Migration
132
+ module TestHelper
133
+ attr_accessor :connection
134
+
135
+ CONNECTION_METHODS = %w[
136
+ add_column remove_column rename_column add_index change_column
137
+ rename_table column_exists? index_exists?
138
+ add_reference add_belongs_to remove_reference remove_references
139
+ remove_belongs_to change_column_default
140
+ ].freeze
141
+
142
+ class TestModel < ActiveRecord::Base
143
+ self.table_name = :test_models
144
+ end
145
+
146
+ def setup
147
+ ActiveRecord::Base.establish_connection connector_config
148
+ @connection = ActiveRecord::Base.connection
149
+
150
+ unless @skip_test_table_create
151
+ connection.create_table :test_models do |t|
152
+ t.timestamps null: true
153
+ end
154
+
155
+ TestModel.reset_column_information
156
+ end
157
+ super
158
+ end
159
+
160
+ def skip_test_table_create!
161
+ @skip_test_table_create = true
162
+ end
163
+
164
+ def teardown
165
+ TestModel.reset_table_name
166
+
167
+ unless @skip_test_table_create
168
+ connection.drop_table :test_models, if_exists: true
169
+ end
170
+
171
+ super
172
+ end
173
+
174
+ def generate_id
175
+ connection.next_sequence_value nil
176
+ end
177
+
178
+ delegate *CONNECTION_METHODS, to: :connection
179
+ end
180
+ end
181
+
182
+ module Types
183
+ module TestHelper
184
+ attr_accessor :connection
185
+
186
+ class TestTypeModel < ActiveRecord::Base
187
+ self.table_name = :test_types
188
+ end
189
+
190
+ def setup
191
+ super
192
+
193
+ ActiveRecord::Base.establish_connection connector_config
194
+ @connection = ActiveRecord::Base.connection
195
+
196
+ return if connection.table_exists? :test_types
197
+
198
+ connection.create_table :test_types do |t|
199
+ t.string :name, limit: 255
200
+ t.string :description
201
+ t.text :bio
202
+ t.integer :length
203
+ t.float :weight
204
+ t.numeric :price
205
+ t.boolean :active
206
+ t.binary :file
207
+ t.binary :data, limit: 255
208
+ t.date :start_date
209
+ t.datetime :start_datetime
210
+ t.time :start_time
211
+ t.json :details unless ENV["SPANNER_EMULATOR_HOST"]
212
+ end
213
+ end
214
+
215
+ def teardown
216
+ super
217
+ TestTypeModel.delete_all
218
+ end
219
+ end
220
+ end
221
+
222
+ module Associations
223
+ module TestHelper
224
+ def setup
225
+ ActiveRecord::Base.establish_connection connector_config
226
+ @connection = ActiveRecord::Base.connection
227
+ end
228
+ end
229
+ end
230
+
231
+ class SQLCounter
232
+ class << self
233
+ attr_accessor :ignored_sql, :log, :log_all
234
+
235
+ def clear_log
236
+ self.log = []
237
+ self.log_all = []
238
+ end
239
+ end
240
+
241
+ clear_log
242
+
243
+ def call(name, start, finish, message_id, values)
244
+ return if values[:cached]
245
+
246
+ sql = values[:sql]
247
+ self.class.log_all << sql
248
+ self.class.log << sql unless ["SCHEMA", "TRANSACTION"].include? values[:name]
249
+ end
250
+ end
251
+
252
+ ActiveSupport::Notifications.subscribe("sql.active_record", SQLCounter.new)
253
+ end
254
+
255
+ Minitest.after_run do
256
+ drop_test_database
257
+ end
258
+
259
+ create_test_database
260
+
261
+ # rubocop:enable Style/GlobalVars
@@ -1,27 +1,42 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'activerecord-spanner-adapter/version'
1
+ lib = File.expand_path "lib", __dir__
2
+ $LOAD_PATH.unshift lib unless $LOAD_PATH.include? lib
3
+ require "activerecord_spanner_adapter/version"
5
4
 
6
5
  Gem::Specification.new do |spec|
7
6
  spec.name = "activerecord-spanner-adapter"
8
7
  spec.version = ActiveRecordSpannerAdapter::VERSION
9
- spec.authors = ["Yuki Yugui Sonoda"]
10
- spec.email = ["yuki.sonoda@supership.jp"]
8
+ spec.authors = ["Google LLC"]
9
+ spec.email = ["cloud-spanner-developers@googlegroups.com"]
11
10
 
12
- spec.summary = %q{Adapts Google Cloud Spanner to ActiveRecord}
13
- spec.description = %q{Connection Adapter of Google Cloud Spanner to ActiveRecord O/R mapper library}
14
- spec.homepage = "https://github.com/supership-jp/activerecord-spanner-adapter"
11
+ spec.summary = %q{Rails ActiveRecord connector for Google Spanner Database}
12
+ spec.description = %q{Rails ActiveRecord connector for Google Spanner Database}
13
+ spec.homepage = "https://github.com/googleapis/ruby-spanner-activerecord"
14
+ spec.license = "MIT"
15
15
 
16
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
- f.match(%r{^(test|spec|features)/})
16
+ # Specify which files should be added to the gem when it is released.
17
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
18
+ spec.files = Dir.chdir File.expand_path(__dir__) do
19
+ `git ls-files -z`.split("\x0").reject { |f| f.match %r{^(test|spec|features)/} }
18
20
  end
21
+ spec.bindir = "exe"
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename f }
19
23
  spec.require_paths = ["lib"]
20
24
 
21
- spec.add_dependency 'activerecord', "~> 5.0"
22
- spec.add_dependency 'google-cloud-spanner', "~> 0.23"
23
- spec.add_dependency 'google-gax', '~> 0.8'
24
- spec.add_development_dependency "bundler", "~> 1.14"
25
- spec.add_development_dependency "rake", "~> 10.0"
26
- spec.add_development_dependency "rspec", "~> 3.6.0"
25
+ spec.required_ruby_version = ">= 2.5"
26
+
27
+ spec.add_dependency "google-cloud-spanner", "~> 2.10"
28
+ spec.add_runtime_dependency "activerecord", "~> 6.1.4"
29
+
30
+ spec.add_development_dependency "autotest-suffix", "~> 1.1"
31
+ spec.add_development_dependency "bundler", "~> 2.0"
32
+ spec.add_development_dependency "google-style", "~> 1.24.0"
33
+ spec.add_development_dependency "minitest", "~> 5.10"
34
+ spec.add_development_dependency "minitest-autotest", "~> 1.0"
35
+ spec.add_development_dependency "minitest-focus", "~> 1.1"
36
+ spec.add_development_dependency "minitest-rg", "~> 5.2"
37
+ spec.add_development_dependency "rake", "~> 13.0"
38
+ spec.add_development_dependency "redcarpet", "~> 3.0"
39
+ spec.add_development_dependency "simplecov", "~> 0.9"
40
+ spec.add_development_dependency "yard", "~> 0.9"
41
+ spec.add_development_dependency "yard-doctest", "~> 0.1.13"
27
42
  end
Binary file
@@ -0,0 +1,17 @@
1
+ # Benchmarks
2
+
3
+ Benchmarks the Spanner ActiveRecord adapter using a small set of standardized use cases. The benchmarks consists of two
4
+ separate runs:
5
+ * Execute each use case separately and measure the time needed for each use case.
6
+ * Batch all use cases together and execute multiple batches in parallel, measuring the time needed to finish all
7
+ batches. The number of batches varies between 1 and 400. The session pool is configured to contain at most 400
8
+ connections.
9
+
10
+ Change the configuration in the file `config/database.yml` before running the benchmarks. The instance and database in
11
+ the configuration must exist. The tables that are needed will automatically be created by the benchmark script.
12
+
13
+ Run the benchmark with the command
14
+
15
+ ```bash
16
+ bundle exec rake run
17
+ ```
@@ -0,0 +1,14 @@
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
+ require_relative "config/environment"
8
+ require "sinatra/activerecord/rake"
9
+
10
+ desc "Runs benchmarks against a Cloud Spanner database."
11
+ task :run do |_t, _args|
12
+ sh "rake db:migrate"
13
+ sh "ruby application.rb"
14
+ end
@@ -0,0 +1,308 @@
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
+ require "benchmark"
8
+ require "io/console"
9
+ require "securerandom"
10
+ require_relative "config/environment"
11
+ require_relative "models/singer"
12
+ require_relative "models/album"
13
+
14
+ class Application
15
+ def self.run # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
16
+ ActiveRecord::Base.logger.level = Logger::WARN
17
+ config = ActiveRecord::Base.connection_config
18
+ spanner = Google::Cloud::Spanner.new project: config[:project], credentials: config[:credentials]
19
+ spanner_client = spanner.client config[:instance], config[:database], pool: { max: config[:pool], fail: false }
20
+
21
+ [nil, spanner_client].each do |client| # rubocop:disable Metrics/BlockLength
22
+ puts ""
23
+ puts ""
24
+ puts "Benchmarks for #{client ? 'Spanner client' : 'ActiveRecord'}"
25
+ Album.delete_all
26
+ Singer.delete_all
27
+
28
+ # Seed the database with 10,000 random singers.
29
+ # Having a relatively large number of records in the database prevents the parallel test cases from using the same
30
+ # records, which would cause many of the transactions to be aborted and retried all the time.
31
+ singer = nil
32
+ 10.times do
33
+ singer = create_singers 1000, client
34
+ end
35
+
36
+ execute_individual_benchmarks singer, client
37
+
38
+ Benchmark.bm 75 do |bm|
39
+ [1, 5, 10, 25, 50, 100, 200, 400].each do |parallel_benchmarks|
40
+ bm.report "Total execution time (#{parallel_benchmarks}):" do
41
+ threads = []
42
+ parallel_benchmarks.times do
43
+ threads << Thread.new do
44
+ benchmark_select_one_singer singer, client
45
+ benchmark_select_and_update_using_mutation 1, client
46
+ benchmark_select_and_update_using_dml 1, client
47
+ benchmark_create_and_reload client
48
+ benchmark_create_albums_using_mutations 1, client
49
+ benchmark_create_albums_using_dml 1, client
50
+ benchmark_select_100_singers client
51
+ benchmark_select_100_singers_in_read_only_transaction client
52
+ benchmark_select_100_singers_in_read_write_transaction client
53
+ end
54
+ end
55
+ threads.each(&:join)
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ spanner_client.close
62
+ ActiveRecord::Base.connection_pool.disconnect
63
+
64
+ puts ""
65
+ puts "Press any key to end the application"
66
+ STDIN.getch
67
+ end
68
+
69
+ def self.execute_individual_benchmarks singer, client
70
+ puts ""
71
+ Benchmark.bm 75 do |bm| # rubocop:disable Metrics/BlockLength
72
+ bm.report "Select one row:" do
73
+ benchmark_select_one_singer singer, client
74
+ end
75
+
76
+ bm.report "Save one row with fetch after:" do
77
+ benchmark_create_and_reload client
78
+ end
79
+
80
+ bm.report "Select and update 1 row in transaction using mutation:" do
81
+ benchmark_select_and_update_using_mutation 1, client
82
+ end
83
+
84
+ bm.report "Select and update 1 row in transaction using DML:" do
85
+ benchmark_select_and_update_using_dml 1, client
86
+ end
87
+
88
+ bm.report "Select and update 25 rows in transaction using mutation:" do
89
+ benchmark_select_and_update_using_mutation 25, client
90
+ end
91
+
92
+ bm.report "Select and update 25 rows in transaction using DML:" do
93
+ benchmark_select_and_update_using_dml 25, client
94
+ end
95
+
96
+ bm.report "Create 100 albums using mutations:" do
97
+ benchmark_create_albums_using_mutations 100, client
98
+ end
99
+
100
+ bm.report "Create 100 albums using DML:" do
101
+ benchmark_create_albums_using_dml 100, client
102
+ end
103
+
104
+ bm.report "Select and iterate over 100 singers:" do
105
+ benchmark_select_100_singers client
106
+ end
107
+
108
+ bm.report "Select and iterate over 100 singers in a read-only transaction:" do
109
+ benchmark_select_100_singers_in_read_only_transaction client
110
+ end
111
+
112
+ bm.report "Select and iterate over 100 singers in a read/write transaction:" do
113
+ benchmark_select_100_singers_in_read_write_transaction client
114
+ end
115
+ end
116
+ end
117
+
118
+ def self.benchmark_select_one_singer singer, client
119
+ if client
120
+ sql = "SELECT * FROM Singers WHERE id=@id"
121
+ params = { id: singer[:id] }
122
+ param_types = { id: :INT64 }
123
+ client.execute(sql, params: params, types: param_types).rows.each do |row|
124
+ return row
125
+ end
126
+ else
127
+ Singer.find singer.id
128
+ end
129
+ end
130
+
131
+ def self.benchmark_create_and_reload client
132
+ singer = create_singers 1, client
133
+ if client
134
+ sql = "SELECT * FROM Singers WHERE id=@id"
135
+ params = { id: singer[:id] }
136
+ param_types = { id: :INT64 }
137
+ client.execute(sql, params: params, types: param_types).rows.each do |row|
138
+ return row
139
+ end
140
+ else
141
+ singer.reload
142
+ end
143
+ end
144
+
145
+ def self.benchmark_select_and_update_using_mutation count, client
146
+ benchmark_select_and_update count, :buffered_mutations, client
147
+ end
148
+
149
+ def self.benchmark_select_and_update_using_dml count, client
150
+ benchmark_select_and_update count, :serializable, client
151
+ end
152
+
153
+ def self.benchmark_select_and_update count, isolation, client
154
+ # Select some random singers OUTSIDE of a transaction to prevent the random select to cause transactions to abort.
155
+ sql = "SELECT id FROM singers TABLESAMPLE RESERVOIR (#{count} ROWS)"
156
+ rows = Singer.connection.select_all(sql).to_a
157
+ if client
158
+ client.transaction do |transaction|
159
+ rows.each do |row|
160
+ singer = transaction.execute("SELECT * FROM singers WHERE id=@id",
161
+ params: { id: row["id"] },
162
+ types: { id: :INT64 }).rows.first
163
+ if isolation == :buffered_mutations
164
+ transaction.update "singers", id: singer[:id], last_name: SecureRandom.uuid
165
+ else
166
+ params = { id: singer[:id], last_name: SecureRandom.uuid }
167
+ param_types = { id: :INT64, last_name: :STRING }
168
+ transaction.execute_update "UPDATE singers SET last_name=@last_name WHERE id=@id",
169
+ params: params,
170
+ types: param_types
171
+ end
172
+ end
173
+ end
174
+ else
175
+ Singer.transaction isolation: isolation do
176
+ rows.each do |row|
177
+ singer = Singer.find row["id"]
178
+ singer.update last_name: SecureRandom.uuid
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ def self.benchmark_create_albums_using_mutations count, client
185
+ create_albums count, :buffered_mutations, client
186
+ end
187
+
188
+ def self.benchmark_create_albums_using_dml count, client
189
+ create_albums count, :serializable, client
190
+ end
191
+
192
+ def self.benchmark_select_100_singers client
193
+ sql = "SELECT * FROM singers TABLESAMPLE RESERVOIR (100 ROWS)"
194
+ count = 0
195
+ if client
196
+ client.execute(sql).rows.each do |_row|
197
+ count += 1
198
+ end
199
+ else
200
+ Singer.find_by_sql(sql).each do
201
+ count += 1
202
+ end
203
+ end
204
+ end
205
+
206
+ def self.benchmark_select_100_singers_in_read_only_transaction client
207
+ sql = "SELECT * FROM singers TABLESAMPLE RESERVOIR (100 ROWS)"
208
+ count = 0
209
+ if client
210
+ client.snapshot do |snapshot|
211
+ snapshot.execute(sql).rows.each do |_row|
212
+ count += 1
213
+ end
214
+ end
215
+ else
216
+ Singer.transaction isolation: :read_only do
217
+ Singer.find_by_sql(sql).each do
218
+ count += 1
219
+ end
220
+ end
221
+ end
222
+ end
223
+
224
+ def self.benchmark_select_100_singers_in_read_write_transaction client
225
+ sql = "SELECT * FROM singers TABLESAMPLE RESERVOIR (100 ROWS)"
226
+ count = 0
227
+ if client
228
+ client.transaction do |transaction|
229
+ transaction.execute(sql).rows.each do |_row|
230
+ count += 1
231
+ end
232
+ end
233
+ else
234
+ Singer.transaction do
235
+ Singer.find_by_sql(sql).each do
236
+ count += 1
237
+ end
238
+ end
239
+ end
240
+ end
241
+
242
+ def self.create_singers count, client
243
+ first_names = %w[Pete Alice John Ethel Trudy Naomi Wendy Ruben Thomas Elly Cora Elise April Libby Alexandra Shania]
244
+ last_names = %w[Wendelson Allison Peterson Johnson Henderson Ericsson Aronson Tennet Courtou Mcdonald Berry Ramirez]
245
+
246
+ last_singer = nil
247
+ if client
248
+ client.commit do |c|
249
+ singers = []
250
+ count.times do
251
+ last_singer = { id: SecureRandom.uuid.gsub("-", "").hex & 0x7FFFFFFFFFFFFFFF,
252
+ first_name: first_names.sample, last_name: last_names.sample,
253
+ birth_date: Date.new(rand(1920..2005), rand(1..12), rand(1..28)),
254
+ picture: StringIO.new(SecureRandom.uuid) }
255
+ singers << last_singer
256
+ end
257
+ c.insert "singers", singers
258
+ end
259
+ else
260
+ Singer.transaction isolation: :buffered_mutations do
261
+ count.times do
262
+ last_singer = Singer.create first_name: first_names.sample, last_name: last_names.sample,
263
+ birth_date: Date.new(rand(1920..2005), rand(1..12), rand(1..28)),
264
+ picture: StringIO.new("some-picture-#{SecureRandom.uuid}")
265
+ end
266
+ end
267
+ end
268
+ last_singer
269
+ end
270
+
271
+ def self.create_albums count, isolation, client
272
+ sql = "SELECT * FROM singers TABLESAMPLE RESERVOIR (1 ROWS)"
273
+ singer = Singer.find_by_sql(sql).first
274
+ if client
275
+ if isolation == :buffered_mutations
276
+ client.commit do |c|
277
+ albums = []
278
+ count.times do
279
+ albums << { id: SecureRandom.uuid.gsub("-", "").hex & 0x7FFFFFFFFFFFFFFF,
280
+ singer_id: singer.id, title: "Some random title",
281
+ release_date: Date.new(2021, 7, 1) }
282
+ end
283
+ c.insert "albums", albums
284
+ end
285
+ else
286
+ client.transaction do |transaction|
287
+ sql = "INSERT INTO albums (id, singer_id, title, release_date) VALUES (@id, @singer, @title, @release_date)"
288
+ transaction.batch_update do |b|
289
+ count.times do
290
+ params = { id: SecureRandom.uuid.gsub("-", "").hex & 0x7FFFFFFFFFFFFFFF, singer: singer.id,
291
+ title: "Some random title", release_date: Date.new(2021, 7, 1) }
292
+ param_types = { id: :INT64, singer: :INT64, title: :STRING, release_date: :DATE }
293
+ b.batch_update sql, params: params, types: param_types
294
+ end
295
+ end
296
+ end
297
+ end
298
+ else
299
+ Album.transaction isolation: isolation do
300
+ count.times do
301
+ Album.create singer: singer, title: "Some random title", release_date: Date.new(2021, 7, 1)
302
+ end
303
+ end
304
+ end
305
+ end
306
+ end
307
+
308
+ Application.run
@@ -0,0 +1,8 @@
1
+ development:
2
+ adapter: spanner
3
+ project: <benchmark-project>
4
+ instance: <benchmark-instance>
5
+ database: <benchmark-database>
6
+ credentials: <benchmark-credentials>
7
+ pool: 400
8
+ checkout_timeout: 60000
@@ -0,0 +1,12 @@
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
+ require "active_record"
8
+ require "bundler"
9
+
10
+ Dir["../lib/*.rb"].each { |file| require file }
11
+
12
+ Bundler.require