activerecord-spanner-adapter 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (264) hide show
  1. checksums.yaml +5 -5
  2. data/.github/CODEOWNERS +7 -0
  3. data/.github/sync-repo-settings.yaml +16 -0
  4. data/.github/workflows/acceptance-tests-on-emulator.yaml +45 -0
  5. data/.github/workflows/acceptance-tests-on-production.yaml +36 -0
  6. data/.github/workflows/ci.yaml +33 -0
  7. data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +52 -0
  8. data/.github/workflows/nightly-acceptance-tests-on-production.yaml +35 -0
  9. data/.github/workflows/nightly-unit-tests.yaml +40 -0
  10. data/.github/workflows/release-please-label.yml +25 -0
  11. data/.github/workflows/release-please.yml +39 -0
  12. data/.github/workflows/rubocop.yaml +31 -0
  13. data/.gitignore +67 -5
  14. data/.kokoro/populate-secrets.sh +77 -0
  15. data/.kokoro/release.cfg +33 -0
  16. data/.kokoro/release.sh +15 -0
  17. data/.kokoro/trampoline_v2.sh +489 -0
  18. data/.rubocop.yml +46 -0
  19. data/.toys/release.rb +18 -0
  20. data/.trampolinerc +48 -0
  21. data/.yardopts +11 -0
  22. data/CHANGELOG.md +26 -0
  23. data/CODE_OF_CONDUCT.md +40 -0
  24. data/CONTRIBUTING.md +79 -0
  25. data/Gemfile +9 -4
  26. data/LICENSE +6 -6
  27. data/README.md +67 -30
  28. data/Rakefile +79 -3
  29. data/SECURITY.md +7 -0
  30. data/acceptance/cases/associations/has_many_associations_test.rb +119 -0
  31. data/acceptance/cases/associations/has_many_through_associations_test.rb +63 -0
  32. data/acceptance/cases/associations/has_one_associations_test.rb +79 -0
  33. data/acceptance/cases/associations/has_one_through_associations_test.rb +98 -0
  34. data/acceptance/cases/interleaved_associations/has_many_associations_using_interleaved_test.rb +211 -0
  35. data/acceptance/cases/migration/change_schema_test.rb +433 -0
  36. data/acceptance/cases/migration/change_table_test.rb +115 -0
  37. data/acceptance/cases/migration/column_attributes_test.rb +122 -0
  38. data/acceptance/cases/migration/column_positioning_test.rb +48 -0
  39. data/acceptance/cases/migration/columns_test.rb +201 -0
  40. data/acceptance/cases/migration/command_recorder_test.rb +406 -0
  41. data/acceptance/cases/migration/create_join_table_test.rb +216 -0
  42. data/acceptance/cases/migration/ddl_batching_test.rb +80 -0
  43. data/acceptance/cases/migration/foreign_key_test.rb +297 -0
  44. data/acceptance/cases/migration/index_test.rb +211 -0
  45. data/acceptance/cases/migration/references_foreign_key_test.rb +259 -0
  46. data/acceptance/cases/migration/references_index_test.rb +135 -0
  47. data/acceptance/cases/migration/references_statements_test.rb +166 -0
  48. data/acceptance/cases/migration/rename_column_test.rb +96 -0
  49. data/acceptance/cases/models/calculation_query_test.rb +128 -0
  50. data/acceptance/cases/models/generated_column_test.rb +126 -0
  51. data/acceptance/cases/models/mutation_test.rb +122 -0
  52. data/acceptance/cases/models/query_test.rb +147 -0
  53. data/acceptance/cases/sessions/session_not_found_test.rb +121 -0
  54. data/acceptance/cases/transactions/optimistic_locking_test.rb +141 -0
  55. data/acceptance/cases/transactions/read_only_transactions_test.rb +67 -0
  56. data/acceptance/cases/transactions/read_write_transactions_test.rb +248 -0
  57. data/acceptance/cases/type/all_types_test.rb +152 -0
  58. data/acceptance/cases/type/binary_test.rb +59 -0
  59. data/acceptance/cases/type/boolean_test.rb +31 -0
  60. data/acceptance/cases/type/date_test.rb +32 -0
  61. data/acceptance/cases/type/date_time_test.rb +30 -0
  62. data/acceptance/cases/type/float_test.rb +27 -0
  63. data/acceptance/cases/type/integer_test.rb +44 -0
  64. data/acceptance/cases/type/numeric_test.rb +27 -0
  65. data/acceptance/cases/type/string_test.rb +79 -0
  66. data/acceptance/cases/type/text_test.rb +30 -0
  67. data/acceptance/cases/type/time_test.rb +87 -0
  68. data/acceptance/models/account.rb +13 -0
  69. data/acceptance/models/address.rb +9 -0
  70. data/acceptance/models/album.rb +12 -0
  71. data/acceptance/models/all_types.rb +8 -0
  72. data/acceptance/models/author.rb +11 -0
  73. data/acceptance/models/club.rb +12 -0
  74. data/acceptance/models/comment.rb +9 -0
  75. data/acceptance/models/customer.rb +9 -0
  76. data/acceptance/models/department.rb +9 -0
  77. data/acceptance/models/firm.rb +10 -0
  78. data/acceptance/models/member.rb +13 -0
  79. data/acceptance/models/member_type.rb +9 -0
  80. data/acceptance/models/membership.rb +10 -0
  81. data/acceptance/models/organization.rb +9 -0
  82. data/acceptance/models/post.rb +10 -0
  83. data/acceptance/models/singer.rb +10 -0
  84. data/acceptance/models/track.rb +20 -0
  85. data/acceptance/models/transaction.rb +9 -0
  86. data/acceptance/schema/schema.rb +143 -0
  87. data/acceptance/test_helper.rb +260 -0
  88. data/activerecord-spanner-adapter.gemspec +32 -17
  89. data/assets/solidus-db.png +0 -0
  90. data/benchmarks/README.md +17 -0
  91. data/benchmarks/Rakefile +14 -0
  92. data/benchmarks/application.rb +308 -0
  93. data/benchmarks/config/database.yml +8 -0
  94. data/benchmarks/config/environment.rb +12 -0
  95. data/benchmarks/db/migrate/01_create_tables.rb +25 -0
  96. data/benchmarks/db/schema.rb +29 -0
  97. data/benchmarks/models/album.rb +9 -0
  98. data/benchmarks/models/singer.rb +9 -0
  99. data/bin/console +6 -7
  100. data/examples/rails/README.md +262 -0
  101. data/examples/snippets/README.md +29 -0
  102. data/examples/snippets/Rakefile +57 -0
  103. data/examples/snippets/array-data-type/README.md +45 -0
  104. data/examples/snippets/array-data-type/Rakefile +13 -0
  105. data/examples/snippets/array-data-type/application.rb +45 -0
  106. data/examples/snippets/array-data-type/config/database.yml +8 -0
  107. data/examples/snippets/array-data-type/db/migrate/01_create_tables.rb +24 -0
  108. data/examples/snippets/array-data-type/db/schema.rb +26 -0
  109. data/examples/snippets/array-data-type/db/seeds.rb +5 -0
  110. data/examples/snippets/array-data-type/models/entity_with_array_types.rb +18 -0
  111. data/examples/snippets/bin/create_emulator_instance.rb +18 -0
  112. data/examples/snippets/bulk-insert/README.md +21 -0
  113. data/examples/snippets/bulk-insert/Rakefile +13 -0
  114. data/examples/snippets/bulk-insert/application.rb +64 -0
  115. data/examples/snippets/bulk-insert/config/database.yml +8 -0
  116. data/examples/snippets/bulk-insert/db/migrate/01_create_tables.rb +21 -0
  117. data/examples/snippets/bulk-insert/db/schema.rb +26 -0
  118. data/examples/snippets/bulk-insert/db/seeds.rb +5 -0
  119. data/examples/snippets/bulk-insert/models/album.rb +9 -0
  120. data/examples/snippets/bulk-insert/models/singer.rb +9 -0
  121. data/examples/snippets/commit-timestamp/README.md +18 -0
  122. data/examples/snippets/commit-timestamp/Rakefile +13 -0
  123. data/examples/snippets/commit-timestamp/application.rb +53 -0
  124. data/examples/snippets/commit-timestamp/config/database.yml +8 -0
  125. data/examples/snippets/commit-timestamp/db/migrate/01_create_tables.rb +26 -0
  126. data/examples/snippets/commit-timestamp/db/schema.rb +29 -0
  127. data/examples/snippets/commit-timestamp/db/seeds.rb +5 -0
  128. data/examples/snippets/commit-timestamp/models/album.rb +9 -0
  129. data/examples/snippets/commit-timestamp/models/singer.rb +9 -0
  130. data/examples/snippets/config/environment.rb +21 -0
  131. data/examples/snippets/create-records/README.md +12 -0
  132. data/examples/snippets/create-records/Rakefile +13 -0
  133. data/examples/snippets/create-records/application.rb +42 -0
  134. data/examples/snippets/create-records/config/database.yml +8 -0
  135. data/examples/snippets/create-records/db/migrate/01_create_tables.rb +21 -0
  136. data/examples/snippets/create-records/db/schema.rb +26 -0
  137. data/examples/snippets/create-records/db/seeds.rb +5 -0
  138. data/examples/snippets/create-records/models/album.rb +9 -0
  139. data/examples/snippets/create-records/models/singer.rb +9 -0
  140. data/examples/snippets/date-data-type/README.md +19 -0
  141. data/examples/snippets/date-data-type/Rakefile +13 -0
  142. data/examples/snippets/date-data-type/application.rb +35 -0
  143. data/examples/snippets/date-data-type/config/database.yml +8 -0
  144. data/examples/snippets/date-data-type/db/migrate/01_create_tables.rb +20 -0
  145. data/examples/snippets/date-data-type/db/schema.rb +21 -0
  146. data/examples/snippets/date-data-type/db/seeds.rb +16 -0
  147. data/examples/snippets/date-data-type/models/singer.rb +8 -0
  148. data/examples/snippets/generated-column/README.md +41 -0
  149. data/examples/snippets/generated-column/Rakefile +13 -0
  150. data/examples/snippets/generated-column/application.rb +37 -0
  151. data/examples/snippets/generated-column/config/database.yml +8 -0
  152. data/examples/snippets/generated-column/db/migrate/01_create_tables.rb +23 -0
  153. data/examples/snippets/generated-column/db/schema.rb +21 -0
  154. data/examples/snippets/generated-column/db/seeds.rb +18 -0
  155. data/examples/snippets/generated-column/models/singer.rb +8 -0
  156. data/examples/snippets/interleaved-tables/README.md +152 -0
  157. data/examples/snippets/interleaved-tables/Rakefile +13 -0
  158. data/examples/snippets/interleaved-tables/application.rb +109 -0
  159. data/examples/snippets/interleaved-tables/config/database.yml +8 -0
  160. data/examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb +44 -0
  161. data/examples/snippets/interleaved-tables/db/schema.rb +32 -0
  162. data/examples/snippets/interleaved-tables/db/seeds.rb +40 -0
  163. data/examples/snippets/interleaved-tables/models/album.rb +15 -0
  164. data/examples/snippets/interleaved-tables/models/singer.rb +20 -0
  165. data/examples/snippets/interleaved-tables/models/track.rb +25 -0
  166. data/examples/snippets/migrations/README.md +43 -0
  167. data/examples/snippets/migrations/Rakefile +13 -0
  168. data/examples/snippets/migrations/application.rb +26 -0
  169. data/examples/snippets/migrations/config/database.yml +8 -0
  170. data/examples/snippets/migrations/db/migrate/01_create_tables.rb +28 -0
  171. data/examples/snippets/migrations/db/schema.rb +33 -0
  172. data/examples/snippets/migrations/db/seeds.rb +5 -0
  173. data/examples/snippets/migrations/models/album.rb +10 -0
  174. data/examples/snippets/migrations/models/singer.rb +10 -0
  175. data/examples/snippets/migrations/models/track.rb +9 -0
  176. data/examples/snippets/mutations/README.md +34 -0
  177. data/examples/snippets/mutations/Rakefile +13 -0
  178. data/examples/snippets/mutations/application.rb +47 -0
  179. data/examples/snippets/mutations/config/database.yml +8 -0
  180. data/examples/snippets/mutations/db/migrate/01_create_tables.rb +22 -0
  181. data/examples/snippets/mutations/db/schema.rb +27 -0
  182. data/examples/snippets/mutations/db/seeds.rb +25 -0
  183. data/examples/snippets/mutations/models/album.rb +9 -0
  184. data/examples/snippets/mutations/models/singer.rb +9 -0
  185. data/examples/snippets/optimistic-locking/README.md +12 -0
  186. data/examples/snippets/optimistic-locking/Rakefile +13 -0
  187. data/examples/snippets/optimistic-locking/application.rb +48 -0
  188. data/examples/snippets/optimistic-locking/config/database.yml +8 -0
  189. data/examples/snippets/optimistic-locking/db/migrate/01_create_tables.rb +26 -0
  190. data/examples/snippets/optimistic-locking/db/schema.rb +29 -0
  191. data/examples/snippets/optimistic-locking/db/seeds.rb +25 -0
  192. data/examples/snippets/optimistic-locking/models/album.rb +9 -0
  193. data/examples/snippets/optimistic-locking/models/singer.rb +9 -0
  194. data/examples/snippets/quickstart/README.md +26 -0
  195. data/examples/snippets/quickstart/Rakefile +13 -0
  196. data/examples/snippets/quickstart/application.rb +51 -0
  197. data/examples/snippets/quickstart/config/database.yml +8 -0
  198. data/examples/snippets/quickstart/db/migrate/01_create_tables.rb +21 -0
  199. data/examples/snippets/quickstart/db/schema.rb +26 -0
  200. data/examples/snippets/quickstart/db/seeds.rb +24 -0
  201. data/examples/snippets/quickstart/models/album.rb +9 -0
  202. data/examples/snippets/quickstart/models/singer.rb +9 -0
  203. data/examples/snippets/read-only-transactions/README.md +13 -0
  204. data/examples/snippets/read-only-transactions/Rakefile +13 -0
  205. data/examples/snippets/read-only-transactions/application.rb +49 -0
  206. data/examples/snippets/read-only-transactions/config/database.yml +8 -0
  207. data/examples/snippets/read-only-transactions/db/migrate/01_create_tables.rb +21 -0
  208. data/examples/snippets/read-only-transactions/db/schema.rb +26 -0
  209. data/examples/snippets/read-only-transactions/db/seeds.rb +24 -0
  210. data/examples/snippets/read-only-transactions/models/album.rb +9 -0
  211. data/examples/snippets/read-only-transactions/models/singer.rb +9 -0
  212. data/examples/snippets/read-write-transactions/README.md +12 -0
  213. data/examples/snippets/read-write-transactions/Rakefile +13 -0
  214. data/examples/snippets/read-write-transactions/application.rb +39 -0
  215. data/examples/snippets/read-write-transactions/config/database.yml +8 -0
  216. data/examples/snippets/read-write-transactions/db/migrate/01_create_tables.rb +22 -0
  217. data/examples/snippets/read-write-transactions/db/schema.rb +27 -0
  218. data/examples/snippets/read-write-transactions/db/seeds.rb +25 -0
  219. data/examples/snippets/read-write-transactions/models/album.rb +9 -0
  220. data/examples/snippets/read-write-transactions/models/singer.rb +9 -0
  221. data/examples/snippets/timestamp-data-type/README.md +17 -0
  222. data/examples/snippets/timestamp-data-type/Rakefile +13 -0
  223. data/examples/snippets/timestamp-data-type/application.rb +42 -0
  224. data/examples/snippets/timestamp-data-type/config/database.yml +8 -0
  225. data/examples/snippets/timestamp-data-type/db/migrate/01_create_tables.rb +21 -0
  226. data/examples/snippets/timestamp-data-type/db/schema.rb +21 -0
  227. data/examples/snippets/timestamp-data-type/db/seeds.rb +6 -0
  228. data/examples/snippets/timestamp-data-type/models/meeting.rb +19 -0
  229. data/examples/solidus/README.md +172 -0
  230. data/lib/active_record/connection_adapters/spanner/database_statements.rb +224 -269
  231. data/lib/active_record/connection_adapters/spanner/quoting.rb +42 -50
  232. data/lib/active_record/connection_adapters/spanner/schema_cache.rb +43 -0
  233. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +125 -9
  234. data/lib/active_record/connection_adapters/spanner/schema_definitions.rb +122 -0
  235. data/lib/active_record/connection_adapters/spanner/schema_dumper.rb +19 -0
  236. data/lib/active_record/connection_adapters/spanner/schema_statements.rb +553 -139
  237. data/lib/active_record/connection_adapters/spanner/type_metadata.rb +37 -0
  238. data/lib/active_record/connection_adapters/spanner_adapter.rb +182 -78
  239. data/lib/active_record/tasks/spanner_database_tasks.rb +74 -0
  240. data/lib/active_record/type/spanner/array.rb +32 -0
  241. data/lib/active_record/type/spanner/bytes.rb +26 -0
  242. data/lib/active_record/type/spanner/spanner_active_record_converter.rb +32 -0
  243. data/lib/active_record/type/spanner/time.rb +37 -0
  244. data/lib/activerecord-spanner-adapter.rb +23 -0
  245. data/lib/activerecord_spanner_adapter/base.rb +217 -0
  246. data/lib/activerecord_spanner_adapter/connection.rb +324 -0
  247. data/lib/activerecord_spanner_adapter/errors.rb +13 -0
  248. data/lib/activerecord_spanner_adapter/foreign_key.rb +29 -0
  249. data/lib/activerecord_spanner_adapter/index/column.rb +38 -0
  250. data/lib/activerecord_spanner_adapter/index.rb +80 -0
  251. data/lib/activerecord_spanner_adapter/information_schema.rb +261 -0
  252. data/lib/activerecord_spanner_adapter/primary_key.rb +31 -0
  253. data/lib/activerecord_spanner_adapter/table/column.rb +59 -0
  254. data/lib/activerecord_spanner_adapter/table.rb +61 -0
  255. data/lib/activerecord_spanner_adapter/transaction.rb +113 -0
  256. data/lib/activerecord_spanner_adapter/version.rb +9 -0
  257. data/lib/arel/visitors/spanner.rb +35 -0
  258. data/lib/spanner_client_ext.rb +82 -0
  259. data/renovate.json +5 -0
  260. metadata +387 -34
  261. data/.travis.yml +0 -5
  262. data/lib/active_record/connection_adapters/spanner/client.rb +0 -190
  263. data/lib/active_record/connection_adapters/spanner.rb +0 -10
  264. data/lib/activerecord-spanner-adapter/version.rb +0 -3
@@ -1,66 +1,58 @@
1
- # -*- frozen_string_literal: true -*-
2
- require 'json'
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
+ # frozen_string_literal: true
3
30
 
4
31
  module ActiveRecord
5
32
  module ConnectionAdapters
6
33
  module Spanner
7
34
  module Quoting
8
- IDENTIFIERS_PATTERN = /\A[a-zA-Z][a-zA-Z0-9_]*\z/
9
-
10
- def quote_identifier(name)
11
- # https://cloud.google.com/spanner/docs/data-definition-language?hl=ja#ddl_syntax
12
- # raise ArgumentError, "invalid table name #{name}" unless IDENTIFIERS_PATTERN =~ name
13
- "`#{name}`"
35
+ def quote_column_name name
36
+ self.class.quoted_column_names[name] ||= "`#{super.gsub '`', '``'}`".freeze
14
37
  end
15
38
 
16
- alias quote_table_name quote_identifier
17
- alias quote_column_name quote_identifier
18
-
19
- private
20
- def _type_cast(value)
21
- # NOTE: Spanner APIs are strongly typed unlike typical SQL interfaces.
22
- # So we don't want to serialize the value into string unlike other adapters.
23
- case value
24
- when Symbol, ActiveSupport::Multibyte::Chars, Type::Binary::Data
25
- value.to_s
26
- else
27
- value
28
- end
39
+ def quote_table_name name
40
+ self.class.quoted_table_names[name] ||= super.gsub(".", "`.`").freeze
29
41
  end
30
42
 
31
- def _quote(value)
32
- case value
33
- when Symbol, String, ActiveSupport::Multibyte::Chars, Type::Binary::Data
34
- quote_string(value.to_s)
35
- when true
36
- quoted_true
37
- when false
38
- quoted_false
39
- when nil
40
- 'NULL'
41
- when Numeric, ActiveSupport::Duration
42
- value.to_s
43
- when Type::Time::Value
44
- %Q["#{quoted_time(value)}"]
45
- when Date, Time
46
- %Q["#{quoted_date(value)}"]
47
- else
48
- raise TypeError, "can't quote #{value.class.name}"
49
- end
50
- end
43
+ STR_ESCAPE_REGX = /[\n\r'\\]/.freeze
44
+ STR_ESCAPE_VALUES = {
45
+ "\n" => "\\n", "\r" => "\\r", "'" => "\\'", "\\" => "\\\\"
46
+ }.freeze
51
47
 
52
- def quote_string(value)
53
- # Not sure but string-escape syntax in SELECT statements in Spanner
54
- # looks to be the one in JSON by observation.
55
- JSON.generate(value)
56
- end
48
+ private_constant :STR_ESCAPE_REGX, :STR_ESCAPE_VALUES
57
49
 
58
- def quoted_true
59
- 'true'
50
+ def quote_string s
51
+ s.gsub STR_ESCAPE_REGX, STR_ESCAPE_VALUES
60
52
  end
61
53
 
62
- def quoted_false
63
- 'false'
54
+ def quoted_binary value
55
+ "b'#{value}'"
64
56
  end
65
57
  end
66
58
  end
@@ -0,0 +1,43 @@
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 ConnectionAdapters
9
+ class SpannerSchemaCache < SchemaCache
10
+ def initialize conn
11
+ @primary_and_parent_keys = {}
12
+ super
13
+ end
14
+
15
+ def initialize_dup other
16
+ @primary_and_parent_keys = @primary_and_parent_keys.dup
17
+ super
18
+ end
19
+
20
+ def encode_with coder
21
+ coder["primary_and_parent_keys"] = @primary_and_parent_keys
22
+ super
23
+ end
24
+
25
+ def init_with coder
26
+ @primary_and_parent_keys = coder["primary_and_parent_keys"]
27
+ super
28
+ end
29
+
30
+ def primary_and_parent_keys table_name
31
+ @primary_and_parent_keys[table_name] ||=
32
+ if data_source_exists? table_name
33
+ connection.primary_and_parent_keys table_name
34
+ end
35
+ end
36
+
37
+ def clear!
38
+ @primary_and_parent_keys.clear
39
+ super
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,22 +1,138 @@
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
  module ActiveRecord
2
8
  module ConnectionAdapters
3
9
  module Spanner
4
- class DDL < String; end
10
+ class SchemaCreation < SchemaCreation
11
+ private
12
+
13
+ # rubocop:disable Naming/MethodName, Metrics/AbcSize
14
+
15
+ def visit_TableDefinition o
16
+ create_sql = +"CREATE TABLE #{quote_table_name o.name} "
17
+ statements = o.columns.map { |c| accept c }
18
+
19
+ o.foreign_keys.each do |to_table, options|
20
+ statements << foreign_key_in_create(o.name, to_table, options)
21
+ end
22
+
23
+ create_sql << "(#{statements.join ', '}) " if statements.any?
24
+
25
+ primary_keys = if o.primary_keys
26
+ o.primary_keys
27
+ else
28
+ pk_names = o.columns.each_with_object [] do |c, r|
29
+ if c.type == :primary_key || c.primary_key?
30
+ r << c.name
31
+ end
32
+ end
33
+ PrimaryKeyDefinition.new pk_names
34
+ end
35
+
36
+ if o.interleave_in?
37
+ parent_names = o.columns.each_with_object [] do |c, r|
38
+ if c.type == :parent_key
39
+ r << c.name
40
+ end
41
+ end
42
+ primary_keys.name = parent_names.concat primary_keys.name
43
+ create_sql << accept(primary_keys)
44
+ create_sql << ", INTERLEAVE IN PARENT #{quote_table_name o.interleave_in_parent}"
45
+ create_sql << " ON DELETE #{o.on_delete}" if o.on_delete
46
+ else
47
+ create_sql << accept(primary_keys)
48
+ end
49
+
50
+ create_sql
51
+ end
52
+
53
+ def visit_DropTableDefinition o
54
+ "DROP TABLE #{quote_table_name o.name}"
55
+ end
56
+
57
+ def visit_ColumnDefinition o
58
+ o.sql_type = type_to_sql o.type, **o.options
59
+ column_sql = +"#{quote_column_name o.name} #{o.sql_type}"
60
+ add_column_options! column_sql, column_options(o)
61
+ column_sql
62
+ end
63
+
64
+ def visit_AddColumnDefinition o
65
+ # Overridden to add the optional COLUMN keyword. The keyword is only optional
66
+ # on real Cloud Spanner, the emulator requires the COLUMN keyword to be included.
67
+ +"ADD COLUMN #{accept o.column}"
68
+ end
69
+
70
+ def visit_DropColumnDefinition o
71
+ "ALTER TABLE #{quote_table_name o.table_name} DROP" \
72
+ " COLUMN #{quote_column_name o.name}"
73
+ end
5
74
 
6
- class SchemaCreation < AbstractAdapter::SchemaCreation
7
- def visit_TableDefinition(o)
8
- pk = o.columns.find {|c| c.options[:primary_key] }
9
- ddl = "#{super} PRIMARY KEY (#{quote_column_name(pk.name)})"
10
- DDL.new(ddl)
75
+ def visit_ChangeColumnDefinition o
76
+ sql = +"ALTER TABLE #{quote_table_name o.table_name} ALTER COLUMN "
77
+ sql << accept(o.column)
78
+ sql
11
79
  end
12
80
 
13
- def add_column_options!(sql, options)
14
- if options[:null] == false
81
+ def visit_DropIndexDefinition o
82
+ "DROP INDEX #{quote_table_name o.name}"
83
+ end
84
+
85
+ def visit_IndexDefinition o
86
+ sql = +"CREATE"
87
+ sql << " UNIQUE" if o.unique
88
+ sql << " NULL_FILTERED" if o.null_filtered
89
+ sql << " INDEX #{quote_table_name o.name} "
90
+
91
+ columns_sql = o.columns_with_order.map do |c, order|
92
+ order_sql = +quote_column_name(c)
93
+ order_sql << " DESC" if order == "DESC"
94
+ order_sql
95
+ end
96
+
97
+ sql << "ON #{quote_table_name o.table} (#{columns_sql.join ', '})"
98
+
99
+ if o.storing.any?
100
+ storing = o.storing.map { |s| quote_column_name s }
101
+ sql << " STORING (#{storing.join ', '})"
102
+ end
103
+ if o.interleave_in
104
+ sql << ", INTERLEAVE IN #{quote_table_name o.interleave_in}"
105
+ end
106
+ sql
107
+ end
108
+
109
+ # rubocop:enable Naming/MethodName, Metrics/AbcSize
110
+
111
+ def add_column_options! sql, options
112
+ if options[:null] == false || options[:primary_key] == true
15
113
  sql << " NOT NULL"
16
114
  end
115
+
116
+ if !options[:allow_commit_timestamp].nil? &&
117
+ options[:column].sql_type == "TIMESTAMP"
118
+ sql << " OPTIONS (allow_commit_timestamp = "\
119
+ "#{options[:allow_commit_timestamp]})"
120
+ end
121
+
122
+ if (as = options[:as])
123
+ sql << " AS (#{as})"
124
+
125
+ sql << " STORED" if options[:stored]
126
+ unless options[:stored]
127
+ raise ArgumentError, "" \
128
+ "Cloud Spanner currently does not support generated columns without the STORED option." \
129
+ "Specify 'stored: true' option for `#{options[:column].name}`"
130
+ end
131
+ end
132
+
133
+ sql
17
134
  end
18
135
  end
19
136
  end
20
137
  end
21
138
  end
22
-
@@ -0,0 +1,122 @@
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 ConnectionAdapters #:nodoc:
9
+ module Spanner
10
+ class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
11
+ attr_reader :interleave_in_parent
12
+
13
+ def interleave_in parent, on_delete = nil
14
+ @interleave_in_parent = parent
15
+ @on_delete = on_delete
16
+ end
17
+
18
+ def parent_key name
19
+ column name, :parent_key, null: false
20
+ end
21
+
22
+ def interleave_in?
23
+ @interleave_in_parent != nil
24
+ end
25
+
26
+ def on_delete
27
+ "CASCADE" if @on_delete == :cascade
28
+ end
29
+
30
+ def references *args, **options
31
+ args.each do |ref_name|
32
+ Spanner::ReferenceDefinition.new(ref_name, **options).add_to(self)
33
+ end
34
+ end
35
+ alias belongs_to references
36
+ end
37
+
38
+ class Table < ActiveRecord::ConnectionAdapters::Table
39
+ def primary_key name, type = :primary_key, **options
40
+ type = :string # rubocop:disable Lint/ShadowedArgument
41
+ options.merge primary_key: true
42
+ super
43
+ end
44
+ end
45
+
46
+ DropTableDefinition = Struct.new :name, :options
47
+ DropColumnDefinition = Struct.new :table_name, :name
48
+ ChangeColumnDefinition = Struct.new :table_name, :column, :name
49
+ DropIndexDefinition = Struct.new :name
50
+
51
+ class ReferenceDefinition < ActiveRecord::ConnectionAdapters::ReferenceDefinition
52
+ def initialize \
53
+ name,
54
+ polymorphic: false,
55
+ index: true,
56
+ foreign_key: false,
57
+ type: :integer,
58
+ **options
59
+ @name = name
60
+ @polymorphic = polymorphic
61
+ @foreign_key = foreign_key
62
+ # Only add an index if there is no foreign key, as Cloud Spanner will automatically add a managed index when
63
+ # a foreign key is added.
64
+ @index = index unless foreign_key
65
+ @type = type
66
+ @options = options
67
+
68
+ return unless polymorphic && foreign_key
69
+ raise ArgumentError, "Cannot add a foreign key to a polymorphic relation"
70
+ end
71
+
72
+ private
73
+
74
+ def columns
75
+ result = [[column_name, type, options]]
76
+
77
+ if polymorphic
78
+ type_options = polymorphic_options.merge limit: 255
79
+ result.unshift ["#{name}_type", :string, type_options]
80
+ end
81
+ result
82
+ end
83
+ end
84
+
85
+ class IndexDefinition < ActiveRecord::ConnectionAdapters::IndexDefinition
86
+ attr_reader :null_filtered, :interleave_in, :storing, :orders
87
+
88
+ def initialize \
89
+ table_name,
90
+ name,
91
+ columns,
92
+ unique: false,
93
+ null_filtered: false,
94
+ interleave_in: nil,
95
+ storing: nil,
96
+ orders: nil
97
+ @table = table_name
98
+ @name = name
99
+ @unique = unique
100
+ @null_filtered = null_filtered
101
+ @interleave_in = interleave_in
102
+ @storing = Array(storing)
103
+ columns = columns.split(/\W/) if columns.is_a? String
104
+ @columns = Array(columns).map(&:to_s)
105
+ @orders = orders || {}
106
+
107
+ unless @orders.is_a? Hash
108
+ @orders = columns.each_with_object({}) { |c, r| r[c] = orders }
109
+ end
110
+
111
+ @orders = @orders.symbolize_keys
112
+ end
113
+
114
+ def columns_with_order
115
+ columns.each_with_object({}) do |c, result|
116
+ result[c] = orders[c.to_sym].to_s.upcase
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,19 @@
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
+ module ActiveRecord
10
+ module ConnectionAdapters
11
+ module Spanner
12
+ class SchemaDumper < ConnectionAdapters::SchemaDumper # :nodoc:
13
+ def default_primary_key? column
14
+ schema_type(column) == :integer
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,173 +1,587 @@
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 "active_record/connection_adapters/spanner/schema_creation"
10
+ require "active_record/connection_adapters/spanner/schema_dumper"
11
+
1
12
  module ActiveRecord
2
13
  module ConnectionAdapters
3
14
  module Spanner
15
+ #
16
+ # # SchemaStatements
17
+ #
18
+ # Collection of methods to handle database schema.
19
+ #
20
+ # [Schema Doc](https://cloud.google.com/spanner/docs/information-schema)
21
+ #
4
22
  module SchemaStatements
5
- include ConnectionAdapters::SchemaStatements
6
-
7
- NATIVE_DATABASE_TYPES = {
8
- primary_key: 'STRING(36)',
9
- string: { name: 'STRING', limit: 255 },
10
- text: { name: 'STRING', limit: 'MAX' },
11
- integer: { name: 'INT64' },
12
- float: { name: 'FLOAT64' },
13
- datetime: { name: 'TIMESTAMP' },
14
- date: { name: 'DATE' },
15
- binary: { name: 'BYTES', limit: 'MAX' },
16
- boolean: { name: 'BOOL' },
17
- }
18
-
19
- def native_database_types # :nodoc:
20
- NATIVE_DATABASE_TYPES
21
- end
22
-
23
- def tables
24
- # https://cloud.google.com/spanner/docs/information-schema
25
- select_values(<<-SQL, 'SCHEMA')
26
- SELECT
27
- t.table_name
28
- FROM
29
- information_schema.tables AS t
30
- WHERE
31
- t.table_catalog = '' AND t.table_schema = ''
32
- SQL
33
- end
34
-
35
- def views
36
- []
37
- end
38
-
39
- def indexes(table, name = :ignored)
40
- params = {table: table}
41
- results = exec_query(<<-"SQL", 'SCHEMA', params, prepare: false)
42
- SELECT
43
- idx.index_name,
44
- idx.index_type,
45
- idx.parent_table_name,
46
- idx.is_unique,
47
- idx.is_null_filtered
48
- FROM
49
- information_schema.indexes AS idx
50
- WHERE
51
- idx.table_catalog = '' AND
52
- idx.table_schema = '' AND
53
- idx.table_name = @table
54
- SQL
55
-
56
- results.map do |row|
57
- col_params = { table: table, index: row['index_name'] }
58
- col_results = exec_query(<<-"SQL", 'SCHEMA', col_params, prepare: false)
59
- SELECT
60
- col.column_name,
61
- col.column_ordering
62
- FROM
63
- information_schema.index_columns AS col
64
- WHERE
65
- col.table_catalog = '' AND
66
- col.table_schema = '' AND
67
- col.table_name = @table AND
68
- col.index_name = @index
69
- ORDER BY
70
- col.ordinal_position
71
- SQL
23
+ VERSION_6_1_0 = Gem::Version.create "6.1.0"
24
+ VERSION_6_0_3 = Gem::Version.create "6.0.3"
72
25
 
73
- IndexDefinition.new(
74
- table,
75
- row['index_name'],
76
- row['is_unique'],
77
- col_results.map {|row| row['column_name'] },
78
- nil, # length
79
- col_results.map {|row| row['column_ordering'] },
80
- nil, # where
81
- row['index_type'],
26
+ def current_database
27
+ @connection.database_id
28
+ end
29
+
30
+ # Table
31
+
32
+ def data_sources
33
+ information_schema { |i| i.tables.map(&:name) }
34
+ end
35
+ alias tables data_sources
36
+
37
+ def table_exists? table_name
38
+ information_schema { |i| i.table table_name }.present?
39
+ end
40
+ alias data_source_exists? table_exists?
41
+
42
+ def create_table table_name, **options
43
+ td = create_table_definition table_name, options
44
+
45
+ if options[:id] != false
46
+ pk = options.fetch :primary_key do
47
+ Base.get_primary_key table_name.to_s.singularize
48
+ end
49
+
50
+ if pk.is_a? Array
51
+ td.primary_keys pk
52
+ else
53
+ td.primary_key pk, options.fetch(:id, :primary_key), **{}
54
+ end
55
+ end
56
+
57
+ yield td if block_given?
58
+
59
+ statements = []
60
+
61
+ if options[:force]
62
+ statements.concat drop_table_with_indexes_sql(table_name, options)
63
+ end
64
+
65
+ statements << schema_creation.accept(td)
66
+
67
+ td.indexes.each do |column_name, index_options|
68
+ id = create_index_definition table_name, column_name, **index_options
69
+ statements << schema_creation.accept(id)
70
+ end
71
+
72
+ execute_schema_statements statements
73
+ end
74
+
75
+ def drop_table table_name, options = {}
76
+ statements = drop_table_with_indexes_sql table_name, options
77
+ execute_schema_statements statements
78
+ end
79
+
80
+ # Creates a join table that uses all the columns in the table as the primary key by default, unless
81
+ # an explicit primary key has been defined for the table. ActiveRecord will by default generate join
82
+ # tables without a primary key. Cloud Spanner however requires all tables to have a primary key.
83
+ # Instead of adding an additional column to the table only for the purpose of being the primary key,
84
+ # the Spanner ActiveRecord adapter defines a primary key that contains all the columns in the join
85
+ # table, as all values in the table should be unique anyways.
86
+ def create_join_table table_1, table_2, column_options: {}, **options
87
+ super do |td|
88
+ unless td.columns.any?(&:primary_key?)
89
+ td.columns.each do |col|
90
+ def col.primary_key?
91
+ true
92
+ end
93
+ end
94
+ end
95
+ yield td if block_given?
96
+ end
97
+ end
98
+
99
+ def rename_table _table_name, _new_name
100
+ raise ActiveRecordSpannerAdapter::NotSupportedError, \
101
+ "rename_table is not implemented"
102
+ end
103
+
104
+ # Column
105
+
106
+ def column_definitions table_name
107
+ information_schema { |i| i.table_columns table_name }
108
+ end
109
+
110
+ def new_column_from_field _table_name, field
111
+ ConnectionAdapters::Column.new \
112
+ field.name,
113
+ field.default,
114
+ fetch_type_metadata(field.spanner_type, field.ordinal_position),
115
+ field.nullable
116
+ end
117
+
118
+ def fetch_type_metadata sql_type, ordinal_position = nil
119
+ Spanner::TypeMetadata.new \
120
+ super(sql_type), ordinal_position: ordinal_position
121
+ end
122
+
123
+ def add_column table_name, column_name, type, **options
124
+ # Add column with NOT NULL not supported by spanner.
125
+ # It is currently un-implemented state in spanner service.
126
+ nullable = options.delete(:null) == false
127
+
128
+ at = create_alter_table table_name
129
+ at.add_column column_name, type, **options
130
+
131
+ statements = [schema_creation.accept(at)]
132
+
133
+ # Alter NOT NULL
134
+ if nullable
135
+ cd = at.adds.first.column
136
+ cd.null = false
137
+ ccd = Spanner::ChangeColumnDefinition.new(
138
+ table_name, cd, column_name
82
139
  )
140
+ statements << schema_creation.accept(ccd)
141
+ end
142
+
143
+ execute_schema_statements statements
144
+ end
145
+
146
+ def remove_column table_name, column_name, _type = nil, _options = {}
147
+ statements = drop_column_sql table_name, column_name
148
+ execute_schema_statements statements
149
+ end
150
+
151
+ if ActiveRecord.gem_version < VERSION_6_1_0
152
+ def remove_columns table_name, *column_names
153
+ _remove_columns table_name, *column_names
154
+ end
155
+ else
156
+ def remove_columns table_name, *column_names, _type: nil, **_options
157
+ _remove_columns table_name, *column_names
83
158
  end
84
159
  end
85
160
 
86
- def columns(table)
87
- params = {table: table}
88
- results = exec_query(<<-'SQL', 'SCHEMA', params, prepare: false)
89
- SELECT
90
- col.column_name,
91
- col.column_default,
92
- col.is_nullable,
93
- col.spanner_type
94
- FROM
95
- information_schema.columns AS col
96
- WHERE
97
- col.table_catalog = '' AND
98
- col.table_schema = '' AND
99
- col.table_name = @table
100
- ORDER BY
101
- col.ordinal_position
102
- SQL
103
-
104
- results.map do |row|
105
- Column.new(
106
- row['column_name'],
107
- row['column_default'],
108
- fetch_type_metadata(row['spanner_type']),
109
- row['is_nullable'],
110
- table,
161
+ def _remove_columns table_name, *column_names
162
+ if column_names.empty?
163
+ raise ArgumentError, "You must specify at least one column name. "\
164
+ "Example: remove_columns(:people, :first_name)"
165
+ end
166
+
167
+ statements = []
168
+
169
+ column_names.each do |column_name|
170
+ statements.concat drop_column_sql(table_name, column_name)
171
+ end
172
+
173
+ execute_schema_statements statements
174
+ end
175
+
176
+ if ActiveRecord.gem_version < VERSION_6_1_0
177
+ def change_column table_name, column_name, type, options = {}
178
+ _change_column table_name, column_name, type, **options
179
+ end
180
+ else
181
+ def change_column table_name, column_name, type, **options
182
+ _change_column table_name, column_name, type, **options
183
+ end
184
+ end
185
+
186
+ def change_column_null table_name, column_name, null, _default = nil
187
+ change_column table_name, column_name, nil, null: null
188
+ end
189
+
190
+ def change_column_default _table_name, _column_name, _default_or_changes
191
+ raise ActiveRecordSpannerAdapter::NotSupportedError, \
192
+ "change column with default value not supported."
193
+ end
194
+
195
+ def rename_column table_name, column_name, new_column_name
196
+ if ActiveRecord::Base.connection.ddl_batch?
197
+ raise ActiveRecordSpannerAdapter::NotSupportedError, \
198
+ "rename_column in a DDL Batch is not supported."
199
+ end
200
+ column = information_schema do |i|
201
+ i.table_column table_name, column_name
202
+ end
203
+
204
+ unless column
205
+ raise ArgumentError,
206
+ "Column '#{column_name}' not exist for table '#{table_name}'"
207
+ end
208
+
209
+ # Add Column
210
+ cast_type = lookup_cast_type column.spanner_type
211
+ add_column table_name, new_column_name, cast_type.type, **column.options
212
+
213
+ # Copy data
214
+ copy_data table_name, column_name, new_column_name
215
+
216
+ # Recreate Indexes
217
+ recreate_indexes table_name, column_name, new_column_name
218
+
219
+ # Recreate Foreign keys
220
+ recreate_foreign_keys table_name, column_name, new_column_name
221
+
222
+ # Drop Indexes, Drop Foreign keys and columns
223
+ remove_column table_name, column_name
224
+ end
225
+
226
+ # Index
227
+
228
+ def indexes table_name
229
+ result = information_schema do |i|
230
+ i.indexes table_name, index_type: "INDEX"
231
+ end
232
+
233
+ result.map do |index|
234
+ IndexDefinition.new(
235
+ index.table,
236
+ index.name,
237
+ index.columns.map(&:name),
238
+ unique: index.unique,
239
+ null_filtered: index.null_filtered,
240
+ interleave_in: index.interleave_in,
241
+ storing: index.storing,
242
+ orders: index.orders
111
243
  )
112
244
  end
113
245
  end
114
246
 
115
- def primary_keys(table_name) # :nodoc:
116
- indexes(table_name).find {|index|
117
- index.type == 'PRIMARY_KEY'
118
- }.columns
247
+ def index_name_exists? table_name, index_name
248
+ information_schema { |i| i.index table_name, index_name }.present?
119
249
  end
120
250
 
121
- def create_database(name, instance_id: nil, statements: [])
122
- job = conn.create_database(instance_id || @instance_id, name,
123
- statements: statements)
124
- job.wait_until_done! unless job.done?
125
- raise_on_error(job)
251
+ def add_index table_name, column_name, options = {}
252
+ id = create_index_definition table_name, column_name, **options
253
+
254
+ if data_source_exists?(table_name) &&
255
+ index_name_exists?(table_name, id.name)
256
+ raise ArgumentError, "Index name '#{id.name}' on table" \
257
+ "'#{table_name}' already exists"
258
+ end
259
+
260
+ execute_schema_statements schema_creation.accept(id)
126
261
  end
127
262
 
128
- def drop_database(name, instance_id: nil)
129
- conn.service.drop_database(instance_id || @instance_id, name)
263
+ if ActiveRecord.gem_version < VERSION_6_1_0
264
+ def remove_index table_name, options = {}
265
+ index_name = index_name_for_remove table_name, options
266
+ execute "DROP INDEX #{quote_table_name index_name}"
267
+ end
268
+ else
269
+ def remove_index table_name, column_name = nil, **options
270
+ index_name = index_name_for_remove table_name, column_name, options
271
+ execute "DROP INDEX #{quote_table_name index_name}"
272
+ end
130
273
  end
131
274
 
132
- def drop_table(name, options = {})
133
- raise NotImplementedError, 'if_exists in drop_table' if options[:if_exists]
134
- raise NotImplementedError, 'force in drop_table' if options[:force]
275
+ def rename_index table_name, old_name, new_name
276
+ validate_index_length! table_name, new_name
135
277
 
136
- ddls = indexes(name).select {|index|
137
- index.type != 'PRIMARY_KEY'
138
- }.map {|index|
139
- "DROP INDEX #{index.name}"
140
- }
278
+ old_index = information_schema { |i| i.index table_name, old_name }
279
+ return unless old_index
280
+
281
+ statements = [
282
+ schema_creation.accept(DropIndexDefinition.new(old_name))
283
+ ]
284
+
285
+ id = IndexDefinition.new \
286
+ old_index.table,
287
+ new_name,
288
+ old_index.columns.map(&:name),
289
+ unique: old_index.unique,
290
+ null_filtered: old_index.null_filtered,
291
+ interleave_in: old_index.interleave_in,
292
+ storing: old_index.storing,
293
+ orders: old_index.orders
294
+
295
+ statements << schema_creation.accept(id)
296
+ execute_schema_statements statements
297
+ end
298
+
299
+ # Primary Keys
300
+
301
+ def primary_keys table_name
302
+ columns = information_schema do |i|
303
+ i.table_primary_keys table_name
304
+ end
305
+
306
+ columns.map(&:name)
307
+ end
308
+
309
+ def primary_and_parent_keys table_name
310
+ columns = information_schema do |i|
311
+ i.table_primary_keys table_name, true
312
+ end
141
313
 
142
- ddls << "DROP TABLE #{quote_table_name(name)}"
143
- execute_ddl(*ddls)
314
+ columns.map(&:name)
144
315
  end
145
316
 
146
- def add_index(table_name, column_name, options = {})
147
- index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options)
148
- execute_ddl(<<-"SQL")
149
- CREATE #{index_type} INDEX
150
- #{quote_column_name(index_name)}
151
- ON
152
- #{quote_table_name(table_name)} (#{index_columns})
153
- #{index_options}
154
- SQL
317
+ # Foreign Keys
318
+
319
+ def foreign_keys table_name, column: nil
320
+ raise ArgumentError if table_name.blank?
321
+
322
+ result = information_schema { |i| i.foreign_keys table_name }
323
+
324
+ if column
325
+ result = result.select { |fk| fk.columns.include? column.to_s }
326
+ end
327
+
328
+ result.map do |fk|
329
+ options = {
330
+ column: fk.columns.first,
331
+ name: fk.name,
332
+ primary_key: fk.ref_columns.first,
333
+ on_delete: fk.on_update,
334
+ on_update: fk.on_update
335
+ }
336
+
337
+ ForeignKeyDefinition.new table_name, fk.ref_table, options
338
+ end
155
339
  end
156
340
 
157
- def execute_ddl(*ddls)
158
- log(ddls.join(";\n"), 'SCHEMA') do
159
- job = database.update(statements: ddls.map(&:to_str))
160
- job.wait_until_done! unless job.done?
161
- raise_on_error(job.grpc)
341
+ if ActiveRecord.gem_version < VERSION_6_0_3
342
+ def add_foreign_key from_table, to_table, options = {}
343
+ _add_foreign_key from_table, to_table, **options
344
+ end
345
+ else
346
+ def add_foreign_key from_table, to_table, **options
347
+ _add_foreign_key from_table, to_table, **options
162
348
  end
163
349
  end
164
350
 
351
+ def _add_foreign_key from_table, to_table, **options
352
+ options = foreign_key_options from_table, to_table, options
353
+ at = create_alter_table from_table
354
+ at.add_foreign_key to_table, options
355
+
356
+ execute_schema_statements schema_creation.accept(at)
357
+ end
358
+
359
+ def remove_foreign_key from_table, to_table = nil, **options
360
+ fk_name_to_delete = foreign_key_for!(
361
+ from_table, to_table: to_table, **options
362
+ ).name
363
+
364
+ at = create_alter_table from_table
365
+ at.drop_foreign_key fk_name_to_delete
366
+
367
+ execute_schema_statements schema_creation.accept(at)
368
+ end
369
+
370
+ # Reference Column
371
+
372
+ def add_reference table_name, ref_name, **options
373
+ ReferenceDefinition.new(ref_name, **options).add_to(
374
+ update_table_definition(table_name, self)
375
+ )
376
+ end
377
+ alias add_belongs_to add_reference
378
+
379
+ def quoted_scope name = nil, type: nil
380
+ scope = { schema: quote("") }
381
+ scope[:name] = quote name if name
382
+ scope[:type] = quote type if type
383
+ scope
384
+ end
385
+
386
+ def create_schema_dumper options
387
+ SchemaDumper.create self, options
388
+ end
389
+
390
+ # rubocop:disable Lint/UnusedMethodArgument
391
+ def type_to_sql type, limit: nil, precision: nil, scale: nil, **opts
392
+ type = type.to_sym if type
393
+ native = native_database_types[type]
394
+
395
+ return type.to_s unless native
396
+
397
+ sql_type = (native.is_a?(Hash) ? native[:name] : native).dup
398
+
399
+ sql_type = "#{sql_type}(#{limit || native[:limit]})" if [:string, :text, :binary].include? type
400
+ sql_type = "ARRAY<#{sql_type}>" if opts[:array]
401
+
402
+ sql_type
403
+ end
404
+ # rubocop:enable Lint/UnusedMethodArgument
405
+
165
406
  private
166
- def raise_on_error(job)
167
- raise Google::Cloud::Error.from_error(job.error) if job.error?
407
+
408
+ def schema_creation
409
+ SchemaCreation.new self
410
+ end
411
+
412
+ def create_table_definition *args
413
+ TableDefinition.new self, args[0], options: args[1]
414
+ end
415
+
416
+ def able_to_ddl_batch? table_name
417
+ [ActiveRecord::InternalMetadata.table_name, ActiveRecord::SchemaMigration.table_name].exclude? table_name.to_s
418
+ end
419
+
420
+ def _change_column table_name, column_name, type, **options # rubocop:disable Metrics/AbcSize
421
+ column = information_schema do |i|
422
+ i.table_column table_name, column_name
423
+ end
424
+
425
+ unless column
426
+ raise ArgumentError,
427
+ "Column '#{column_name}' not exist for table '#{table_name}'"
428
+ end
429
+
430
+ indexes = information_schema do |i|
431
+ i.indexes_by_columns table_name, column_name
432
+ end
433
+
434
+ statements = indexes.map do |index|
435
+ schema_creation.accept DropIndexDefinition.new(index.name)
436
+ end
437
+
438
+ column = new_column_from_field table_name, column
439
+
440
+ type ||= column.type
441
+ options[:null] = column.null unless options.key? :null
442
+
443
+ if ["STRING", "BYTES"].include? type
444
+ options[:limit] = column.limit unless options.key? :limit
445
+ end
446
+
447
+ # Only timestamp type can set commit timestamp
448
+ if type == "TIMESTAMP" && options.key?(:allow_commit_timestamp) == false
449
+ options[:allow_commit_timestamp] = column.allow_commit_timestamp
450
+ end
451
+
452
+ td = create_table_definition table_name
453
+ cd = td.new_column_definition column.name, type, **options
454
+
455
+ ccd = Spanner::ChangeColumnDefinition.new table_name, cd, column.name
456
+ statements << schema_creation.accept(ccd)
457
+
458
+ # Recreate indexes
459
+ indexes.each do |index|
460
+ id = create_index_definition(
461
+ table_name,
462
+ index.column_names,
463
+ **index.options
464
+ )
465
+ statements << schema_creation.accept(id)
466
+ end
467
+
468
+ execute_schema_statements statements
469
+ end
470
+
471
+ def copy_data table_name, src_column_name, dest_column_name
472
+ sql = "UPDATE %<table>s SET %<dest_column_name>s = %<src_column_name>s WHERE true"
473
+ values = {
474
+ table: table_name,
475
+ dest_column_name: quote_column_name(dest_column_name),
476
+ src_column_name: quote_column_name(src_column_name)
477
+ }
478
+ ActiveRecord::Base.connection.transaction isolation: :pdml do
479
+ execute sql % values
480
+ end
481
+ end
482
+
483
+ def recreate_indexes table_name, column_name, new_column_name
484
+ indexes = information_schema.indexes_by_columns table_name, column_name
485
+ indexes.each do |index|
486
+ remove_index table_name, name: index.name
487
+ options = index.rename_column_options column_name, new_column_name
488
+ options[:options][:name] = options[:options][:name].to_s.gsub(
489
+ column_name.to_s, new_column_name.to_s
490
+ )
491
+ add_index table_name, options[:columns], **options[:options]
492
+ end
493
+ end
494
+
495
+ def recreate_foreign_keys table_name, column_name, new_column_name
496
+ fkeys = foreign_keys table_name, column: column_name
497
+ fkeys.each do |fk|
498
+ remove_foreign_key table_name, name: fk.name
499
+ options = fk.options.except :column, :name
500
+ options[:column] = new_column_name
501
+ add_foreign_key table_name, fk.to_table, **options
502
+ end
503
+ end
504
+
505
+ def create_index_definition table_name, column_name, **options
506
+ column_names = index_column_names column_name
507
+
508
+ options.assert_valid_keys(
509
+ :unique, :order, :name, :where, :length, :internal, :using,
510
+ :algorithm, :type, :opclass, :interleave_in, :storing,
511
+ :null_filtered
512
+ )
513
+
514
+ index_name = options[:name].to_s if options.key? :name
515
+ index_name ||= index_name table_name, column_names
516
+
517
+ validate_index_length! table_name, index_name
518
+
519
+ IndexDefinition.new \
520
+ table_name,
521
+ index_name,
522
+ column_names,
523
+ unique: options[:unique],
524
+ null_filtered: options[:null_filtered],
525
+ interleave_in: options[:interleave_in],
526
+ storing: options[:storing],
527
+ orders: options[:order]
528
+ end
529
+
530
+ def drop_table_with_indexes_sql table_name, options
531
+ statements = []
532
+
533
+ table = information_schema { |i| i.table table_name, view: :indexes }
534
+ return statements unless table
535
+
536
+ table.indexes.each do |index|
537
+ next if index.primary?
538
+
539
+ statements << schema_creation.accept(
540
+ DropIndexDefinition.new(index.name)
541
+ )
542
+ end
543
+
544
+ statements << schema_creation.accept(
545
+ DropTableDefinition.new(table_name, options)
546
+ )
547
+ statements
548
+ end
549
+
550
+ def drop_column_sql table_name, column_name
551
+ indexes = information_schema do |i|
552
+ i.indexes_by_columns table_name, column_name
553
+ end
554
+
555
+ statements = indexes.map do |index|
556
+ schema_creation.accept DropIndexDefinition.new(index.name)
557
+ end
558
+
559
+ foreign_keys(table_name, column: column_name).each do |fk|
560
+ at = create_alter_table table_name
561
+ at.drop_foreign_key fk.name
562
+ statements << schema_creation.accept(at)
563
+ end
564
+
565
+ statements << schema_creation.accept(
566
+ DropColumnDefinition.new(table_name, column_name)
567
+ )
568
+
569
+ statements
570
+ end
571
+
572
+ def execute_schema_statements statements
573
+ execute_ddl statements
574
+ end
575
+
576
+ def information_schema
577
+ info_schema = \
578
+ ActiveRecordSpannerAdapter::Connection.information_schema @config
579
+
580
+ return info_schema unless block_given?
581
+
582
+ yield info_schema
168
583
  end
169
584
  end
170
585
  end
171
586
  end
172
587
  end
173
-