activerecord-spanner-adapter 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. checksums.yaml +5 -5
  2. data/.github/CODEOWNERS +7 -0
  3. data/.github/sync-repo-settings.yaml +16 -0
  4. data/.github/workflows/acceptance-tests-on-emulator.yaml +45 -0
  5. data/.github/workflows/acceptance-tests-on-production.yaml +49 -0
  6. data/.github/workflows/ci.yaml +33 -0
  7. data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +52 -0
  8. data/.github/workflows/nightly-acceptance-tests-on-production.yaml +35 -0
  9. data/.github/workflows/nightly-unit-tests.yaml +40 -0
  10. data/.github/workflows/release-please-label.yml +25 -0
  11. data/.github/workflows/release-please.yml +39 -0
  12. data/.github/workflows/rubocop.yaml +31 -0
  13. data/.gitignore +67 -5
  14. data/.kokoro/populate-secrets.sh +77 -0
  15. data/.kokoro/release.cfg +33 -0
  16. data/.kokoro/release.sh +15 -0
  17. data/.kokoro/trampoline_v2.sh +489 -0
  18. data/.rubocop.yml +46 -0
  19. data/.toys/release.rb +18 -0
  20. data/.trampolinerc +48 -0
  21. data/.yardopts +11 -0
  22. data/CHANGELOG.md +55 -0
  23. data/CODE_OF_CONDUCT.md +40 -0
  24. data/CONTRIBUTING.md +79 -0
  25. data/Gemfile +9 -4
  26. data/LICENSE +6 -6
  27. data/README.md +66 -30
  28. data/Rakefile +79 -3
  29. data/SECURITY.md +7 -0
  30. data/acceptance/cases/associations/has_many_associations_test.rb +119 -0
  31. data/acceptance/cases/associations/has_many_through_associations_test.rb +63 -0
  32. data/acceptance/cases/associations/has_one_associations_test.rb +79 -0
  33. data/acceptance/cases/associations/has_one_through_associations_test.rb +98 -0
  34. data/acceptance/cases/interleaved_associations/has_many_associations_using_interleaved_test.rb +211 -0
  35. data/acceptance/cases/migration/change_schema_test.rb +433 -0
  36. data/acceptance/cases/migration/change_table_test.rb +115 -0
  37. data/acceptance/cases/migration/column_attributes_test.rb +122 -0
  38. data/acceptance/cases/migration/column_positioning_test.rb +48 -0
  39. data/acceptance/cases/migration/columns_test.rb +201 -0
  40. data/acceptance/cases/migration/command_recorder_test.rb +406 -0
  41. data/acceptance/cases/migration/create_join_table_test.rb +216 -0
  42. data/acceptance/cases/migration/ddl_batching_test.rb +80 -0
  43. data/acceptance/cases/migration/foreign_key_test.rb +297 -0
  44. data/acceptance/cases/migration/index_test.rb +211 -0
  45. data/acceptance/cases/migration/references_foreign_key_test.rb +259 -0
  46. data/acceptance/cases/migration/references_index_test.rb +135 -0
  47. data/acceptance/cases/migration/references_statements_test.rb +166 -0
  48. data/acceptance/cases/migration/rename_column_test.rb +96 -0
  49. data/acceptance/cases/models/calculation_query_test.rb +128 -0
  50. data/acceptance/cases/models/generated_column_test.rb +126 -0
  51. data/acceptance/cases/models/mutation_test.rb +122 -0
  52. data/acceptance/cases/models/query_test.rb +171 -0
  53. data/acceptance/cases/sessions/session_not_found_test.rb +121 -0
  54. data/acceptance/cases/transactions/optimistic_locking_test.rb +141 -0
  55. data/acceptance/cases/transactions/read_only_transactions_test.rb +130 -0
  56. data/acceptance/cases/transactions/read_write_transactions_test.rb +248 -0
  57. data/acceptance/cases/type/all_types_test.rb +172 -0
  58. data/acceptance/cases/type/binary_test.rb +59 -0
  59. data/acceptance/cases/type/boolean_test.rb +31 -0
  60. data/acceptance/cases/type/date_test.rb +32 -0
  61. data/acceptance/cases/type/date_time_test.rb +30 -0
  62. data/acceptance/cases/type/float_test.rb +27 -0
  63. data/acceptance/cases/type/integer_test.rb +44 -0
  64. data/acceptance/cases/type/json_test.rb +34 -0
  65. data/acceptance/cases/type/numeric_test.rb +27 -0
  66. data/acceptance/cases/type/string_test.rb +79 -0
  67. data/acceptance/cases/type/text_test.rb +30 -0
  68. data/acceptance/cases/type/time_test.rb +87 -0
  69. data/acceptance/models/account.rb +13 -0
  70. data/acceptance/models/address.rb +9 -0
  71. data/acceptance/models/album.rb +12 -0
  72. data/acceptance/models/all_types.rb +8 -0
  73. data/acceptance/models/author.rb +11 -0
  74. data/acceptance/models/club.rb +12 -0
  75. data/acceptance/models/comment.rb +9 -0
  76. data/acceptance/models/customer.rb +9 -0
  77. data/acceptance/models/department.rb +9 -0
  78. data/acceptance/models/firm.rb +10 -0
  79. data/acceptance/models/member.rb +13 -0
  80. data/acceptance/models/member_type.rb +9 -0
  81. data/acceptance/models/membership.rb +10 -0
  82. data/acceptance/models/organization.rb +9 -0
  83. data/acceptance/models/post.rb +10 -0
  84. data/acceptance/models/singer.rb +10 -0
  85. data/acceptance/models/track.rb +20 -0
  86. data/acceptance/models/transaction.rb +9 -0
  87. data/acceptance/schema/schema.rb +147 -0
  88. data/acceptance/test_helper.rb +261 -0
  89. data/activerecord-spanner-adapter.gemspec +32 -17
  90. data/assets/solidus-db.png +0 -0
  91. data/benchmarks/README.md +17 -0
  92. data/benchmarks/Rakefile +14 -0
  93. data/benchmarks/application.rb +308 -0
  94. data/benchmarks/config/database.yml +8 -0
  95. data/benchmarks/config/environment.rb +12 -0
  96. data/benchmarks/db/migrate/01_create_tables.rb +25 -0
  97. data/benchmarks/db/schema.rb +29 -0
  98. data/benchmarks/models/album.rb +9 -0
  99. data/benchmarks/models/singer.rb +9 -0
  100. data/bin/console +6 -7
  101. data/examples/rails/README.md +262 -0
  102. data/examples/snippets/README.md +29 -0
  103. data/examples/snippets/Rakefile +57 -0
  104. data/examples/snippets/array-data-type/README.md +45 -0
  105. data/examples/snippets/array-data-type/Rakefile +13 -0
  106. data/examples/snippets/array-data-type/application.rb +45 -0
  107. data/examples/snippets/array-data-type/config/database.yml +8 -0
  108. data/examples/snippets/array-data-type/db/migrate/01_create_tables.rb +24 -0
  109. data/examples/snippets/array-data-type/db/schema.rb +26 -0
  110. data/examples/snippets/array-data-type/db/seeds.rb +5 -0
  111. data/examples/snippets/array-data-type/models/entity_with_array_types.rb +18 -0
  112. data/examples/snippets/bin/create_emulator_instance.rb +18 -0
  113. data/examples/snippets/bulk-insert/README.md +21 -0
  114. data/examples/snippets/bulk-insert/Rakefile +13 -0
  115. data/examples/snippets/bulk-insert/application.rb +64 -0
  116. data/examples/snippets/bulk-insert/config/database.yml +8 -0
  117. data/examples/snippets/bulk-insert/db/migrate/01_create_tables.rb +21 -0
  118. data/examples/snippets/bulk-insert/db/schema.rb +26 -0
  119. data/examples/snippets/bulk-insert/db/seeds.rb +5 -0
  120. data/examples/snippets/bulk-insert/models/album.rb +9 -0
  121. data/examples/snippets/bulk-insert/models/singer.rb +9 -0
  122. data/examples/snippets/commit-timestamp/README.md +18 -0
  123. data/examples/snippets/commit-timestamp/Rakefile +13 -0
  124. data/examples/snippets/commit-timestamp/application.rb +53 -0
  125. data/examples/snippets/commit-timestamp/config/database.yml +8 -0
  126. data/examples/snippets/commit-timestamp/db/migrate/01_create_tables.rb +26 -0
  127. data/examples/snippets/commit-timestamp/db/schema.rb +29 -0
  128. data/examples/snippets/commit-timestamp/db/seeds.rb +5 -0
  129. data/examples/snippets/commit-timestamp/models/album.rb +9 -0
  130. data/examples/snippets/commit-timestamp/models/singer.rb +9 -0
  131. data/examples/snippets/config/environment.rb +21 -0
  132. data/examples/snippets/create-records/README.md +12 -0
  133. data/examples/snippets/create-records/Rakefile +13 -0
  134. data/examples/snippets/create-records/application.rb +42 -0
  135. data/examples/snippets/create-records/config/database.yml +8 -0
  136. data/examples/snippets/create-records/db/migrate/01_create_tables.rb +21 -0
  137. data/examples/snippets/create-records/db/schema.rb +26 -0
  138. data/examples/snippets/create-records/db/seeds.rb +5 -0
  139. data/examples/snippets/create-records/models/album.rb +9 -0
  140. data/examples/snippets/create-records/models/singer.rb +9 -0
  141. data/examples/snippets/date-data-type/README.md +19 -0
  142. data/examples/snippets/date-data-type/Rakefile +13 -0
  143. data/examples/snippets/date-data-type/application.rb +35 -0
  144. data/examples/snippets/date-data-type/config/database.yml +8 -0
  145. data/examples/snippets/date-data-type/db/migrate/01_create_tables.rb +20 -0
  146. data/examples/snippets/date-data-type/db/schema.rb +21 -0
  147. data/examples/snippets/date-data-type/db/seeds.rb +16 -0
  148. data/examples/snippets/date-data-type/models/singer.rb +8 -0
  149. data/examples/snippets/generated-column/README.md +41 -0
  150. data/examples/snippets/generated-column/Rakefile +13 -0
  151. data/examples/snippets/generated-column/application.rb +37 -0
  152. data/examples/snippets/generated-column/config/database.yml +8 -0
  153. data/examples/snippets/generated-column/db/migrate/01_create_tables.rb +23 -0
  154. data/examples/snippets/generated-column/db/schema.rb +21 -0
  155. data/examples/snippets/generated-column/db/seeds.rb +18 -0
  156. data/examples/snippets/generated-column/models/singer.rb +8 -0
  157. data/examples/snippets/hints/README.md +19 -0
  158. data/examples/snippets/hints/Rakefile +13 -0
  159. data/examples/snippets/hints/application.rb +47 -0
  160. data/examples/snippets/hints/config/database.yml +8 -0
  161. data/examples/snippets/hints/db/migrate/01_create_tables.rb +23 -0
  162. data/examples/snippets/hints/db/schema.rb +28 -0
  163. data/examples/snippets/hints/db/seeds.rb +29 -0
  164. data/examples/snippets/hints/models/album.rb +9 -0
  165. data/examples/snippets/hints/models/singer.rb +9 -0
  166. data/examples/snippets/migrations/README.md +43 -0
  167. data/examples/snippets/migrations/Rakefile +13 -0
  168. data/examples/snippets/migrations/application.rb +26 -0
  169. data/examples/snippets/migrations/config/database.yml +8 -0
  170. data/examples/snippets/migrations/db/migrate/01_create_tables.rb +28 -0
  171. data/examples/snippets/migrations/db/schema.rb +33 -0
  172. data/examples/snippets/migrations/db/seeds.rb +5 -0
  173. data/examples/snippets/migrations/models/album.rb +10 -0
  174. data/examples/snippets/migrations/models/singer.rb +10 -0
  175. data/examples/snippets/migrations/models/track.rb +9 -0
  176. data/examples/snippets/mutations/README.md +34 -0
  177. data/examples/snippets/mutations/Rakefile +13 -0
  178. data/examples/snippets/mutations/application.rb +47 -0
  179. data/examples/snippets/mutations/config/database.yml +8 -0
  180. data/examples/snippets/mutations/db/migrate/01_create_tables.rb +22 -0
  181. data/examples/snippets/mutations/db/schema.rb +27 -0
  182. data/examples/snippets/mutations/db/seeds.rb +25 -0
  183. data/examples/snippets/mutations/models/album.rb +9 -0
  184. data/examples/snippets/mutations/models/singer.rb +9 -0
  185. data/examples/snippets/optimistic-locking/README.md +12 -0
  186. data/examples/snippets/optimistic-locking/Rakefile +13 -0
  187. data/examples/snippets/optimistic-locking/application.rb +48 -0
  188. data/examples/snippets/optimistic-locking/config/database.yml +8 -0
  189. data/examples/snippets/optimistic-locking/db/migrate/01_create_tables.rb +26 -0
  190. data/examples/snippets/optimistic-locking/db/schema.rb +29 -0
  191. data/examples/snippets/optimistic-locking/db/seeds.rb +25 -0
  192. data/examples/snippets/optimistic-locking/models/album.rb +9 -0
  193. data/examples/snippets/optimistic-locking/models/singer.rb +9 -0
  194. data/examples/snippets/partitioned-dml/README.md +16 -0
  195. data/examples/snippets/partitioned-dml/Rakefile +13 -0
  196. data/examples/snippets/partitioned-dml/application.rb +48 -0
  197. data/examples/snippets/partitioned-dml/config/database.yml +8 -0
  198. data/examples/snippets/partitioned-dml/db/migrate/01_create_tables.rb +21 -0
  199. data/examples/snippets/partitioned-dml/db/schema.rb +26 -0
  200. data/examples/snippets/partitioned-dml/db/seeds.rb +29 -0
  201. data/examples/snippets/partitioned-dml/models/album.rb +9 -0
  202. data/examples/snippets/partitioned-dml/models/singer.rb +9 -0
  203. data/examples/snippets/quickstart/README.md +26 -0
  204. data/examples/snippets/quickstart/Rakefile +13 -0
  205. data/examples/snippets/quickstart/application.rb +51 -0
  206. data/examples/snippets/quickstart/config/database.yml +8 -0
  207. data/examples/snippets/quickstart/db/migrate/01_create_tables.rb +21 -0
  208. data/examples/snippets/quickstart/db/schema.rb +26 -0
  209. data/examples/snippets/quickstart/db/seeds.rb +24 -0
  210. data/examples/snippets/quickstart/models/album.rb +9 -0
  211. data/examples/snippets/quickstart/models/singer.rb +9 -0
  212. data/examples/snippets/read-only-transactions/README.md +13 -0
  213. data/examples/snippets/read-only-transactions/Rakefile +13 -0
  214. data/examples/snippets/read-only-transactions/application.rb +77 -0
  215. data/examples/snippets/read-only-transactions/config/database.yml +8 -0
  216. data/examples/snippets/read-only-transactions/db/migrate/01_create_tables.rb +21 -0
  217. data/examples/snippets/read-only-transactions/db/schema.rb +26 -0
  218. data/examples/snippets/read-only-transactions/db/seeds.rb +24 -0
  219. data/examples/snippets/read-only-transactions/models/album.rb +9 -0
  220. data/examples/snippets/read-only-transactions/models/singer.rb +9 -0
  221. data/examples/snippets/read-write-transactions/README.md +12 -0
  222. data/examples/snippets/read-write-transactions/Rakefile +13 -0
  223. data/examples/snippets/read-write-transactions/application.rb +39 -0
  224. data/examples/snippets/read-write-transactions/config/database.yml +8 -0
  225. data/examples/snippets/read-write-transactions/db/migrate/01_create_tables.rb +22 -0
  226. data/examples/snippets/read-write-transactions/db/schema.rb +27 -0
  227. data/examples/snippets/read-write-transactions/db/seeds.rb +25 -0
  228. data/examples/snippets/read-write-transactions/models/album.rb +9 -0
  229. data/examples/snippets/read-write-transactions/models/singer.rb +9 -0
  230. data/examples/snippets/stale-reads/README.md +27 -0
  231. data/examples/snippets/stale-reads/Rakefile +13 -0
  232. data/examples/snippets/stale-reads/application.rb +63 -0
  233. data/examples/snippets/stale-reads/config/database.yml +8 -0
  234. data/examples/snippets/stale-reads/db/migrate/01_create_tables.rb +21 -0
  235. data/examples/snippets/stale-reads/db/schema.rb +26 -0
  236. data/examples/snippets/stale-reads/db/seeds.rb +24 -0
  237. data/examples/snippets/stale-reads/models/album.rb +9 -0
  238. data/examples/snippets/stale-reads/models/singer.rb +9 -0
  239. data/examples/snippets/timestamp-data-type/README.md +17 -0
  240. data/examples/snippets/timestamp-data-type/Rakefile +13 -0
  241. data/examples/snippets/timestamp-data-type/application.rb +42 -0
  242. data/examples/snippets/timestamp-data-type/config/database.yml +8 -0
  243. data/examples/snippets/timestamp-data-type/db/migrate/01_create_tables.rb +21 -0
  244. data/examples/snippets/timestamp-data-type/db/schema.rb +21 -0
  245. data/examples/snippets/timestamp-data-type/db/seeds.rb +6 -0
  246. data/examples/snippets/timestamp-data-type/models/meeting.rb +19 -0
  247. data/examples/solidus/README.md +172 -0
  248. data/lib/active_record/connection_adapters/spanner/database_statements.rb +244 -266
  249. data/lib/active_record/connection_adapters/spanner/quoting.rb +42 -50
  250. data/lib/active_record/connection_adapters/spanner/schema_cache.rb +43 -0
  251. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +125 -9
  252. data/lib/active_record/connection_adapters/spanner/schema_definitions.rb +122 -0
  253. data/lib/active_record/connection_adapters/spanner/schema_dumper.rb +19 -0
  254. data/lib/active_record/connection_adapters/spanner/schema_statements.rb +553 -139
  255. data/lib/active_record/connection_adapters/spanner/type_metadata.rb +37 -0
  256. data/lib/active_record/connection_adapters/spanner_adapter.rb +185 -78
  257. data/lib/active_record/tasks/spanner_database_tasks.rb +74 -0
  258. data/lib/active_record/type/spanner/array.rb +32 -0
  259. data/lib/active_record/type/spanner/bytes.rb +26 -0
  260. data/lib/active_record/type/spanner/spanner_active_record_converter.rb +33 -0
  261. data/lib/active_record/type/spanner/time.rb +37 -0
  262. data/lib/activerecord-spanner-adapter.rb +23 -0
  263. data/lib/activerecord_spanner_adapter/base.rb +238 -0
  264. data/lib/activerecord_spanner_adapter/connection.rb +350 -0
  265. data/lib/activerecord_spanner_adapter/errors.rb +13 -0
  266. data/lib/activerecord_spanner_adapter/foreign_key.rb +29 -0
  267. data/lib/activerecord_spanner_adapter/index/column.rb +38 -0
  268. data/lib/activerecord_spanner_adapter/index.rb +80 -0
  269. data/lib/activerecord_spanner_adapter/information_schema.rb +262 -0
  270. data/lib/activerecord_spanner_adapter/primary_key.rb +31 -0
  271. data/lib/activerecord_spanner_adapter/table/column.rb +59 -0
  272. data/lib/activerecord_spanner_adapter/table.rb +61 -0
  273. data/lib/activerecord_spanner_adapter/transaction.rb +154 -0
  274. data/lib/activerecord_spanner_adapter/version.rb +9 -0
  275. data/lib/arel/visitors/spanner.rb +111 -0
  276. data/lib/spanner_client_ext.rb +107 -0
  277. data/renovate.json +5 -0
  278. metadata +405 -34
  279. data/.travis.yml +0 -5
  280. data/lib/active_record/connection_adapters/spanner/client.rb +0 -190
  281. data/lib/active_record/connection_adapters/spanner.rb +0 -10
  282. data/lib/activerecord-spanner-adapter/version.rb +0 -3
@@ -0,0 +1,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,262 @@
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,"
66
+ sql << " CAST(COLUMN_DEFAULT AS STRING) AS COLUMN_DEFAULT, ORDINAL_POSITION"
67
+ sql << " FROM INFORMATION_SCHEMA.COLUMNS"
68
+ sql << " WHERE TABLE_NAME=%<table_name>s"
69
+ sql << " AND COLUMN_NAME=%<column_name>s" if column_name
70
+ sql << " ORDER BY ORDINAL_POSITION ASC"
71
+
72
+ execute_query(
73
+ sql,
74
+ table_name: table_name,
75
+ column_name: column_name
76
+ ).map do |row|
77
+ type, limit = parse_type_and_limit row["SPANNER_TYPE"]
78
+ Table::Column.new \
79
+ table_name,
80
+ row["COLUMN_NAME"],
81
+ type,
82
+ limit: limit,
83
+ ordinal_position: row["ORDINAL_POSITION"],
84
+ nullable: row["IS_NULLABLE"] == "YES",
85
+ default: row["COLUMN_DEFAULT"]
86
+ end
87
+ end
88
+
89
+ def table_column table_name, column_name
90
+ table_columns(table_name, column_name: column_name).first
91
+ end
92
+
93
+ # Returns the primary key columns of the given table. By default it will only return the columns that are not part
94
+ # of the primary key of the parent table (if any). These are the columns that are considered the primary key by
95
+ # ActiveRecord. The parent primary key columns are filtered out by default to allow interleaved tables to be
96
+ # considered as tables with a single-column primary key by ActiveRecord. The actual primary key of the table will
97
+ # include both the parent primary key columns and the 'own' primary key columns of a table.
98
+ def table_primary_keys table_name, include_parent_keys = false
99
+ sql = +"WITH TABLE_PK_COLS AS ( "
100
+ sql << "SELECT C.TABLE_NAME, C.COLUMN_NAME, C.INDEX_NAME, C.COLUMN_ORDERING, C.ORDINAL_POSITION "
101
+ sql << "FROM INFORMATION_SCHEMA.INDEX_COLUMNS C "
102
+ sql << "WHERE C.INDEX_TYPE = 'PRIMARY_KEY' "
103
+ sql << "AND TABLE_CATALOG = '' "
104
+ sql << "AND TABLE_SCHEMA = '') "
105
+ sql << "SELECT INDEX_NAME, COLUMN_NAME, COLUMN_ORDERING, ORDINAL_POSITION "
106
+ sql << "FROM TABLE_PK_COLS "
107
+ sql << "INNER JOIN INFORMATION_SCHEMA.TABLES T USING (TABLE_NAME) "
108
+ sql << "WHERE TABLE_NAME = %<table_name>s "
109
+ sql << "AND TABLE_CATALOG = '' "
110
+ sql << "AND TABLE_SCHEMA = '' "
111
+ unless include_parent_keys
112
+ sql << "AND (T.PARENT_TABLE_NAME IS NULL OR COLUMN_NAME NOT IN ( "
113
+ sql << " SELECT COLUMN_NAME "
114
+ sql << " FROM TABLE_PK_COLS "
115
+ sql << " WHERE TABLE_NAME = T.PARENT_TABLE_NAME "
116
+ sql << ")) "
117
+ end
118
+ sql << "ORDER BY ORDINAL_POSITION"
119
+ execute_query(
120
+ sql,
121
+ table_name: table_name
122
+ ).map do |row|
123
+ Index::Column.new \
124
+ table_name,
125
+ row["INDEX_NAME"],
126
+ row["COLUMN_NAME"],
127
+ order: row["COLUMN_ORDERING"],
128
+ ordinal_position: row["ORDINAL_POSITION"]
129
+ end
130
+ end
131
+
132
+ def indexes table_name, index_name: nil, index_type: nil
133
+ table_indexes_columns = index_columns(
134
+ table_name,
135
+ index_name: index_name
136
+ )
137
+
138
+ sql = +"SELECT INDEX_NAME, INDEX_TYPE, IS_UNIQUE, IS_NULL_FILTERED, PARENT_TABLE_NAME, INDEX_STATE"
139
+ sql << " FROM INFORMATION_SCHEMA.INDEXES"
140
+ sql << " WHERE TABLE_NAME=%<table_name>s"
141
+ sql << " AND TABLE_CATALOG = ''"
142
+ sql << " AND TABLE_SCHEMA = ''"
143
+ sql << " AND INDEX_NAME=%<index_name>s" if index_name
144
+ sql << " AND INDEX_TYPE=%<index_type>s" if index_type
145
+ sql << " AND SPANNER_IS_MANAGED=FALSE"
146
+
147
+ execute_query(
148
+ sql,
149
+ table_name: table_name,
150
+ index_name: index_name,
151
+ index_type: index_type
152
+ ).map do |row|
153
+ columns = []
154
+ storing = []
155
+ table_indexes_columns.each do |c|
156
+ next unless c.index_name == row["INDEX_NAME"]
157
+ if c.ordinal_position
158
+ columns << c
159
+ else
160
+ storing << c.name
161
+ end
162
+ end
163
+
164
+ Index.new \
165
+ table_name,
166
+ row["INDEX_NAME"],
167
+ columns,
168
+ type: row["INDEX_TYPE"],
169
+ unique: row["IS_UNIQUE"],
170
+ null_filtered: row["IS_NULL_FILTERED"],
171
+ interleave_in: row["PARENT_TABLE_NAME"],
172
+ storing: storing,
173
+ state: row["INDEX_STATE"]
174
+ end
175
+ end
176
+
177
+ def index table_name, index_name
178
+ indexes(table_name, index_name: index_name).first
179
+ end
180
+
181
+ def index_columns table_name, index_name: nil
182
+ sql = +"SELECT INDEX_NAME, COLUMN_NAME, COLUMN_ORDERING, ORDINAL_POSITION"
183
+ sql << " FROM INFORMATION_SCHEMA.INDEX_COLUMNS"
184
+ sql << " WHERE TABLE_NAME=%<table_name>s"
185
+ sql << " AND TABLE_CATALOG = ''"
186
+ sql << " AND TABLE_SCHEMA = ''"
187
+ sql << " AND INDEX_NAME=%<index_name>s" if index_name
188
+ sql << " ORDER BY ORDINAL_POSITION ASC"
189
+
190
+ execute_query(
191
+ sql,
192
+ table_name: table_name, index_name: index_name
193
+ ).map do |row|
194
+ Index::Column.new \
195
+ table_name,
196
+ row["INDEX_NAME"],
197
+ row["COLUMN_NAME"],
198
+ order: row["COLUMN_ORDERING"],
199
+ ordinal_position: row["ORDINAL_POSITION"]
200
+ end
201
+ end
202
+
203
+ def indexes_by_columns table_name, column_names
204
+ column_names = Array(column_names).map(&:to_s)
205
+
206
+ indexes(table_name).select do |index|
207
+ index.columns.any? { |c| column_names.include? c.name }
208
+ end
209
+ end
210
+
211
+ def foreign_keys table_name
212
+ sql = <<~SQL
213
+ SELECT cc.table_name AS to_table,
214
+ cc.column_name AS primary_key,
215
+ fk.column_name as column,
216
+ fk.constraint_name AS name,
217
+ rc.update_rule AS on_update,
218
+ rc.delete_rule AS on_delete
219
+ FROM information_schema.referential_constraints rc
220
+ INNER JOIN information_schema.key_column_usage fk ON rc.constraint_name = fk.constraint_name
221
+ INNER JOIN information_schema.constraint_column_usage cc ON rc.constraint_name = cc.constraint_name
222
+ WHERE fk.table_name = %<table_name>s
223
+ AND fk.constraint_schema = %<constraint_schema>s
224
+ SQL
225
+
226
+ rows = execute_query(
227
+ sql, table_name: table_name, constraint_schema: ""
228
+ )
229
+
230
+ rows.map do |row|
231
+ ForeignKey.new(
232
+ table_name,
233
+ row["name"],
234
+ row["column"],
235
+ row["to_table"],
236
+ row["primary_key"],
237
+ on_delete: row["on_delete"],
238
+ on_update: row["on_update"]
239
+ )
240
+ end
241
+ end
242
+
243
+ def parse_type_and_limit value
244
+ matched = /^([A-Z]*)\((.*)\)/.match value
245
+ return [value] unless matched
246
+
247
+ limit = matched[2]
248
+ limit = limit.to_i unless limit == "MAX"
249
+
250
+ [matched[1], limit]
251
+ end
252
+
253
+ private
254
+
255
+ def execute_query sql, params = {}
256
+ params = params.transform_values { |v| quote v }
257
+ sql = format sql, params
258
+
259
+ @connection.execute_query(sql).rows
260
+ end
261
+ end
262
+ 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,154 @@
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
+ # Begins the transaction.
34
+ #
35
+ # Read-only and PDML transactions are started by executing a BeginTransaction RPC.
36
+ # Read/write transactions are not really started by this method, and instead a
37
+ # transaction selector is prepared that will be included with the first statement
38
+ # on the transaction.
39
+ def begin
40
+ raise "Nested transactions are not allowed" if @state != :INITIALIZED
41
+ begin
42
+ case @isolation
43
+ when Hash
44
+ if @isolation[:timestamp]
45
+ @grpc_transaction = @connection.session.create_snapshot timestamp: @isolation[:timestamp]
46
+ elsif @isolation[:staleness]
47
+ @grpc_transaction = @connection.session.create_snapshot staleness: @isolation[:staleness]
48
+ elsif @isolation[:strong]
49
+ @grpc_transaction = @connection.session.create_snapshot strong: true
50
+ else
51
+ raise "Invalid snapshot argument: #{@isolation}"
52
+ end
53
+ when :read_only
54
+ @grpc_transaction = @connection.session.create_snapshot strong: true
55
+ when :pdml
56
+ @grpc_transaction = @connection.session.create_pdml
57
+ else
58
+ @begin_transaction_selector = Google::Spanner::V1::TransactionSelector.new \
59
+ begin: Google::Spanner::V1::TransactionOptions.new(
60
+ read_write: Google::Spanner::V1::TransactionOptions::ReadWrite.new
61
+ )
62
+
63
+ end
64
+ @state = :STARTED
65
+ rescue Google::Cloud::NotFoundError => e
66
+ if @connection.session_not_found? e
67
+ @connection.reset!
68
+ retry
69
+ end
70
+ @state = :FAILED
71
+ raise
72
+ rescue StandardError
73
+ @state = :FAILED
74
+ raise
75
+ end
76
+ end
77
+
78
+ # Forces a BeginTransaction RPC for a read/write transaction. This is used by a
79
+ # connection if the first statement of a transaction failed.
80
+ def force_begin_read_write
81
+ @grpc_transaction = @connection.session.create_transaction
82
+ end
83
+
84
+ def next_sequence_number
85
+ @sequence_number += 1 if @committable
86
+ end
87
+
88
+ def commit
89
+ raise "This transaction is not active" unless active?
90
+
91
+ begin
92
+ # Start a transaction with an explicit BeginTransaction RPC if the transaction only contains mutations.
93
+ force_begin_read_write if @committable && !@mutations.empty? && !@grpc_transaction
94
+
95
+ @connection.session.commit_transaction @grpc_transaction, @mutations if @committable && @grpc_transaction
96
+ @state = :COMMITTED
97
+ rescue Google::Cloud::NotFoundError => e
98
+ if @connection.session_not_found? e
99
+ shoot_and_forget_rollback
100
+ @connection.reset!
101
+ @connection.raise_aborted_err
102
+ end
103
+ @state = :FAILED
104
+ raise
105
+ rescue StandardError
106
+ @state = :FAILED
107
+ raise
108
+ end
109
+ end
110
+
111
+ def rollback
112
+ # Allow rollback after abort and/or a failed commit.
113
+ raise "This transaction is not active" unless active? || @state == :FAILED || @state == :ABORTED
114
+ if active? && @grpc_transaction
115
+ # We do a shoot-and-forget rollback here, as the error that caused the transaction to be rolled back could
116
+ # also have invalidated the transaction (e.g. `Session not found`). If the rollback fails for any other
117
+ # reason, we also do not need to retry it or propagate the error to the application, as the transaction will
118
+ # automatically be aborted by Cloud Spanner after 10 seconds anyways.
119
+ shoot_and_forget_rollback
120
+ end
121
+ @state = :ROLLED_BACK
122
+ end
123
+
124
+ def shoot_and_forget_rollback
125
+ @connection.session.rollback @grpc_transaction.transaction_id if @committable
126
+ rescue StandardError # rubocop:disable Lint/HandleExceptions
127
+ # Ignored
128
+ end
129
+
130
+ def mark_aborted
131
+ @state = :ABORTED
132
+ end
133
+
134
+ # Sets the underlying gRPC transaction to use for this Transaction.
135
+ # This is used for queries/DML statements that inlined the BeginTransaction option and returned
136
+ # a transaction in the metadata.
137
+ def grpc_transaction= grpc
138
+ @grpc_transaction = Google::Cloud::Spanner::Transaction.from_grpc grpc, @connection.session
139
+ end
140
+
141
+ def transaction_selector
142
+ return unless active?
143
+
144
+ # Use the transaction that has been started by a BeginTransaction RPC or returned by a
145
+ # statement, if present.
146
+ return Google::Spanner::V1::TransactionSelector.new id: @grpc_transaction.transaction_id \
147
+ if @grpc_transaction
148
+
149
+ # Return a transaction selector that will instruct the statement to also start a transaction
150
+ # and return its id as a side effect.
151
+ @begin_transaction_selector
152
+ end
153
+ end
154
+ 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 = "1.0.0".freeze
9
+ end