activerecord-spanner-adapter 0.1.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (292) hide show
  1. checksums.yaml +5 -5
  2. data/.github/CODEOWNERS +7 -0
  3. data/.github/sync-repo-settings.yaml +16 -0
  4. data/.github/workflows/acceptance-tests-on-emulator.yaml +45 -0
  5. data/.github/workflows/acceptance-tests-on-production.yaml +49 -0
  6. data/.github/workflows/ci.yaml +33 -0
  7. data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +52 -0
  8. data/.github/workflows/nightly-acceptance-tests-on-production.yaml +35 -0
  9. data/.github/workflows/nightly-unit-tests.yaml +40 -0
  10. data/.github/workflows/release-please-label.yml +25 -0
  11. data/.github/workflows/release-please.yml +39 -0
  12. data/.github/workflows/rubocop.yaml +31 -0
  13. data/.gitignore +67 -5
  14. data/.kokoro/populate-secrets.sh +77 -0
  15. data/.kokoro/release.cfg +33 -0
  16. data/.kokoro/release.sh +15 -0
  17. data/.kokoro/trampoline_v2.sh +489 -0
  18. data/.rubocop.yml +46 -0
  19. data/.toys/release.rb +18 -0
  20. data/.trampolinerc +48 -0
  21. data/.yardopts +11 -0
  22. data/CHANGELOG.md +42 -0
  23. data/CODE_OF_CONDUCT.md +40 -0
  24. data/CONTRIBUTING.md +79 -0
  25. data/Gemfile +9 -5
  26. data/LICENSE +6 -6
  27. data/README.md +67 -30
  28. data/Rakefile +74 -2
  29. data/SECURITY.md +7 -0
  30. data/acceptance/cases/associations/has_many_associations_test.rb +119 -0
  31. data/acceptance/cases/associations/has_many_through_associations_test.rb +63 -0
  32. data/acceptance/cases/associations/has_one_associations_test.rb +79 -0
  33. data/acceptance/cases/associations/has_one_through_associations_test.rb +98 -0
  34. data/acceptance/cases/interleaved_associations/has_many_associations_using_interleaved_test.rb +211 -0
  35. data/acceptance/cases/migration/change_schema_test.rb +433 -0
  36. data/acceptance/cases/migration/change_table_test.rb +115 -0
  37. data/acceptance/cases/migration/column_attributes_test.rb +122 -0
  38. data/acceptance/cases/migration/column_positioning_test.rb +48 -0
  39. data/acceptance/cases/migration/columns_test.rb +201 -0
  40. data/acceptance/cases/migration/command_recorder_test.rb +406 -0
  41. data/acceptance/cases/migration/create_join_table_test.rb +216 -0
  42. data/acceptance/cases/migration/ddl_batching_test.rb +80 -0
  43. data/acceptance/cases/migration/foreign_key_test.rb +297 -0
  44. data/acceptance/cases/migration/index_test.rb +211 -0
  45. data/acceptance/cases/migration/references_foreign_key_test.rb +259 -0
  46. data/acceptance/cases/migration/references_index_test.rb +135 -0
  47. data/acceptance/cases/migration/references_statements_test.rb +166 -0
  48. data/acceptance/cases/migration/rename_column_test.rb +96 -0
  49. data/acceptance/cases/models/calculation_query_test.rb +128 -0
  50. data/acceptance/cases/models/generated_column_test.rb +126 -0
  51. data/acceptance/cases/models/mutation_test.rb +122 -0
  52. data/acceptance/cases/models/query_test.rb +171 -0
  53. data/acceptance/cases/sessions/session_not_found_test.rb +121 -0
  54. data/acceptance/cases/transactions/optimistic_locking_test.rb +141 -0
  55. data/acceptance/cases/transactions/read_only_transactions_test.rb +130 -0
  56. data/acceptance/cases/transactions/read_write_transactions_test.rb +248 -0
  57. data/acceptance/cases/type/all_types_test.rb +172 -0
  58. data/acceptance/cases/type/binary_test.rb +59 -0
  59. data/acceptance/cases/type/boolean_test.rb +31 -0
  60. data/acceptance/cases/type/date_test.rb +32 -0
  61. data/acceptance/cases/type/date_time_test.rb +30 -0
  62. data/acceptance/cases/type/float_test.rb +27 -0
  63. data/acceptance/cases/type/integer_test.rb +44 -0
  64. data/acceptance/cases/type/json_test.rb +34 -0
  65. data/acceptance/cases/type/numeric_test.rb +27 -0
  66. data/acceptance/cases/type/string_test.rb +79 -0
  67. data/acceptance/cases/type/text_test.rb +30 -0
  68. data/acceptance/cases/type/time_test.rb +87 -0
  69. data/acceptance/models/account.rb +13 -0
  70. data/acceptance/models/address.rb +9 -0
  71. data/acceptance/models/album.rb +12 -0
  72. data/acceptance/models/all_types.rb +8 -0
  73. data/acceptance/models/author.rb +11 -0
  74. data/acceptance/models/club.rb +12 -0
  75. data/acceptance/models/comment.rb +9 -0
  76. data/acceptance/models/customer.rb +9 -0
  77. data/acceptance/models/department.rb +9 -0
  78. data/acceptance/models/firm.rb +10 -0
  79. data/acceptance/models/member.rb +13 -0
  80. data/acceptance/models/member_type.rb +9 -0
  81. data/acceptance/models/membership.rb +10 -0
  82. data/acceptance/models/organization.rb +9 -0
  83. data/acceptance/models/post.rb +10 -0
  84. data/acceptance/models/singer.rb +10 -0
  85. data/acceptance/models/track.rb +20 -0
  86. data/acceptance/models/transaction.rb +9 -0
  87. data/acceptance/schema/schema.rb +147 -0
  88. data/acceptance/test_helper.rb +261 -0
  89. data/activerecord-spanner-adapter.gemspec +32 -17
  90. data/assets/solidus-db.png +0 -0
  91. data/benchmarks/README.md +17 -0
  92. data/benchmarks/Rakefile +14 -0
  93. data/benchmarks/application.rb +308 -0
  94. data/benchmarks/config/database.yml +8 -0
  95. data/benchmarks/config/environment.rb +12 -0
  96. data/benchmarks/db/migrate/01_create_tables.rb +25 -0
  97. data/benchmarks/db/schema.rb +29 -0
  98. data/benchmarks/models/album.rb +9 -0
  99. data/benchmarks/models/singer.rb +9 -0
  100. data/bin/console +6 -7
  101. data/examples/rails/README.md +262 -0
  102. data/examples/snippets/README.md +29 -0
  103. data/examples/snippets/Rakefile +57 -0
  104. data/examples/snippets/array-data-type/README.md +45 -0
  105. data/examples/snippets/array-data-type/Rakefile +13 -0
  106. data/examples/snippets/array-data-type/application.rb +45 -0
  107. data/examples/snippets/array-data-type/config/database.yml +8 -0
  108. data/examples/snippets/array-data-type/db/migrate/01_create_tables.rb +24 -0
  109. data/examples/snippets/array-data-type/db/schema.rb +26 -0
  110. data/examples/snippets/array-data-type/db/seeds.rb +5 -0
  111. data/examples/snippets/array-data-type/models/entity_with_array_types.rb +18 -0
  112. data/examples/snippets/bin/create_emulator_instance.rb +18 -0
  113. data/examples/snippets/bulk-insert/README.md +21 -0
  114. data/examples/snippets/bulk-insert/Rakefile +13 -0
  115. data/examples/snippets/bulk-insert/application.rb +64 -0
  116. data/examples/snippets/bulk-insert/config/database.yml +8 -0
  117. data/examples/snippets/bulk-insert/db/migrate/01_create_tables.rb +21 -0
  118. data/examples/snippets/bulk-insert/db/schema.rb +26 -0
  119. data/examples/snippets/bulk-insert/db/seeds.rb +5 -0
  120. data/examples/snippets/bulk-insert/models/album.rb +9 -0
  121. data/examples/snippets/bulk-insert/models/singer.rb +9 -0
  122. data/examples/snippets/commit-timestamp/README.md +18 -0
  123. data/examples/snippets/commit-timestamp/Rakefile +13 -0
  124. data/examples/snippets/commit-timestamp/application.rb +53 -0
  125. data/examples/snippets/commit-timestamp/config/database.yml +8 -0
  126. data/examples/snippets/commit-timestamp/db/migrate/01_create_tables.rb +26 -0
  127. data/examples/snippets/commit-timestamp/db/schema.rb +29 -0
  128. data/examples/snippets/commit-timestamp/db/seeds.rb +5 -0
  129. data/examples/snippets/commit-timestamp/models/album.rb +9 -0
  130. data/examples/snippets/commit-timestamp/models/singer.rb +9 -0
  131. data/examples/snippets/config/environment.rb +21 -0
  132. data/examples/snippets/create-records/README.md +12 -0
  133. data/examples/snippets/create-records/Rakefile +13 -0
  134. data/examples/snippets/create-records/application.rb +42 -0
  135. data/examples/snippets/create-records/config/database.yml +8 -0
  136. data/examples/snippets/create-records/db/migrate/01_create_tables.rb +21 -0
  137. data/examples/snippets/create-records/db/schema.rb +26 -0
  138. data/examples/snippets/create-records/db/seeds.rb +5 -0
  139. data/examples/snippets/create-records/models/album.rb +9 -0
  140. data/examples/snippets/create-records/models/singer.rb +9 -0
  141. data/examples/snippets/date-data-type/README.md +19 -0
  142. data/examples/snippets/date-data-type/Rakefile +13 -0
  143. data/examples/snippets/date-data-type/application.rb +35 -0
  144. data/examples/snippets/date-data-type/config/database.yml +8 -0
  145. data/examples/snippets/date-data-type/db/migrate/01_create_tables.rb +20 -0
  146. data/examples/snippets/date-data-type/db/schema.rb +21 -0
  147. data/examples/snippets/date-data-type/db/seeds.rb +16 -0
  148. data/examples/snippets/date-data-type/models/singer.rb +8 -0
  149. data/examples/snippets/generated-column/README.md +41 -0
  150. data/examples/snippets/generated-column/Rakefile +13 -0
  151. data/examples/snippets/generated-column/application.rb +37 -0
  152. data/examples/snippets/generated-column/config/database.yml +8 -0
  153. data/examples/snippets/generated-column/db/migrate/01_create_tables.rb +23 -0
  154. data/examples/snippets/generated-column/db/schema.rb +21 -0
  155. data/examples/snippets/generated-column/db/seeds.rb +18 -0
  156. data/examples/snippets/generated-column/models/singer.rb +8 -0
  157. data/examples/snippets/hints/README.md +19 -0
  158. data/examples/snippets/hints/Rakefile +13 -0
  159. data/examples/snippets/hints/application.rb +47 -0
  160. data/examples/snippets/hints/config/database.yml +8 -0
  161. data/examples/snippets/hints/db/migrate/01_create_tables.rb +23 -0
  162. data/examples/snippets/hints/db/schema.rb +28 -0
  163. data/examples/snippets/hints/db/seeds.rb +29 -0
  164. data/examples/snippets/hints/models/album.rb +9 -0
  165. data/examples/snippets/hints/models/singer.rb +9 -0
  166. data/examples/snippets/interleaved-tables/README.md +152 -0
  167. data/examples/snippets/interleaved-tables/Rakefile +13 -0
  168. data/examples/snippets/interleaved-tables/application.rb +109 -0
  169. data/examples/snippets/interleaved-tables/config/database.yml +8 -0
  170. data/examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb +44 -0
  171. data/examples/snippets/interleaved-tables/db/schema.rb +32 -0
  172. data/examples/snippets/interleaved-tables/db/seeds.rb +40 -0
  173. data/examples/snippets/interleaved-tables/models/album.rb +15 -0
  174. data/examples/snippets/interleaved-tables/models/singer.rb +20 -0
  175. data/examples/snippets/interleaved-tables/models/track.rb +25 -0
  176. data/examples/snippets/migrations/README.md +43 -0
  177. data/examples/snippets/migrations/Rakefile +13 -0
  178. data/examples/snippets/migrations/application.rb +26 -0
  179. data/examples/snippets/migrations/config/database.yml +8 -0
  180. data/examples/snippets/migrations/db/migrate/01_create_tables.rb +28 -0
  181. data/examples/snippets/migrations/db/schema.rb +33 -0
  182. data/examples/snippets/migrations/db/seeds.rb +5 -0
  183. data/examples/snippets/migrations/models/album.rb +10 -0
  184. data/examples/snippets/migrations/models/singer.rb +10 -0
  185. data/examples/snippets/migrations/models/track.rb +9 -0
  186. data/examples/snippets/mutations/README.md +34 -0
  187. data/examples/snippets/mutations/Rakefile +13 -0
  188. data/examples/snippets/mutations/application.rb +47 -0
  189. data/examples/snippets/mutations/config/database.yml +8 -0
  190. data/examples/snippets/mutations/db/migrate/01_create_tables.rb +22 -0
  191. data/examples/snippets/mutations/db/schema.rb +27 -0
  192. data/examples/snippets/mutations/db/seeds.rb +25 -0
  193. data/examples/snippets/mutations/models/album.rb +9 -0
  194. data/examples/snippets/mutations/models/singer.rb +9 -0
  195. data/examples/snippets/optimistic-locking/README.md +12 -0
  196. data/examples/snippets/optimistic-locking/Rakefile +13 -0
  197. data/examples/snippets/optimistic-locking/application.rb +48 -0
  198. data/examples/snippets/optimistic-locking/config/database.yml +8 -0
  199. data/examples/snippets/optimistic-locking/db/migrate/01_create_tables.rb +26 -0
  200. data/examples/snippets/optimistic-locking/db/schema.rb +29 -0
  201. data/examples/snippets/optimistic-locking/db/seeds.rb +25 -0
  202. data/examples/snippets/optimistic-locking/models/album.rb +9 -0
  203. data/examples/snippets/optimistic-locking/models/singer.rb +9 -0
  204. data/examples/snippets/partitioned-dml/README.md +16 -0
  205. data/examples/snippets/partitioned-dml/Rakefile +13 -0
  206. data/examples/snippets/partitioned-dml/application.rb +48 -0
  207. data/examples/snippets/partitioned-dml/config/database.yml +8 -0
  208. data/examples/snippets/partitioned-dml/db/migrate/01_create_tables.rb +21 -0
  209. data/examples/snippets/partitioned-dml/db/schema.rb +26 -0
  210. data/examples/snippets/partitioned-dml/db/seeds.rb +29 -0
  211. data/examples/snippets/partitioned-dml/models/album.rb +9 -0
  212. data/examples/snippets/partitioned-dml/models/singer.rb +9 -0
  213. data/examples/snippets/quickstart/README.md +26 -0
  214. data/examples/snippets/quickstart/Rakefile +13 -0
  215. data/examples/snippets/quickstart/application.rb +51 -0
  216. data/examples/snippets/quickstart/config/database.yml +8 -0
  217. data/examples/snippets/quickstart/db/migrate/01_create_tables.rb +21 -0
  218. data/examples/snippets/quickstart/db/schema.rb +26 -0
  219. data/examples/snippets/quickstart/db/seeds.rb +24 -0
  220. data/examples/snippets/quickstart/models/album.rb +9 -0
  221. data/examples/snippets/quickstart/models/singer.rb +9 -0
  222. data/examples/snippets/read-only-transactions/README.md +13 -0
  223. data/examples/snippets/read-only-transactions/Rakefile +13 -0
  224. data/examples/snippets/read-only-transactions/application.rb +77 -0
  225. data/examples/snippets/read-only-transactions/config/database.yml +8 -0
  226. data/examples/snippets/read-only-transactions/db/migrate/01_create_tables.rb +21 -0
  227. data/examples/snippets/read-only-transactions/db/schema.rb +26 -0
  228. data/examples/snippets/read-only-transactions/db/seeds.rb +24 -0
  229. data/examples/snippets/read-only-transactions/models/album.rb +9 -0
  230. data/examples/snippets/read-only-transactions/models/singer.rb +9 -0
  231. data/examples/snippets/read-write-transactions/README.md +12 -0
  232. data/examples/snippets/read-write-transactions/Rakefile +13 -0
  233. data/examples/snippets/read-write-transactions/application.rb +39 -0
  234. data/examples/snippets/read-write-transactions/config/database.yml +8 -0
  235. data/examples/snippets/read-write-transactions/db/migrate/01_create_tables.rb +22 -0
  236. data/examples/snippets/read-write-transactions/db/schema.rb +27 -0
  237. data/examples/snippets/read-write-transactions/db/seeds.rb +25 -0
  238. data/examples/snippets/read-write-transactions/models/album.rb +9 -0
  239. data/examples/snippets/read-write-transactions/models/singer.rb +9 -0
  240. data/examples/snippets/stale-reads/README.md +27 -0
  241. data/examples/snippets/stale-reads/Rakefile +13 -0
  242. data/examples/snippets/stale-reads/application.rb +63 -0
  243. data/examples/snippets/stale-reads/config/database.yml +8 -0
  244. data/examples/snippets/stale-reads/db/migrate/01_create_tables.rb +21 -0
  245. data/examples/snippets/stale-reads/db/schema.rb +26 -0
  246. data/examples/snippets/stale-reads/db/seeds.rb +24 -0
  247. data/examples/snippets/stale-reads/models/album.rb +9 -0
  248. data/examples/snippets/stale-reads/models/singer.rb +9 -0
  249. data/examples/snippets/timestamp-data-type/README.md +17 -0
  250. data/examples/snippets/timestamp-data-type/Rakefile +13 -0
  251. data/examples/snippets/timestamp-data-type/application.rb +42 -0
  252. data/examples/snippets/timestamp-data-type/config/database.yml +8 -0
  253. data/examples/snippets/timestamp-data-type/db/migrate/01_create_tables.rb +21 -0
  254. data/examples/snippets/timestamp-data-type/db/schema.rb +21 -0
  255. data/examples/snippets/timestamp-data-type/db/seeds.rb +6 -0
  256. data/examples/snippets/timestamp-data-type/models/meeting.rb +19 -0
  257. data/examples/solidus/README.md +172 -0
  258. data/lib/active_record/connection_adapters/spanner/database_statements.rb +244 -251
  259. data/lib/active_record/connection_adapters/spanner/quoting.rb +42 -50
  260. data/lib/active_record/connection_adapters/spanner/schema_cache.rb +43 -0
  261. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +129 -7
  262. data/lib/active_record/connection_adapters/spanner/schema_definitions.rb +122 -0
  263. data/lib/active_record/connection_adapters/spanner/schema_dumper.rb +19 -0
  264. data/lib/active_record/connection_adapters/spanner/schema_statements.rb +553 -141
  265. data/lib/active_record/connection_adapters/spanner/type_metadata.rb +37 -0
  266. data/lib/active_record/connection_adapters/spanner_adapter.rb +188 -70
  267. data/lib/active_record/tasks/spanner_database_tasks.rb +74 -0
  268. data/lib/active_record/type/spanner/array.rb +32 -0
  269. data/lib/active_record/type/spanner/bytes.rb +26 -0
  270. data/lib/active_record/type/spanner/spanner_active_record_converter.rb +33 -0
  271. data/lib/active_record/type/spanner/time.rb +37 -0
  272. data/lib/activerecord-spanner-adapter.rb +23 -0
  273. data/lib/activerecord_spanner_adapter/base.rb +238 -0
  274. data/lib/activerecord_spanner_adapter/connection.rb +324 -0
  275. data/lib/activerecord_spanner_adapter/errors.rb +13 -0
  276. data/lib/activerecord_spanner_adapter/foreign_key.rb +29 -0
  277. data/lib/activerecord_spanner_adapter/index/column.rb +38 -0
  278. data/lib/activerecord_spanner_adapter/index.rb +80 -0
  279. data/lib/activerecord_spanner_adapter/information_schema.rb +261 -0
  280. data/lib/activerecord_spanner_adapter/primary_key.rb +31 -0
  281. data/lib/activerecord_spanner_adapter/table/column.rb +59 -0
  282. data/lib/activerecord_spanner_adapter/table.rb +61 -0
  283. data/lib/activerecord_spanner_adapter/transaction.rb +123 -0
  284. data/lib/activerecord_spanner_adapter/version.rb +9 -0
  285. data/lib/arel/visitors/spanner.rb +111 -0
  286. data/lib/spanner_client_ext.rb +103 -0
  287. data/renovate.json +5 -0
  288. metadata +417 -36
  289. data/.gitmodules +0 -3
  290. data/.travis.yml +0 -5
  291. data/lib/active_record/connection_adapters/spanner.rb +0 -10
  292. data/lib/activerecord-spanner-adapter/version.rb +0 -3
@@ -0,0 +1,40 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of
4
+ fostering an open and welcoming community, we pledge to respect all people who
5
+ contribute through reporting issues, posting feature requests, updating
6
+ documentation, submitting pull requests or patches, and other activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, ethnicity, age, religion, or nationality.
12
+
13
+ Examples of unacceptable behavior by participants include:
14
+
15
+ * The use of sexualized language or imagery
16
+ * Personal attacks
17
+ * Trolling or insulting/derogatory comments
18
+ * Public or private harassment
19
+ * Publishing other's private information, such as physical or electronic
20
+ addresses, without explicit permission
21
+ * Other unethical or unprofessional conduct.
22
+
23
+ Project maintainers have the right and responsibility to remove, edit, or reject
24
+ comments, commits, code, wiki edits, issues, and other contributions that are
25
+ not aligned to this Code of Conduct. By adopting this Code of Conduct, project
26
+ maintainers commit themselves to fairly and consistently applying these
27
+ principles to every aspect of managing this project. Project maintainers who do
28
+ not follow or enforce the Code of Conduct may be permanently removed from the
29
+ project team.
30
+
31
+ This code of conduct applies both within project spaces and in public spaces
32
+ when an individual is representing the project or its community.
33
+
34
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
35
+ reported by opening an issue or contacting one or more of the project
36
+ maintainers.
37
+
38
+ This Code of Conduct is adapted from the [Contributor
39
+ Covenant](http://contributor-covenant.org), version 1.2.0, available at
40
+ [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,79 @@
1
+ # How to Contribute
2
+
3
+ We'd love to accept your patches and contributions to this project. There are
4
+ just a few small guidelines you need to follow.
5
+
6
+ ## Contributor License Agreement
7
+
8
+ Contributions to this project must be accompanied by a Contributor License
9
+ Agreement. You (or your employer) retain the copyright to your contribution;
10
+ this simply gives us permission to use and redistribute your contributions as
11
+ part of the project. Head over to <https://cla.developers.google.com/> to see
12
+ your current agreements on file or to sign a new one.
13
+
14
+ You generally only need to submit a CLA once, so if you've already submitted one
15
+ (even if it was for a different project), you probably don't need to do it
16
+ again.
17
+
18
+ ## Code reviews
19
+
20
+ All submissions, including submissions by project members, require review. We
21
+ use GitHub pull requests for this purpose. Consult
22
+ [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
23
+ information on using pull requests.
24
+
25
+ ## Community Guidelines
26
+
27
+ This project follows [Google's Open Source Community
28
+ Guidelines](https://opensource.google/conduct/).
29
+
30
+ ## Tests
31
+
32
+ ### Functional tests
33
+ We have functional tests for individual components that can be run by
34
+ ```shell
35
+ bundle exec rake test
36
+ ```
37
+
38
+ ### ActiveRecord integration tests
39
+ We run full integration tests with continuous integration on Google Cloud Build with Kokoro.
40
+
41
+ Command : `bundle exec rake acceptance[project,keyfile,instance]`
42
+
43
+ Variable|Description|Comment
44
+ ---|---|---
45
+ `project`|The project id of the Google Application credentials being used|For example `appdev-soda-spanner-staging`
46
+ `keyfile`|The Google Application Credentials file|For example `~/Downloads/creds.json`
47
+ `instance`|The Cloud Spanner instance to use, it MUST exist before running tests| For example
48
+ `activerecord_tests`
49
+
50
+ #### Example
51
+ ```shell
52
+ bundle exec rake acceptance[appdev-soda-spanner-staging,/home/Downloads/creds.json,activerecord_tests]
53
+ ```
54
+
55
+ ## Coding Style
56
+
57
+ Please follow the established coding style in the library. The style is is
58
+ largely based on [The Ruby Style
59
+ Guide](https://github.com/bbatsov/ruby-style-guide) with a few exceptions based
60
+ on seattle-style:
61
+
62
+ * Avoid parenthesis when possible, including in method definitions.
63
+ * Always use double quotes strings. ([Option
64
+ B](https://github.com/bbatsov/ruby-style-guide#strings))
65
+
66
+ You can check your code against these rules by running Rubocop like so:
67
+
68
+ ```sh
69
+ $ cd ruby-spanner-activerecord
70
+ $ bundle exec rubocop
71
+ ```
72
+
73
+ The rubocop settings depend on [googleapis/ruby-style](https://github.com/googleapis/ruby-style/), in addition to [.rubocop.yml](https://github.com/googleapis/ruby-spanner-activerecord/blob/master/.rubocop.yml).
74
+
75
+ ## Code of Conduct
76
+
77
+ Please note that this project is released with a Contributor Code of Conduct. By
78
+ participating in this project you agree to abide by its terms. See
79
+ {file:CODE_OF_CONDUCT.md Code of Conduct} for more information.
data/Gemfile CHANGED
@@ -1,8 +1,12 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
 
3
- # Specify your gem's dependencies in activerecord-spanner-adapter.gemspec
3
+ # Specify your gem's dependencies in activerecord-spanner.gemspec
4
4
  gemspec
5
5
 
6
- gem 'google-cloud-spanner', path: 'vendor/gcloud-ruby/google-cloud-spanner'
7
- gem 'pry'
8
- gem 'pry-byebug'
6
+ gem "minitest", "~> 5.14.0"
7
+ gem "pry", "~> 0.13.0"
8
+ gem "pry-byebug", "~> 3.9.0"
9
+
10
+ # Required for samples
11
+ gem 'docker-api'
12
+ gem "sinatra-activerecord"
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
- MIT License
1
+ The MIT License (MIT)
2
2
 
3
- Copyright (c) 2017 Supership Inc.
3
+ Copyright (c) 2020 Google LLC.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
9
  copies of the Software, and to permit persons to whom the Software is
10
10
  furnished to do so, subject to the following conditions:
11
11
 
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
14
 
15
15
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
16
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
17
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md CHANGED
@@ -1,59 +1,96 @@
1
- # ActiveRecord Spanner adapter
1
+ # ActiveRecord Cloud Spanner Adapter
2
2
 
3
- The [Cloud Spanner](https://cloud.google.com/spanner/) adapter for ActiveRecord.
3
+ [Google Cloud Spanner](https://cloud.google.com/spanner) adapter for ActiveRecord.
4
4
 
5
- ## Status
6
- Proof of concept.
7
- You cannot expect that this gem is ready for production use -- many features are not supported.
5
+ ![rubocop](https://github.com/googleapis/ruby-spanner-activerecord/workflows/rubocop/badge.svg)
6
+
7
+ This project provides a Cloud Spanner adapter for ActiveRecord. It has the __Preview__ release status and supports the following versions:
8
+
9
+ - ActiveRecord 6.0.x with Ruby 2.6 and 2.7.
10
+ - ActiveRecord 6.1.x with Ruby 2.6 and higher.
11
+
12
+ Known limitations are listed in the [Limitations](#limitations) section.
13
+ Please report any problems that you might encounter by [creating a new issue](https://github.com/googleapis/ruby-spanner-activerecord/issues/new).
8
14
 
9
15
  ## Installation
10
16
 
11
17
  Add this line to your application's Gemfile:
12
18
 
13
19
  ```ruby
14
- gem 'activerecord-spanner-adapter',
15
- git: 'https://github.com/supership-jp/activerecord-spanner-adapter.git'
20
+ gem 'activerecord-spanner-adapter'
16
21
  ```
17
22
 
18
- And then execute:
23
+ If you would like to use latest adapter version from github then specify
19
24
 
20
- $ bundle
25
+ ```ruby
26
+ gem 'activerecord-spanner-adapter', :git => 'git@github.com:googleapis/ruby-spanner-activerecord.git'
27
+ ```
21
28
 
22
- Or install it yourself as:
29
+ And then execute:
23
30
 
24
- $ gem install activerecord-spanner-adapter
31
+ $ bundle
25
32
 
26
33
  ## Usage
27
34
 
28
- Add a configuration like this into your `database.yml`.
35
+ ### Database Connection
36
+ In Rails application `config/database.yml`, make the change as the following:
29
37
 
30
- ```yaml
31
- default:
32
- adapter: spanner
33
- project: your-gcp-project-name
34
- instance: your-spanner-instance-name
35
- database: your-spanner-database-name
36
- keyfile: path/to/serivce-account-credential.json
38
+ ```
39
+ development:
40
+ adapter: "spanner"
41
+ project: "<google project name>"
42
+ instance: "<google instance name>"
43
+ credentials: "<google credentails file path>"
44
+ database: "app-dev"
37
45
  ```
38
46
 
39
- *NOTE*: This adapter uses UUIDs as primary keys by default unlike other adapters.
40
- This is because monotonically increasing primary key restricts write performance in Spanner.
41
-
42
- c.f. https://cloud.google.com/spanner/docs/best-practices
43
-
47
+ ## Examples
48
+ To get started with Rails, read the tutorial under {file:examples/rails/README.md examples/rails/README.md}.
44
49
 
45
- ## Development
50
+ You can also find a list of short self-contained code examples that show how
51
+ to use ActiveRecord with Cloud Spanner under the directory [examples/snippets](examples/snippets). Each example is directly runnable without the need to setup a Cloud Spanner
52
+ database, as all samples will automatically start a Cloud Spanner emulator in a Docker container and execute the sample
53
+ code against that emulator. All samples can be executed by navigating to the sample directory on your local machine and
54
+ then executing the command `bundle exec rake run`. Example:
46
55
 
47
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
56
+ ```bash
57
+ cd ruby-spanner-activerecord/examples/snippets/quickstart
58
+ bundle exec rake run
59
+ ```
48
60
 
49
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
61
+ __NOTE__: You do need to have [Docker](https://docs.docker.com/get-docker/) installed on your local system to run these examples.
62
+
63
+ Some noteworthy examples in the snippets directory:
64
+ - [quickstart](examples/snippets/quickstart): A simple application that shows how to create and query a simple database containing two tables.
65
+ - [migrations](examples/snippets/migrations): Shows a best-practice for executing migrations on Cloud Spanner.
66
+ - [read-write-transactions](examples/snippets/read-write-transactions): Shows how to execute transactions on Cloud Spanner.
67
+ - [read-only-transactions](examples/snippets/read-only-transactions): Shows how to execute read-only transactions on Cloud Spanner.
68
+ - [bulk-insert](examples/snippets/bulk-insert): Shows the best way to insert a large number of new records.
69
+ - [mutations](examples/snippets/mutations): Shows how you can use [mutations instead of DML](https://cloud.google.com/spanner/docs/dml-versus-mutations)
70
+ for inserting, updating and deleting data in a Cloud Spanner database. Mutations can have a significant performance
71
+ advantage compared to DML statements, but do not allow read-your-writes semantics during a transaction.
72
+ - [interleaved-tables](examples/snippets/interleaved-tables): Shows how to create and work with a hierarchy of `INTERLEAVED IN` tables.
73
+ - [array-data-type](examples/snippets/array-data-type): Shows how to work with `ARRAY` data types.
74
+
75
+ ## Limitations
76
+
77
+ Limitation|Comment|Resolution
78
+ ---|---|---
79
+ Lack of DEFAULT for columns [change_column_default](https://apidock.com/rails/v5.2.3/ActiveRecord/ConnectionAdapters/SchemaStatements/change_column_default)|Cloud Spanner does not support DEFAULT values for columns. The use of default must be enforced in your controller logic| Always set a value in your model or controller logic.
80
+ Lack of sequential and auto-assigned IDs|Cloud Spanner doesn't autogenerate IDs and this integration instead creates UUID4 to avoid [hotspotting](https://cloud.google.com/spanner/docs/schema-design#uuid_primary_key) so you SHOULD NOT rely on IDs being sorted| UUID4s are automatically generated for primary keys.
81
+ Table without Primary Key| Cloud Spanner support does not support tables without a primary key.| Always define a primary key for your table.
82
+ Table names CANNOT have spaces within them whether back-ticked or not|Cloud Spanner DOES NOT support tables with spaces in them for example `Entity ID`|Ensure that your table names don't contain spaces.
83
+ Table names CANNOT have punctuation marks and MUST contain valid UTF-8|Cloud Spanner DOES NOT support punctuation marks e.g. periods ".", question marks "?" in table names|Ensure that your table names don't contain punctuation marks.
84
+ Index with fields length [add_index](https://apidock.com/rails/v5.2.3/ActiveRecord/ConnectionAdapters/SchemaStatements/add_index)|Cloud Spanner does not support index with fields length | Ensure that your database definition does not include index definitions with field lengths.
50
85
 
51
86
  ## Contributing
52
87
 
53
- Bug reports and pull requests are welcome on GitHub at https://github.com/supership-jp/activerecord-spanner-adapter.
88
+ Bug reports and pull requests are welcome on GitHub at https://github.com/googleapis/ruby-spanner-activerecord. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
54
89
 
55
90
  ## License
56
- Copyright (c) 2017 Supership Inc.
57
91
 
58
- Licensed under MIT license.
92
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
93
+
94
+ ## Code of Conduct
59
95
 
96
+ Everyone interacting in the Activerecord::Spanner project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/googleapis/ruby-spanner-activerecord/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile CHANGED
@@ -1,10 +1,82 @@
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
+
1
7
  require "bundler/gem_tasks"
2
8
  require "rake/testtask"
9
+ require "securerandom"
3
10
 
4
- Rake::TestTask.new(:test) do |t|
11
+ desc "Run tests."
12
+ Rake::TestTask.new do |t|
5
13
  t.libs << "test"
6
14
  t.libs << "lib"
7
- t.test_files = FileList['test/**/*_test.rb']
15
+ t.test_files = FileList["test/**/*_test.rb"]
16
+ t.warning = false
8
17
  end
9
18
 
10
19
  task :default => :test
20
+
21
+ require "yard"
22
+ require "yard/rake/yardoc_task"
23
+ YARD::Rake::YardocTask.new do |y|
24
+ # y.options << "--fail-on-warning"
25
+ end
26
+
27
+ desc "Run the spanner connector acceptance tests."
28
+ task :acceptance, [:project, :keyfile, :instance, :tests] do |t, args|
29
+ project = args[:project]
30
+ project ||= ENV["SPANNER_TEST_PROJECT"] || ENV["GCLOUD_TEST_PROJECT"]
31
+ emulator_host = args[:emulator_host]
32
+ emulator_host ||= ENV["SPANNER_EMULATOR_HOST"]
33
+ keyfile = args[:keyfile]
34
+ keyfile ||= ENV["SPANNER_TEST_KEYFILE"] || ENV["GCLOUD_TEST_KEYFILE"] || ENV["GOOGLE_APPLICATION_CREDENTIALS"]
35
+ if keyfile
36
+ keyfile = File.read keyfile
37
+ else
38
+ keyfile ||= ENV["SPANNER_TEST_KEYFILE_JSON"] || ENV["GCLOUD_TEST_KEYFILE_JSON"]
39
+ end
40
+ if project.nil? || (keyfile.nil? && emulator_host.nil?)
41
+ fail "You must provide a project and keyfile or emulator host name."
42
+ end
43
+ instance = args[:instance]
44
+ instance ||= ENV["SPANNER_TEST_INSTANCE"]
45
+ if instance.nil?
46
+ fail "You must provide an instance name"
47
+ end
48
+
49
+ # clear any env var already set
50
+ require "google/cloud/spanner/credentials"
51
+ Google::Cloud::Spanner::Credentials.env_vars.each do |path|
52
+ ENV[path] = nil
53
+ end
54
+
55
+ tests = args[:tests]
56
+ tests ||= "**"
57
+
58
+ # always overwrite when running tests
59
+ ENV["SPANNER_PROJECT"] = project
60
+ ENV["SPANNER_KEYFILE_JSON"] = keyfile
61
+ ENV["SPANNER_TEST_INSTANCE"] = instance
62
+ ENV["SPANNER_EMULATOR_HOST"] = emulator_host
63
+
64
+ Rake::TestTask.new :run do |t|
65
+ t.libs << "acceptance"
66
+ t.libs << "lib"
67
+ t.test_files = FileList["acceptance/#{tests}/*_test.rb"] unless tests.start_with? "exclude "
68
+ t.test_files = FileList.new("acceptance/**/*_test.rb") do |fl|
69
+ fl.exclude "acceptance/#{tests.split(" ")[1]}/*_test.rb"
70
+ puts "excluding acceptance/#{tests.split(" ")[1]}/*_test.rb"
71
+ end if tests.start_with? "exclude"
72
+ t.warning = false
73
+ end
74
+
75
+ Rake::Task["run"].invoke
76
+ end
77
+
78
+ desc +"Runs the `examples/snippets/quickstart` example on a Spanner emulator. See the directory `examples/snippets`"
79
+ "for more examples."
80
+ task :example do
81
+ Dir.chdir("examples/snippets/quickstart") { sh "bundle exec rake run" }
82
+ end
data/SECURITY.md ADDED
@@ -0,0 +1,7 @@
1
+ # Security Policy
2
+
3
+ To report a security issue, please use [g.co/vulnz](https://g.co/vulnz).
4
+
5
+ The Google Security Team will respond within 5 working days of your report on g.co/vulnz.
6
+
7
+ We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue.
@@ -0,0 +1,119 @@
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/firm"
11
+ require "models/account"
12
+ require "models/transaction"
13
+ require "models/department"
14
+ require "models/customer"
15
+
16
+ module ActiveRecord
17
+ module Associations
18
+ class HasManyTest < SpannerAdapter::TestCase
19
+ include SpannerAdapter::Associations::TestHelper
20
+
21
+ attr_accessor :customer
22
+
23
+ def setup
24
+ super
25
+
26
+ @customer = Customer.create name: "Customer - 1"
27
+
28
+ Account.create name: "Account - 1", customer: customer, credit_limit: 100
29
+ Account.create name: "Account - 2", customer: customer, credit_limit: 200
30
+ end
31
+
32
+ def teardown
33
+ Customer.destroy_all
34
+ Account.destroy_all
35
+ end
36
+
37
+ def test_has_many
38
+ assert_equal 2, customer.accounts.count
39
+ assert_equal customer.accounts.pluck(:credit_limit).sort, [100, 200]
40
+ end
41
+
42
+ def test_finding_using_associated_fields
43
+ assert_equal Account.where(customer_id: customer.id).to_a, customer.accounts.to_a
44
+ end
45
+
46
+ def test_successful_build_association
47
+ account = customer.accounts.build(name: "Account - 3", credit_limit: 1000)
48
+ assert account.save
49
+
50
+ customer.reload
51
+ assert_equal account, customer.accounts.find(account.id)
52
+ end
53
+
54
+ def test_create_and_destroy_associated_records
55
+ customer2 = Customer.new name: "Customer - 2"
56
+ customer2.accounts.build name: "Account - 11", credit_limit: 100
57
+ customer2.accounts.build name: "Account - 12", credit_limit: 200
58
+ customer2.save!
59
+
60
+ customer2.reload
61
+
62
+ assert_equal 2, customer2.accounts.count
63
+ assert_equal 4, Account.count
64
+
65
+ customer2.accounts.destroy_all
66
+ customer2.reload
67
+
68
+ assert_equal 0, customer2.accounts.count
69
+ assert_equal 2, Account.count
70
+ end
71
+
72
+ def test_create_and_delete_associated_records
73
+ customer2 = Customer.new name: "Customer - 2"
74
+ customer2.accounts.build name: "Account - 11", credit_limit: 100
75
+ customer2.accounts.build name: "Account - 12", credit_limit: 200
76
+ customer2.save!
77
+
78
+ customer2.reload
79
+
80
+ assert_equal 2, customer2.accounts.count
81
+ assert_equal 4, Account.count
82
+
83
+ assert_equal 2, customer2.accounts.delete_all
84
+ customer2.reload
85
+
86
+ assert_equal 0, customer2.accounts.count
87
+ assert_equal 2, Account.where(customer_id: nil).count
88
+ end
89
+
90
+ def test_update_associated_records
91
+ count = customer.accounts.update_all(name: "Account - Update", credit_limit: 1000)
92
+ assert_equal customer.accounts.count, count
93
+
94
+ customer.reload
95
+ customer.accounts.each do |account|
96
+ assert_equal "Account - Update", account.name
97
+ assert_equal 1000, account.credit_limit
98
+ end
99
+ end
100
+
101
+ def test_fetch_associated_record_with_order
102
+ accounts = customer.accounts.order(credit_limit: :desc)
103
+ assert_equal [200, 100], accounts.pluck(:credit_limit)
104
+
105
+ accounts = customer.accounts.order(credit_limit: :asc)
106
+ assert_equal [100, 200], accounts.pluck(:credit_limit)
107
+ end
108
+
109
+ def test_set_counter_cache
110
+ account = Account.first
111
+ account.transactions.create!(amount: 10)
112
+ account.transactions.create!(amount: 20)
113
+
114
+ account.reload
115
+ assert_equal 2, account.transactions_count
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,63 @@
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/member"
11
+ require "models/membership"
12
+ require "models/member_type"
13
+ require "models/club"
14
+
15
+ module ActiveRecord
16
+ module Associations
17
+ class HasManyThroughTest < SpannerAdapter::TestCase
18
+ include SpannerAdapter::Associations::TestHelper
19
+
20
+ attr_accessor :club, :member_one, :member_two
21
+
22
+ def setup
23
+ super
24
+
25
+ @club = Club.create name: "Club - 1"
26
+ @member_one = Member.create name: "Member - 1"
27
+ @member_two = Member.create name: "Member - 2"
28
+ end
29
+
30
+ def teardown
31
+ Member.destroy_all
32
+ Club.destroy_all
33
+ end
34
+
35
+ def test_has_many_through_create_record
36
+ assert club.members.create!(name: "Member - 3")
37
+ end
38
+
39
+ def test_through_association_with_joins
40
+ club.members = [member_one, member_two]
41
+ assert_equal [club, club], Club.where(id: club.id).joins(:members).to_a
42
+ end
43
+
44
+ def test_set_record_after_delete_association
45
+ club.members = [member_one, member_two]
46
+ club.reload
47
+ assert_equal 2, club.members.count
48
+
49
+ club.members = []
50
+ club.reload
51
+ assert_empty club.members
52
+ end
53
+
54
+ def test_has_many_through_eager_loading
55
+ club.members = [member_one, member_two]
56
+
57
+ clubs = Club.includes(:members).all.to_a
58
+ assert_equal 1, clubs.size
59
+ assert_not_nil assert_no_queries { clubs[0].members }
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,79 @@
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/firm"
11
+ require "models/account"
12
+ require "models/department"
13
+
14
+
15
+ module ActiveRecord
16
+ module Associations
17
+ class HasOneTest < SpannerAdapter::TestCase
18
+ include SpannerAdapter::Associations::TestHelper
19
+
20
+ attr_accessor :firm, :account
21
+
22
+ def setup
23
+ super
24
+
25
+ @account = Account.create name: "Account - #{rand 1000}", credit_limit: 100
26
+ @firm = Firm.create name: "Firm-#{rand 1000}", account: account
27
+
28
+ @account.reload
29
+ @firm.reload
30
+ end
31
+
32
+ def teardown
33
+ Firm.destroy_all
34
+ Account.destroy_all
35
+ Department.destroy_all
36
+ end
37
+
38
+ def test_has_one
39
+ assert_equal account, firm.account
40
+ assert_equal account.credit_limit, firm.account.credit_limit
41
+ end
42
+
43
+ def test_has_one_does_not_use_order_by
44
+ sql_log = capture_sql { firm.account }
45
+ assert sql_log.all? { |sql| !/order by/i.match?(sql) }, "ORDER BY was used in the query: #{sql_log}"
46
+ end
47
+
48
+ def test_finding_using_primary_key
49
+ assert_equal Account.find_by(firm_id: firm.id), firm.account
50
+ end
51
+
52
+ def test_successful_build_association
53
+ account = firm.build_account(credit_limit: 1000)
54
+ assert account.save
55
+
56
+ firm.reload
57
+ assert_equal account, firm.account
58
+ end
59
+
60
+ def test_delete_associated_records
61
+ assert_equal account, firm.account
62
+
63
+ firm.account.destroy
64
+ firm.reload
65
+ assert_nil firm.account
66
+ end
67
+
68
+ def test_polymorphic_association
69
+ assert_equal 0, firm.departments.count
70
+
71
+ firm.departments.create(name: "Department - 1")
72
+ firm.reload
73
+
74
+ assert_equal 1, firm.departments.count
75
+ assert_equal "Department - 1", firm.departments.first.name
76
+ end
77
+ end
78
+ end
79
+ end