activerecord-spanner-adapter 0.1.0 → 0.7.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 (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,80 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ require "activerecord_spanner_adapter/index/column"
8
+
9
+ module ActiveRecordSpannerAdapter
10
+ class Index
11
+ attr_accessor :table, :name, :columns, :type, :unique, :null_filtered,
12
+ :interleave_in, :storing, :state
13
+
14
+ def initialize \
15
+ table,
16
+ name,
17
+ columns,
18
+ type: nil,
19
+ unique: false,
20
+ null_filtered: false,
21
+ interleave_in: nil,
22
+ storing: nil,
23
+ state: nil
24
+ @table = table.to_s
25
+ @name = name.to_s
26
+ @columns = Array(columns)
27
+ @type = type
28
+ @unique = unique
29
+ @null_filtered = null_filtered
30
+ @interleave_in = interleave_in unless interleave_in.to_s.empty?
31
+ @storing = storing || []
32
+ @state = state
33
+ end
34
+
35
+ def primary?
36
+ @type == "PRIMARY_KEY"
37
+ end
38
+
39
+ def columns_by_position
40
+ @columns.select(&:ordinal_position).sort do |c1, c2|
41
+ c1.ordinal_position <=> c2.ordinal_position
42
+ end
43
+ end
44
+
45
+ def column_names
46
+ columns_by_position.map(&:name)
47
+ end
48
+
49
+ def orders
50
+ columns_by_position.each_with_object({}) do |c, r|
51
+ r[c.name] = c.desc? ? :desc : :asc
52
+ end
53
+ end
54
+
55
+ def options
56
+ {
57
+ name: name,
58
+ order: orders,
59
+ unique: unique,
60
+ interleave_in: interleave_in,
61
+ null_filtered: null_filtered,
62
+ storing: storing
63
+ }.delete_if { |_, v| v.nil? }
64
+ end
65
+
66
+ def rename_column_options old_column, new_column
67
+ opts = options
68
+
69
+ opts[:order].transform_keys do |key|
70
+ key.to_s == new_column.to_s
71
+ end
72
+
73
+ columns = column_names.map do |c|
74
+ c.to_s == old_column.to_s ? new_column : c
75
+ end
76
+
77
+ { options: opts, columns: columns }
78
+ end
79
+ end
80
+ end
@@ -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
+ require "active_record/connection_adapters/abstract/quoting"
8
+ require "activerecord_spanner_adapter/information_schema"
9
+ require "activerecord_spanner_adapter/table"
10
+ require "activerecord_spanner_adapter/index"
11
+ require "activerecord_spanner_adapter/foreign_key"
12
+
13
+
14
+ module ActiveRecordSpannerAdapter
15
+ class InformationSchema
16
+ include ActiveRecord::ConnectionAdapters::Quoting
17
+
18
+ attr_reader :connection
19
+
20
+ def initialize connection
21
+ @connection = connection
22
+ @mutex = Mutex.new
23
+ end
24
+
25
+ def tables table_name: nil, schema_name: nil, view: nil
26
+ sql = +"SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, PARENT_TABLE_NAME, ON_DELETE_ACTION"
27
+ sql << " FROM INFORMATION_SCHEMA.TABLES"
28
+ sql << " WHERE TABLE_SCHEMA=%<schema_name>s"
29
+ sql << " AND TABLE_NAME=%<table_name>s" if table_name
30
+
31
+ rows = execute_query(
32
+ sql,
33
+ schema_name: (schema_name || ""), table_name: table_name
34
+ )
35
+
36
+ rows.map do |row|
37
+ table = Table.new(
38
+ row["TABLE_NAME"],
39
+ parent_table: row["PARENT_TABLE_NAME"],
40
+ on_delete: row["ON_DELETE_ACTION"],
41
+ schema_name: row["TABLE_SCHEMA"],
42
+ catalog: row["TABLE_CATALOG"]
43
+ )
44
+
45
+ if [:full, :columns].include? view
46
+ table.columns = table_columns table.name
47
+ end
48
+
49
+ if [:full, :indexes].include? view
50
+ table.indexes = indexes table.name
51
+ end
52
+ table
53
+ end
54
+ end
55
+
56
+ def table table_name, schema_name: nil, view: nil
57
+ tables(
58
+ table_name: table_name,
59
+ schema_name: schema_name,
60
+ view: view
61
+ ).first
62
+ end
63
+
64
+ def table_columns table_name, column_name: nil
65
+ sql = +"SELECT COLUMN_NAME, SPANNER_TYPE, IS_NULLABLE, COLUMN_DEFAULT, ORDINAL_POSITION"
66
+ sql << " FROM INFORMATION_SCHEMA.COLUMNS"
67
+ sql << " WHERE TABLE_NAME=%<table_name>s"
68
+ sql << " AND COLUMN_NAME=%<column_name>s" if column_name
69
+ sql << " ORDER BY ORDINAL_POSITION ASC"
70
+
71
+ execute_query(
72
+ sql,
73
+ table_name: table_name,
74
+ column_name: column_name
75
+ ).map do |row|
76
+ type, limit = parse_type_and_limit row["SPANNER_TYPE"]
77
+ Table::Column.new \
78
+ table_name,
79
+ row["COLUMN_NAME"],
80
+ type,
81
+ limit: limit,
82
+ ordinal_position: row["ORDINAL_POSITION"],
83
+ nullable: row["IS_NULLABLE"] == "YES",
84
+ default: row["COLUMN_DEFAULT"]
85
+ end
86
+ end
87
+
88
+ def table_column table_name, column_name
89
+ table_columns(table_name, column_name: column_name).first
90
+ end
91
+
92
+ # Returns the primary key columns of the given table. By default it will only return the columns that are not part
93
+ # of the primary key of the parent table (if any). These are the columns that are considered the primary key by
94
+ # ActiveRecord. The parent primary key columns are filtered out by default to allow interleaved tables to be
95
+ # considered as tables with a single-column primary key by ActiveRecord. The actual primary key of the table will
96
+ # include both the parent primary key columns and the 'own' primary key columns of a table.
97
+ def table_primary_keys table_name, include_parent_keys = false
98
+ sql = +"WITH TABLE_PK_COLS AS ( "
99
+ sql << "SELECT C.TABLE_NAME, C.COLUMN_NAME, C.INDEX_NAME, C.COLUMN_ORDERING, C.ORDINAL_POSITION "
100
+ sql << "FROM INFORMATION_SCHEMA.INDEX_COLUMNS C "
101
+ sql << "WHERE C.INDEX_TYPE = 'PRIMARY_KEY' "
102
+ sql << "AND TABLE_CATALOG = '' "
103
+ sql << "AND TABLE_SCHEMA = '') "
104
+ sql << "SELECT INDEX_NAME, COLUMN_NAME, COLUMN_ORDERING, ORDINAL_POSITION "
105
+ sql << "FROM TABLE_PK_COLS "
106
+ sql << "INNER JOIN INFORMATION_SCHEMA.TABLES T USING (TABLE_NAME) "
107
+ sql << "WHERE TABLE_NAME = %<table_name>s "
108
+ sql << "AND TABLE_CATALOG = '' "
109
+ sql << "AND TABLE_SCHEMA = '' "
110
+ unless include_parent_keys
111
+ sql << "AND (T.PARENT_TABLE_NAME IS NULL OR COLUMN_NAME NOT IN ( "
112
+ sql << " SELECT COLUMN_NAME "
113
+ sql << " FROM TABLE_PK_COLS "
114
+ sql << " WHERE TABLE_NAME = T.PARENT_TABLE_NAME "
115
+ sql << ")) "
116
+ end
117
+ sql << "ORDER BY ORDINAL_POSITION"
118
+ execute_query(
119
+ sql,
120
+ table_name: table_name
121
+ ).map do |row|
122
+ Index::Column.new \
123
+ table_name,
124
+ row["INDEX_NAME"],
125
+ row["COLUMN_NAME"],
126
+ order: row["COLUMN_ORDERING"],
127
+ ordinal_position: row["ORDINAL_POSITION"]
128
+ end
129
+ end
130
+
131
+ def indexes table_name, index_name: nil, index_type: nil
132
+ table_indexes_columns = index_columns(
133
+ table_name,
134
+ index_name: index_name
135
+ )
136
+
137
+ sql = +"SELECT INDEX_NAME, INDEX_TYPE, IS_UNIQUE, IS_NULL_FILTERED, PARENT_TABLE_NAME, INDEX_STATE"
138
+ sql << " FROM INFORMATION_SCHEMA.INDEXES"
139
+ sql << " WHERE TABLE_NAME=%<table_name>s"
140
+ sql << " AND TABLE_CATALOG = ''"
141
+ sql << " AND TABLE_SCHEMA = ''"
142
+ sql << " AND INDEX_NAME=%<index_name>s" if index_name
143
+ sql << " AND INDEX_TYPE=%<index_type>s" if index_type
144
+ sql << " AND SPANNER_IS_MANAGED=FALSE"
145
+
146
+ execute_query(
147
+ sql,
148
+ table_name: table_name,
149
+ index_name: index_name,
150
+ index_type: index_type
151
+ ).map do |row|
152
+ columns = []
153
+ storing = []
154
+ table_indexes_columns.each do |c|
155
+ next unless c.index_name == row["INDEX_NAME"]
156
+ if c.ordinal_position
157
+ columns << c
158
+ else
159
+ storing << c.name
160
+ end
161
+ end
162
+
163
+ Index.new \
164
+ table_name,
165
+ row["INDEX_NAME"],
166
+ columns,
167
+ type: row["INDEX_TYPE"],
168
+ unique: row["IS_UNIQUE"],
169
+ null_filtered: row["IS_NULL_FILTERED"],
170
+ interleave_in: row["PARENT_TABLE_NAME"],
171
+ storing: storing,
172
+ state: row["INDEX_STATE"]
173
+ end
174
+ end
175
+
176
+ def index table_name, index_name
177
+ indexes(table_name, index_name: index_name).first
178
+ end
179
+
180
+ def index_columns table_name, index_name: nil
181
+ sql = +"SELECT INDEX_NAME, COLUMN_NAME, COLUMN_ORDERING, ORDINAL_POSITION"
182
+ sql << " FROM INFORMATION_SCHEMA.INDEX_COLUMNS"
183
+ sql << " WHERE TABLE_NAME=%<table_name>s"
184
+ sql << " AND TABLE_CATALOG = ''"
185
+ sql << " AND TABLE_SCHEMA = ''"
186
+ sql << " AND INDEX_NAME=%<index_name>s" if index_name
187
+ sql << " ORDER BY ORDINAL_POSITION ASC"
188
+
189
+ execute_query(
190
+ sql,
191
+ table_name: table_name, index_name: index_name
192
+ ).map do |row|
193
+ Index::Column.new \
194
+ table_name,
195
+ row["INDEX_NAME"],
196
+ row["COLUMN_NAME"],
197
+ order: row["COLUMN_ORDERING"],
198
+ ordinal_position: row["ORDINAL_POSITION"]
199
+ end
200
+ end
201
+
202
+ def indexes_by_columns table_name, column_names
203
+ column_names = Array(column_names).map(&:to_s)
204
+
205
+ indexes(table_name).select do |index|
206
+ index.columns.any? { |c| column_names.include? c.name }
207
+ end
208
+ end
209
+
210
+ def foreign_keys table_name
211
+ sql = <<~SQL
212
+ SELECT cc.table_name AS to_table,
213
+ cc.column_name AS primary_key,
214
+ fk.column_name as column,
215
+ fk.constraint_name AS name,
216
+ rc.update_rule AS on_update,
217
+ rc.delete_rule AS on_delete
218
+ FROM information_schema.referential_constraints rc
219
+ INNER JOIN information_schema.key_column_usage fk ON rc.constraint_name = fk.constraint_name
220
+ INNER JOIN information_schema.constraint_column_usage cc ON rc.constraint_name = cc.constraint_name
221
+ WHERE fk.table_name = %<table_name>s
222
+ AND fk.constraint_schema = %<constraint_schema>s
223
+ SQL
224
+
225
+ rows = execute_query(
226
+ sql, table_name: table_name, constraint_schema: ""
227
+ )
228
+
229
+ rows.map do |row|
230
+ ForeignKey.new(
231
+ table_name,
232
+ row["name"],
233
+ row["column"],
234
+ row["to_table"],
235
+ row["primary_key"],
236
+ on_delete: row["on_delete"],
237
+ on_update: row["on_update"]
238
+ )
239
+ end
240
+ end
241
+
242
+ def parse_type_and_limit value
243
+ matched = /^([A-Z]*)\((.*)\)/.match value
244
+ return [value] unless matched
245
+
246
+ limit = matched[2]
247
+ limit = limit.to_i unless limit == "MAX"
248
+
249
+ [matched[1], limit]
250
+ end
251
+
252
+ private
253
+
254
+ def execute_query sql, params = {}
255
+ params = params.transform_values { |v| quote v }
256
+ sql = format sql, params
257
+
258
+ @connection.execute_query(sql).rows
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,31 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ module ActiveRecord
8
+ module AttributeMethods
9
+ module PrimaryKey
10
+ module ClassMethods
11
+ def primary_and_parent_key
12
+ reset_primary_and_parent_key unless defined? @primary_and_parent_key
13
+ @primary_and_parent_key
14
+ end
15
+
16
+ def reset_primary_and_parent_key
17
+ self.primary_and_parent_key = base_class? ? fetch_primary_and_parent_key : base_class.primary_and_parent_key
18
+ end
19
+
20
+ def fetch_primary_and_parent_key
21
+ return connection.schema_cache.primary_and_parent_keys table_name \
22
+ if ActiveRecord::Base != self && table_exists?
23
+ end
24
+
25
+ def primary_and_parent_key= value
26
+ @primary_and_parent_key = value
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,59 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ module ActiveRecordSpannerAdapter
8
+ class Table
9
+ class Column
10
+ attr_accessor :table_name, :name, :type, :limit, :ordinal_position,
11
+ :allow_commit_timestamp, :default, :primary_key
12
+ attr_writer :nullable
13
+
14
+ def initialize \
15
+ table_name,
16
+ name,
17
+ type,
18
+ limit: nil,
19
+ ordinal_position: nil,
20
+ nullable: true,
21
+ allow_commit_timestamp: nil,
22
+ default: nil
23
+ @table_name = table_name.to_s
24
+ @name = name.to_s
25
+ @type = type
26
+ @limit = limit
27
+ @nullable = nullable != false
28
+ @ordinal_position = ordinal_position
29
+ @allow_commit_timestamp = allow_commit_timestamp
30
+ @default = default
31
+ @primary_key = false
32
+ end
33
+
34
+ def nullable
35
+ return false if primary_key
36
+ @nullable
37
+ end
38
+
39
+ def spanner_type
40
+ return "#{type}(#{limit || 'MAX'})" if limit_allowed?
41
+ type
42
+ end
43
+
44
+ def options
45
+ {
46
+ limit: limit,
47
+ null: nullable,
48
+ allow_commit_timestamp: allow_commit_timestamp
49
+ }.delete_if { |_, v| v.nil? }
50
+ end
51
+
52
+ private
53
+
54
+ def limit_allowed?
55
+ ["BYTES", "STRING"].include? type
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,61 @@
1
+ # The MIT License (MIT)
2
+ #
3
+ # Copyright (c) 2020 Google LLC.
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # ITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
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
21
+ # THE SOFTWARE.
22
+
23
+ # Copyright 2020 Google LLC
24
+ #
25
+ # Use of this source code is governed by an MIT-style
26
+ # license that can be found in the LICENSE file or at
27
+ # https://opensource.org/licenses/MIT.
28
+
29
+ require "activerecord_spanner_adapter/table/column"
30
+
31
+ module ActiveRecordSpannerAdapter
32
+ class Table
33
+ attr_accessor :name, :on_delete, :parent_table, :schema_name, :catalog,
34
+ :indexes, :columns, :foreign_keys
35
+
36
+ # parent_table == interleave_in
37
+ def initialize \
38
+ name,
39
+ parent_table: nil,
40
+ on_delete: nil,
41
+ schema_name: nil,
42
+ catalog: nil
43
+ @name = name.to_s
44
+ @parent_table = parent_table.to_s if parent_table
45
+ @on_delete = on_delete
46
+ @schema_name = schema_name
47
+ @catalog = catalog
48
+ @columns = []
49
+ @indexes = []
50
+ @foreign_keys = []
51
+ end
52
+
53
+ def primary_keys
54
+ columns.select(&:primary_key).map(&:name)
55
+ end
56
+
57
+ def cascade?
58
+ @on_delete == "CASCADE"
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,123 @@
1
+ # Copyright 2021 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ module ActiveRecordSpannerAdapter
8
+ class Transaction
9
+ attr_reader :state
10
+
11
+ def initialize connection, isolation
12
+ @connection = connection
13
+ @isolation = isolation
14
+ @committable = ![:read_only, :pdml].include?(isolation) && !isolation.is_a?(Hash)
15
+ @state = :INITIALIZED
16
+ @sequence_number = 0
17
+ @mutations = []
18
+ end
19
+
20
+ def active?
21
+ @state == :STARTED
22
+ end
23
+
24
+ def isolation
25
+ return nil unless active?
26
+ @isolation
27
+ end
28
+
29
+ def buffer mutation
30
+ @mutations << mutation
31
+ end
32
+
33
+ def begin
34
+ raise "Nested transactions are not allowed" if @state != :INITIALIZED
35
+ begin
36
+ @grpc_transaction =
37
+ case @isolation
38
+ when Hash
39
+ if @isolation[:timestamp]
40
+ @connection.session.create_snapshot timestamp: @isolation[:timestamp]
41
+ elsif @isolation[:staleness]
42
+ @connection.session.create_snapshot staleness: @isolation[:staleness]
43
+ elsif @isolation[:strong]
44
+ @connection.session.create_snapshot strong: true
45
+ else
46
+ raise "Invalid snapshot argument: #{@isolation}"
47
+ end
48
+ when :read_only
49
+ @connection.session.create_snapshot strong: true
50
+ when :pdml
51
+ @connection.session.create_pdml
52
+ else
53
+ @connection.session.create_transaction
54
+ end
55
+ @state = :STARTED
56
+ rescue Google::Cloud::NotFoundError => e
57
+ if @connection.session_not_found? e
58
+ @connection.reset!
59
+ retry
60
+ end
61
+ @state = :FAILED
62
+ raise
63
+ rescue StandardError
64
+ @state = :FAILED
65
+ raise
66
+ end
67
+ end
68
+
69
+ def next_sequence_number
70
+ @sequence_number += 1 if @committable
71
+ end
72
+
73
+ def commit
74
+ raise "This transaction is not active" unless active?
75
+
76
+ begin
77
+ @connection.session.commit_transaction @grpc_transaction, @mutations if @committable
78
+ @state = :COMMITTED
79
+ rescue Google::Cloud::NotFoundError => e
80
+ if @connection.session_not_found? e
81
+ shoot_and_forget_rollback
82
+ @connection.reset!
83
+ @connection.raise_aborted_err
84
+ end
85
+ @state = :FAILED
86
+ raise
87
+ rescue StandardError
88
+ @state = :FAILED
89
+ raise
90
+ end
91
+ end
92
+
93
+ def rollback
94
+ # Allow rollback after abort and/or a failed commit.
95
+ raise "This transaction is not active" unless active? || @state == :FAILED || @state == :ABORTED
96
+ if active?
97
+ # We do a shoot-and-forget rollback here, as the error that caused the transaction to be rolled back could
98
+ # also have invalidated the transaction (e.g. `Session not found`). If the rollback fails for any other
99
+ # reason, we also do not need to retry it or propagate the error to the application, as the transaction will
100
+ # automatically be aborted by Cloud Spanner after 10 seconds anyways.
101
+ shoot_and_forget_rollback
102
+ end
103
+ @state = :ROLLED_BACK
104
+ end
105
+
106
+ def shoot_and_forget_rollback
107
+ @connection.session.rollback @grpc_transaction.transaction_id if @committable
108
+ rescue StandardError # rubocop:disable Lint/HandleExceptions
109
+ # Ignored
110
+ end
111
+
112
+ def mark_aborted
113
+ @state = :ABORTED
114
+ end
115
+
116
+ def transaction_selector
117
+ return unless active?
118
+
119
+ Google::Spanner::V1::TransactionSelector.new \
120
+ id: @grpc_transaction.transaction_id
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,9 @@
1
+ # Copyright 2020 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ module ActiveRecordSpannerAdapter
8
+ VERSION = "0.7.0".freeze
9
+ end