activerecord-spanner-adapter 1.6.3 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (141) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/acceptance-tests-on-emulator.yaml +3 -7
  3. data/.github/workflows/acceptance-tests-on-production.yaml +1 -1
  4. data/.github/workflows/ci.yaml +3 -7
  5. data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +4 -33
  6. data/.github/workflows/nightly-acceptance-tests-on-production.yaml +1 -1
  7. data/.github/workflows/nightly-unit-tests.yaml +5 -33
  8. data/.github/workflows/rubocop.yaml +1 -1
  9. data/.github/workflows/samples.yaml +30 -0
  10. data/.kokoro/populate-secrets.sh +5 -1
  11. data/.kokoro/release.cfg +22 -12
  12. data/.kokoro/release.sh +1 -3
  13. data/.kokoro/trampoline_v2.sh +19 -11
  14. data/.release-please-manifest.json +1 -1
  15. data/.rubocop.yml +2 -2
  16. data/.trampolinerc +6 -1
  17. data/CHANGELOG.md +37 -0
  18. data/Gemfile +7 -5
  19. data/README.md +11 -9
  20. data/Rakefile +2 -2
  21. data/acceptance/cases/migration/command_recorder_test.rb +7 -38
  22. data/acceptance/cases/migration/references_index_test.rb +2 -11
  23. data/acceptance/cases/migration/schema_dumper_test.rb +21 -9
  24. data/acceptance/cases/models/binary_identifiers.rb +97 -0
  25. data/acceptance/cases/models/insert_all_test.rb +22 -7
  26. data/acceptance/cases/sessions/session_not_found_test.rb +2 -0
  27. data/acceptance/cases/tasks/database_tasks_test.rb +1 -0
  28. data/acceptance/models/binary_project.rb +20 -0
  29. data/acceptance/models/string_io.rb +28 -0
  30. data/acceptance/models/user.rb +20 -0
  31. data/acceptance/test_helper.rb +6 -1
  32. data/activerecord-spanner-adapter.gemspec +3 -3
  33. data/benchmarks/application.rb +3 -7
  34. data/examples/snippets/Rakefile +27 -5
  35. data/examples/snippets/array-data-type/application.rb +1 -5
  36. data/examples/snippets/array-data-type/config/database.yml +1 -0
  37. data/examples/snippets/bit-reversed-sequence/application.rb +0 -4
  38. data/examples/snippets/bit-reversed-sequence/config/database.yml +1 -0
  39. data/examples/snippets/bit-reversed-sequence/db/seeds.rb +2 -2
  40. data/examples/snippets/bulk-insert/application.rb +1 -5
  41. data/examples/snippets/bulk-insert/config/database.yml +1 -0
  42. data/examples/snippets/commit-timestamp/application.rb +0 -4
  43. data/examples/snippets/commit-timestamp/config/database.yml +1 -0
  44. data/examples/snippets/config/environment.rb +5 -0
  45. data/examples/snippets/create-records/application.rb +1 -5
  46. data/examples/snippets/create-records/config/database.yml +1 -0
  47. data/examples/snippets/date-data-type/application.rb +1 -5
  48. data/examples/snippets/date-data-type/config/database.yml +1 -0
  49. data/examples/snippets/date-data-type/db/seeds.rb +1 -1
  50. data/examples/snippets/generated-column/application.rb +0 -4
  51. data/examples/snippets/generated-column/config/database.yml +1 -0
  52. data/examples/snippets/generated-column/db/seeds.rb +1 -1
  53. data/examples/snippets/hints/application.rb +0 -4
  54. data/examples/snippets/hints/config/database.yml +1 -0
  55. data/examples/snippets/hints/db/seeds.rb +1 -1
  56. data/examples/snippets/interleaved-tables/application.rb +1 -5
  57. data/examples/snippets/interleaved-tables/config/database.yml +1 -0
  58. data/examples/snippets/interleaved-tables/db/seeds.rb +1 -1
  59. data/examples/snippets/interleaved-tables/models/album.rb +6 -2
  60. data/examples/snippets/interleaved-tables/models/track.rb +5 -1
  61. data/examples/snippets/interleaved-tables-before-7.1/application.rb +1 -5
  62. data/examples/snippets/interleaved-tables-before-7.1/config/database.yml +1 -0
  63. data/examples/snippets/interleaved-tables-before-7.1/db/seeds.rb +1 -1
  64. data/examples/snippets/migrations/application.rb +0 -4
  65. data/examples/snippets/migrations/config/database.yml +1 -0
  66. data/examples/snippets/mutations/application.rb +1 -5
  67. data/examples/snippets/mutations/config/database.yml +1 -0
  68. data/examples/snippets/mutations/db/seeds.rb +1 -1
  69. data/examples/snippets/optimistic-locking/application.rb +0 -4
  70. data/examples/snippets/optimistic-locking/config/database.yml +1 -0
  71. data/examples/snippets/optimistic-locking/db/seeds.rb +1 -1
  72. data/examples/snippets/partitioned-dml/application.rb +0 -4
  73. data/examples/snippets/partitioned-dml/config/database.yml +1 -0
  74. data/examples/snippets/partitioned-dml/db/seeds.rb +1 -1
  75. data/examples/snippets/query-logs/application.rb +15 -13
  76. data/examples/snippets/query-logs/config/database.yml +1 -0
  77. data/examples/snippets/query-logs/db/seeds.rb +1 -1
  78. data/examples/snippets/quickstart/application.rb +0 -4
  79. data/examples/snippets/quickstart/config/database.yml +1 -0
  80. data/examples/snippets/quickstart/db/seeds.rb +1 -1
  81. data/examples/snippets/read-only-transactions/application.rb +0 -4
  82. data/examples/snippets/read-only-transactions/config/database.yml +1 -0
  83. data/examples/snippets/read-only-transactions/db/seeds.rb +1 -1
  84. data/examples/snippets/read-write-transactions/application.rb +2 -6
  85. data/examples/snippets/read-write-transactions/config/database.yml +1 -0
  86. data/examples/snippets/read-write-transactions/db/seeds.rb +1 -1
  87. data/examples/snippets/stale-reads/application.rb +0 -4
  88. data/examples/snippets/stale-reads/config/database.yml +1 -0
  89. data/examples/snippets/stale-reads/db/seeds.rb +1 -1
  90. data/examples/snippets/tags/application.rb +0 -4
  91. data/examples/snippets/tags/config/database.yml +1 -0
  92. data/examples/snippets/tags/db/seeds.rb +1 -1
  93. data/examples/snippets/timestamp-data-type/application.rb +0 -4
  94. data/examples/snippets/timestamp-data-type/config/database.yml +1 -0
  95. data/lib/active_record/connection_adapters/spanner/column.rb +3 -3
  96. data/lib/active_record/connection_adapters/spanner/database_statements.rb +37 -23
  97. data/lib/active_record/connection_adapters/spanner/quoting.rb +19 -6
  98. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +7 -9
  99. data/lib/active_record/connection_adapters/spanner/schema_definitions.rb +12 -2
  100. data/lib/active_record/connection_adapters/spanner/schema_statements.rb +28 -46
  101. data/lib/active_record/connection_adapters/spanner/type_metadata.rb +4 -6
  102. data/lib/active_record/connection_adapters/spanner_adapter.rb +54 -27
  103. data/lib/active_record/tasks/spanner_database_tasks.rb +4 -4
  104. data/lib/active_record/type/spanner/array.rb +4 -0
  105. data/lib/active_record/type/spanner/bytes.rb +10 -0
  106. data/lib/activerecord-spanner-adapter.rb +5 -1
  107. data/lib/activerecord_spanner_adapter/base.rb +58 -30
  108. data/lib/activerecord_spanner_adapter/connection.rb +9 -5
  109. data/lib/activerecord_spanner_adapter/foreign_key.rb +9 -2
  110. data/lib/activerecord_spanner_adapter/index/column.rb +6 -1
  111. data/lib/activerecord_spanner_adapter/index.rb +10 -2
  112. data/lib/activerecord_spanner_adapter/information_schema.rb +1 -1
  113. data/lib/activerecord_spanner_adapter/primary_key.rb +2 -2
  114. data/lib/activerecord_spanner_adapter/table/column.rb +12 -3
  115. data/lib/activerecord_spanner_adapter/table.rb +8 -2
  116. data/lib/activerecord_spanner_adapter/transaction.rb +1 -1
  117. data/lib/activerecord_spanner_adapter/version.rb +1 -1
  118. data/lib/arel/visitors/spanner.rb +16 -11
  119. data/lib/spanner_client_ext.rb +4 -3
  120. metadata +15 -34
  121. data/examples/snippets/array-data-type/db/schema.rb +0 -31
  122. data/examples/snippets/bit-reversed-sequence/db/schema.rb +0 -31
  123. data/examples/snippets/bulk-insert/db/schema.rb +0 -31
  124. data/examples/snippets/commit-timestamp/db/schema.rb +0 -34
  125. data/examples/snippets/create-records/db/schema.rb +0 -31
  126. data/examples/snippets/date-data-type/db/schema.rb +0 -26
  127. data/examples/snippets/generated-column/db/schema.rb +0 -26
  128. data/examples/snippets/hints/db/schema.rb +0 -33
  129. data/examples/snippets/interleaved-tables/db/schema.rb +0 -39
  130. data/examples/snippets/interleaved-tables-before-7.1/db/schema.rb +0 -37
  131. data/examples/snippets/migrations/db/schema.rb +0 -38
  132. data/examples/snippets/mutations/db/schema.rb +0 -32
  133. data/examples/snippets/optimistic-locking/db/schema.rb +0 -34
  134. data/examples/snippets/partitioned-dml/db/schema.rb +0 -31
  135. data/examples/snippets/query-logs/db/schema.rb +0 -31
  136. data/examples/snippets/quickstart/db/schema.rb +0 -31
  137. data/examples/snippets/read-only-transactions/db/schema.rb +0 -31
  138. data/examples/snippets/read-write-transactions/db/schema.rb +0 -32
  139. data/examples/snippets/stale-reads/db/schema.rb +0 -31
  140. data/examples/snippets/tags/db/schema.rb +0 -31
  141. data/examples/snippets/timestamp-data-type/db/schema.rb +0 -26
@@ -21,15 +21,15 @@ module ActiveRecord
21
21
  internal_execute sql, name, binds
22
22
  end
23
23
 
24
- def internal_exec_query sql, name = "SQL", binds = [], prepare: false, async: false
25
- result = internal_execute sql, name, binds, prepare: prepare, async: async
24
+ def internal_exec_query sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false
25
+ result = internal_execute sql, name, binds, prepare: prepare, async: async, allow_retry: allow_retry
26
26
  ActiveRecord::Result.new(
27
27
  result.fields.keys.map(&:to_s), result.rows.map(&:values)
28
28
  )
29
29
  end
30
30
 
31
31
  def internal_execute sql, name = "SQL", binds = [],
32
- prepare: false, async: false # rubocop:disable Lint/UnusedMethodArgument
32
+ prepare: false, async: false, allow_retry: false # rubocop:disable Lint/UnusedMethodArgument, /
33
33
  statement_type = sql_statement_type sql
34
34
  # Call `transform` to invoke any query transformers that might have been registered.
35
35
  sql = transform sql
@@ -65,7 +65,7 @@ module ActiveRecord
65
65
  end
66
66
 
67
67
  log_args = [sql, name]
68
- log_args.concat [binds, type_casted_binds(binds)] if log_statement_binds
68
+ log_args.push binds, type_casted_binds(binds) if log_statement_binds
69
69
 
70
70
  log(*log_args) do
71
71
  types, params = to_types_and_params binds
@@ -83,12 +83,16 @@ module ActiveRecord
83
83
  end
84
84
 
85
85
  def append_request_tag_from_query_logs sql, binds
86
- legacy_formatter_prefix = "/*request_tag:true,"
87
- sql_commenter_prefix = "/*request_tag='true',"
88
- if sql.start_with? legacy_formatter_prefix
89
- append_request_tag_from_query_logs_with_format sql, binds, legacy_formatter_prefix
90
- elsif sql.start_with? sql_commenter_prefix
91
- append_request_tag_from_query_logs_with_format sql, binds, sql_commenter_prefix
86
+ possible_prefixes = [
87
+ "/*request_tag:true,",
88
+ "/*_request_tag='true',",
89
+ "/*_request_tag:true,",
90
+ "/*_request_tag='true',"
91
+ ]
92
+ possible_prefixes.each do |prefix|
93
+ if sql.start_with? prefix
94
+ append_request_tag_from_query_logs_with_format sql, binds, prefix
95
+ end
92
96
  end
93
97
  end
94
98
 
@@ -101,7 +105,7 @@ module ActiveRecord
101
105
  if options.request_tag == ""
102
106
  options.request_tag = request_tag
103
107
  else
104
- options.request_tag += "," + request_tag
108
+ options.request_tag += ",#{request_tag}"
105
109
  end
106
110
 
107
111
  binds.append options
@@ -192,7 +196,7 @@ module ActiveRecord
192
196
  # and this RPC can return multiple partial result sets for DML as well. Only the last partial
193
197
  # result set will contain the statistics. Although there will never be any rows, this makes
194
198
  # sure that the stream is fully consumed.
195
- result.rows.each { |_| }
199
+ result.rows.each { |_| } # rubocop:disable Lint/EmptyBlock
196
200
  return result.row_count if result.row_count
197
201
 
198
202
  raise ActiveRecord::StatementInvalid.new(
@@ -282,12 +286,12 @@ module ActiveRecord
282
286
  #
283
287
  def begin_isolated_db_transaction isolation
284
288
  if isolation.is_a? Hash
285
- raise "Unsupported isolation level: #{isolation}" unless \
289
+ raise "Unsupported isolation level: #{isolation}" unless
286
290
  isolation[:timestamp] || isolation[:staleness] || isolation[:strong]
287
291
  raise "Only one option is supported. It must be one of `timestamp`, `staleness` or `strong`." \
288
292
  if isolation.count != 1
289
293
  else
290
- raise "Unsupported isolation level: #{isolation}" unless \
294
+ raise "Unsupported isolation level: #{isolation}" unless
291
295
  [:serializable, :read_only, :buffered_mutations, :pdml].include? isolation
292
296
  end
293
297
 
@@ -312,24 +316,35 @@ module ActiveRecord
312
316
 
313
317
  # Translates binds to Spanner types and params.
314
318
  def to_types_and_params binds
315
- types = binds.enum_for(:each_with_index).map do |bind, i|
319
+ types = to_types binds
320
+ params = to_params binds
321
+ [types, params]
322
+ end
323
+
324
+ def to_types binds
325
+ binds.enum_for(:each_with_index).to_h do |bind, i|
316
326
  type = :INT64
317
327
  if bind.respond_to? :type
318
328
  type = ActiveRecord::Type::Spanner::SpannerActiveRecordConverter
319
329
  .convert_active_model_type_to_spanner(bind.type)
320
- elsif bind.class == Symbol
330
+ elsif bind.instance_of? Symbol
321
331
  # This ensures that for example :environment is sent as the string 'environment' to Cloud Spanner.
322
332
  type = :STRING
333
+ elsif bind.instance_of?(TrueClass) || bind.instance_of?(FalseClass)
334
+ type = :BOOL
323
335
  end
324
336
  [
325
337
  # Generates binds for named parameters in the format `@p1, @p2, ...`
326
338
  "p#{i + 1}", type
327
339
  ]
328
- end.to_h
329
- params = binds.enum_for(:each_with_index).map do |bind, i|
340
+ end
341
+ end
342
+
343
+ def to_params binds
344
+ binds.enum_for(:each_with_index).to_h do |bind, i|
330
345
  type = if bind.respond_to? :type
331
346
  bind.type
332
- elsif bind.class == Symbol
347
+ elsif bind.instance_of? Symbol
333
348
  # This ensures that for example :environment is sent as the string 'environment' to Cloud Spanner.
334
349
  :STRING
335
350
  else
@@ -341,8 +356,7 @@ module ActiveRecord
341
356
  .serialize_with_transaction_isolation_level(type, bind_value, :dml)
342
357
 
343
358
  ["p#{i + 1}", value]
344
- end.to_h
345
- [types, params]
359
+ end
346
360
  end
347
361
 
348
362
  # An insert/update/delete statement could use mutations in some specific circumstances.
@@ -351,7 +365,7 @@ module ActiveRecord
351
365
  def should_use_mutation arel
352
366
  !@connection.current_transaction.nil? \
353
367
  && @connection.current_transaction.isolation == :buffered_mutations \
354
- && can_use_mutation(arel) \
368
+ && can_use_mutation(arel)
355
369
  end
356
370
 
357
371
  def can_use_mutation arel
@@ -381,7 +395,7 @@ module ActiveRecord
381
395
  )
382
396
  end
383
397
 
384
- COMMENT_REGEX = %r{(?:--.*\n)*|/\*(?:[^*]|\*[^/])*\*/}m.freeze \
398
+ COMMENT_REGEX = %r{(?:--.*\n)*|/\*(?:[^*]|\*[^/])*\*/}m \
385
399
  unless defined? ActiveRecord::ConnectionAdapters::AbstractAdapter::COMMENT_REGEX
386
400
  COMMENT_REGEX = ActiveRecord::ConnectionAdapters::AbstractAdapter::COMMENT_REGEX \
387
401
  if defined? ActiveRecord::ConnectionAdapters::AbstractAdapter::COMMENT_REGEX
@@ -30,20 +30,33 @@
30
30
 
31
31
  module ActiveRecord
32
32
  module ConnectionAdapters
33
+ QUOTED_COLUMN_NAMES = Concurrent::Map.new # :nodoc:
34
+ QUOTED_TABLE_NAMES = Concurrent::Map.new # :nodoc:
35
+
36
+ module Quoting
37
+ module ClassMethods
38
+ # This is used for ActiveRecord v8 and higher.
39
+ def quote_column_name name
40
+ QUOTED_COLUMN_NAMES[name] ||= "`#{name.to_s.gsub '`', '``'}`".freeze
41
+ end
42
+
43
+ def quote_table_name name
44
+ QUOTED_TABLE_NAMES[name] ||= "`#{name.to_s.gsub '.', '`.`'}`".freeze
45
+ end
46
+ end
47
+ end
48
+
33
49
  module Spanner
34
50
  module Quoting
35
- QUOTED_COLUMN_NAMES = Concurrent::Map.new # :nodoc:
36
- QUOTED_TABLE_NAMES = Concurrent::Map.new # :nodoc:
37
-
38
51
  def quote_column_name name
39
- QUOTED_COLUMN_NAMES[name] ||= "`#{super.gsub '`', '``'}`".freeze
52
+ QUOTED_COLUMN_NAMES[name] ||= "`#{name.to_s.gsub '`', '``'}`".freeze
40
53
  end
41
54
 
42
55
  def quote_table_name name
43
- QUOTED_TABLE_NAMES[name] ||= super.gsub(".", "`.`").freeze
56
+ QUOTED_TABLE_NAMES[name] ||= "`#{name.to_s.gsub '.', '`.`'}`".freeze
44
57
  end
45
58
 
46
- STR_ESCAPE_REGX = /[\n\r'\\]/.freeze
59
+ STR_ESCAPE_REGX = /[\n\r'\\]/
47
60
  STR_ESCAPE_VALUES = {
48
61
  "\n" => "\\n", "\r" => "\\r", "'" => "\\'", "\\" => "\\\\"
49
62
  }.freeze
@@ -44,10 +44,7 @@ module ActiveRecord
44
44
  else
45
45
  [o.options[:primary_key]]
46
46
  end
47
- pk_names = []
48
- columns.each do |c|
49
- pk_names.append c.to_s
50
- end
47
+ pk_names = columns.map(&:to_s)
51
48
  PrimaryKeyDefinition.new pk_names
52
49
  else
53
50
  pk_names = o.columns.each_with_object [] do |c, r|
@@ -93,8 +90,8 @@ module ActiveRecord
93
90
  end
94
91
 
95
92
  def visit_DropColumnDefinition o
96
- "ALTER TABLE #{quote_table_name o.table_name} DROP" \
97
- " COLUMN #{quote_column_name o.name}"
93
+ "ALTER TABLE #{quote_table_name o.table_name} DROP " \
94
+ "COLUMN #{quote_column_name o.name}"
98
95
  end
99
96
 
100
97
  def visit_ChangeColumnDefinition o
@@ -143,7 +140,7 @@ module ActiveRecord
143
140
 
144
141
  if !options[:allow_commit_timestamp].nil? &&
145
142
  options[:column].sql_type == "TIMESTAMP"
146
- sql << " OPTIONS (allow_commit_timestamp = "\
143
+ sql << " OPTIONS (allow_commit_timestamp = " \
147
144
  "#{options[:allow_commit_timestamp]})"
148
145
  end
149
146
 
@@ -153,8 +150,9 @@ module ActiveRecord
153
150
  sql << " STORED" if options[:stored]
154
151
  unless options[:stored]
155
152
  raise ArgumentError, "" \
156
- "Cloud Spanner currently does not support generated columns without the STORED option." \
157
- "Specify 'stored: true' option for `#{options[:column].name}`"
153
+ "Cloud Spanner currently does not support generated columns" \
154
+ "without the STORED option." \
155
+ "Specify 'stored: true' option for `#{options[:column].name}`"
158
156
  end
159
157
  end
160
158
 
@@ -5,7 +5,7 @@
5
5
  # https://opensource.org/licenses/MIT.
6
6
 
7
7
  module ActiveRecord
8
- module ConnectionAdapters #:nodoc:
8
+ module ConnectionAdapters # :nodoc:
9
9
  module Spanner
10
10
  class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
11
11
  attr_reader :interleave_in_parent
@@ -62,6 +62,9 @@ module ActiveRecord
62
62
  DropIndexDefinition = Struct.new :name
63
63
 
64
64
  class ReferenceDefinition < ActiveRecord::ConnectionAdapters::ReferenceDefinition
65
+ # This constructor intentionally does not call super to prevent ActiveRecord
66
+ # from creating an additional secondary index for the foreign key.
67
+ # rubocop:disable Lint/MissingSuper
65
68
  def initialize \
66
69
  name,
67
70
  polymorphic: false,
@@ -81,6 +84,7 @@ module ActiveRecord
81
84
  return unless polymorphic && foreign_key
82
85
  raise ArgumentError, "Cannot add a foreign key to a polymorphic relation"
83
86
  end
87
+ # rubocop:enable Lint/MissingSuper
84
88
 
85
89
  private
86
90
 
@@ -96,8 +100,13 @@ module ActiveRecord
96
100
  end
97
101
 
98
102
  class IndexDefinition < ActiveRecord::ConnectionAdapters::IndexDefinition
99
- attr_reader :null_filtered, :interleave_in, :storing, :orders
103
+ attr_reader :null_filtered
104
+ attr_reader :interleave_in
105
+ attr_reader :storing
106
+ attr_reader :orders
100
107
 
108
+ # This constructor intentionally does not call super.
109
+ # rubocop:disable Lint/MissingSuper
101
110
  def initialize \
102
111
  table_name,
103
112
  name,
@@ -123,6 +132,7 @@ module ActiveRecord
123
132
 
124
133
  @orders = @orders.symbolize_keys
125
134
  end
135
+ # rubocop:enable Lint/MissingSuper
126
136
 
127
137
  def columns_with_order
128
138
  columns.each_with_object({}) do |c, result|
@@ -23,6 +23,7 @@ module ActiveRecord
23
23
  module SchemaStatements
24
24
  VERSION_6_1_0 = Gem::Version.create "6.1.0"
25
25
  VERSION_6_0_3 = Gem::Version.create "6.0.3"
26
+ VERSION_7_2 = Gem::Version.create "7.2.0"
26
27
 
27
28
  def current_database
28
29
  @connection.database_id
@@ -108,8 +109,7 @@ module ActiveRecord
108
109
  end
109
110
 
110
111
  def rename_table _table_name, _new_name
111
- raise ActiveRecordSpannerAdapter::NotSupportedError, \
112
- "rename_table is not implemented"
112
+ raise ActiveRecordSpannerAdapter::NotSupportedError, "rename_table is not implemented"
113
113
  end
114
114
 
115
115
  # Column
@@ -167,20 +167,14 @@ module ActiveRecord
167
167
  execute_schema_statements statements
168
168
  end
169
169
 
170
- if ActiveRecord.gem_version < VERSION_6_1_0
171
- def remove_columns table_name, *column_names
172
- _remove_columns table_name, *column_names
173
- end
174
- else
175
- def remove_columns table_name, *column_names, _type: nil, **_options
176
- _remove_columns table_name, *column_names
177
- end
170
+ def remove_columns table_name, *column_names, _type: nil, **_options
171
+ _remove_columns table_name, *column_names
178
172
  end
179
173
 
180
174
  def _remove_columns table_name, *column_names
181
175
  if column_names.empty?
182
- raise ArgumentError, "You must specify at least one column name. "\
183
- "Example: remove_columns(:people, :first_name)"
176
+ raise ArgumentError, "You must specify at least one column name. " \
177
+ "Example: remove_columns(:people, :first_name)"
184
178
  end
185
179
 
186
180
  statements = []
@@ -192,14 +186,8 @@ module ActiveRecord
192
186
  execute_schema_statements statements
193
187
  end
194
188
 
195
- if ActiveRecord.gem_version < VERSION_6_1_0
196
- def change_column table_name, column_name, type, options = {}
197
- _change_column table_name, column_name, type, **options
198
- end
199
- else
200
- def change_column table_name, column_name, type, **options
201
- _change_column table_name, column_name, type, **options
202
- end
189
+ def change_column table_name, column_name, type, **options
190
+ _change_column table_name, column_name, type, **options
203
191
  end
204
192
 
205
193
  def change_column_null table_name, column_name, null, _default = nil
@@ -207,14 +195,12 @@ module ActiveRecord
207
195
  end
208
196
 
209
197
  def change_column_default _table_name, _column_name, _default_or_changes
210
- raise ActiveRecordSpannerAdapter::NotSupportedError, \
211
- "change column with default value not supported."
198
+ raise ActiveRecordSpannerAdapter::NotSupportedError, "change column with default value not supported."
212
199
  end
213
200
 
214
201
  def rename_column table_name, column_name, new_column_name
215
202
  if ActiveRecord::Base.connection.ddl_batch?
216
- raise ActiveRecordSpannerAdapter::NotSupportedError, \
217
- "rename_column in a DDL Batch is not supported."
203
+ raise ActiveRecordSpannerAdapter::NotSupportedError, "rename_column in a DDL Batch is not supported."
218
204
  end
219
205
  column = information_schema do |i|
220
206
  i.table_column table_name, column_name
@@ -279,16 +265,9 @@ module ActiveRecord
279
265
  execute_schema_statements schema_creation.accept(id)
280
266
  end
281
267
 
282
- if ActiveRecord.gem_version < VERSION_6_1_0
283
- def remove_index table_name, options = {}
284
- index_name = index_name_for_remove table_name, options
285
- execute "DROP INDEX #{quote_table_name index_name}"
286
- end
287
- else
288
- def remove_index table_name, column_name = nil, **options
289
- index_name = index_name_for_remove table_name, column_name, options
290
- execute "DROP INDEX #{quote_table_name index_name}"
291
- end
268
+ def remove_index table_name, column_name = nil, **options
269
+ index_name = index_name_for_remove table_name, column_name, options
270
+ execute "DROP INDEX #{quote_table_name index_name}"
292
271
  end
293
272
 
294
273
  def rename_index table_name, old_name, new_name
@@ -357,14 +336,8 @@ module ActiveRecord
357
336
  end
358
337
  end
359
338
 
360
- if ActiveRecord.gem_version < VERSION_6_0_3
361
- def add_foreign_key from_table, to_table, options = {}
362
- _add_foreign_key from_table, to_table, **options
363
- end
364
- else
365
- def add_foreign_key from_table, to_table, **options
366
- _add_foreign_key from_table, to_table, **options
367
- end
339
+ def add_foreign_key from_table, to_table, **options
340
+ _add_foreign_key from_table, to_table, **options
368
341
  end
369
342
 
370
343
  def _add_foreign_key from_table, to_table, **options
@@ -401,6 +374,16 @@ module ActiveRecord
401
374
  information_schema { |i| i.check_constraints table_name }
402
375
  end
403
376
 
377
+ if ActiveRecord.gem_version >= VERSION_7_2
378
+ def migration_context
379
+ pool.migration_context
380
+ end
381
+
382
+ def schema_migration
383
+ pool.schema_migration
384
+ end
385
+ end
386
+
404
387
  def assume_migrated_upto_version version
405
388
  version = version.to_i
406
389
  sm_table = quote_table_name schema_migration.table_name
@@ -497,8 +480,8 @@ module ActiveRecord
497
480
  type ||= column.type
498
481
  options[:null] = column.null unless options.key? :null
499
482
 
500
- if ["STRING", "BYTES"].include? type
501
- options[:limit] = column.limit unless options.key? :limit
483
+ if ["STRING", "BYTES"].include?(type) && !options.key?(:limit)
484
+ options[:limit] = column.limit
502
485
  end
503
486
 
504
487
  # Only timestamp type can set commit timestamp
@@ -631,8 +614,7 @@ module ActiveRecord
631
614
  end
632
615
 
633
616
  def information_schema
634
- info_schema = \
635
- ActiveRecordSpannerAdapter::Connection.information_schema @config
617
+ info_schema = ActiveRecordSpannerAdapter::Connection.information_schema @config
636
618
 
637
619
  return info_schema unless block_given?
638
620
 
@@ -14,7 +14,9 @@ module ActiveRecord
14
14
 
15
15
  include Deduplicable if defined?(Deduplicable)
16
16
 
17
- attr_reader :ordinal_position, :allow_commit_timestamp, :generated
17
+ attr_reader :ordinal_position
18
+ attr_reader :allow_commit_timestamp
19
+ attr_reader :generated
18
20
 
19
21
  def initialize type_metadata, ordinal_position: nil, allow_commit_timestamp: nil, generated: nil
20
22
  super type_metadata
@@ -33,11 +35,7 @@ module ActiveRecord
33
35
  alias eql? ==
34
36
 
35
37
  def hash
36
- TypeMetadata.hash ^
37
- __getobj__.hash ^
38
- ordinal_position.hash ^
39
- allow_commit_timestamp.hash ^
40
- generated.hash
38
+ [TypeMetadata.name, __getobj__, ordinal_position, allow_commit_timestamp, generated].hash
41
39
  end
42
40
 
43
41
  private
@@ -28,16 +28,18 @@ require "activerecord_spanner_adapter/primary_key"
28
28
  require "activerecord_spanner_adapter/transaction"
29
29
 
30
30
  module ActiveRecord
31
- module ConnectionHandling # :nodoc:
32
- def spanner_connection config
33
- connection = ActiveRecordSpannerAdapter::Connection.new config
34
- connection.connect!
35
- ConnectionAdapters::SpannerAdapter.new connection, logger, nil, config
36
- rescue Google::Cloud::Error => error
37
- if error.instance_of? Google::Cloud::NotFoundError
38
- raise ActiveRecord::NoDatabaseError
39
- end
40
- raise error
31
+ if ActiveRecord.version < Gem::Version.new("7.2")
32
+ module ConnectionHandling # :nodoc:
33
+ def spanner_connection config
34
+ connection = ActiveRecordSpannerAdapter::Connection.new config
35
+ connection.connect!
36
+ ConnectionAdapters::SpannerAdapter.new connection, logger, nil, config
37
+ rescue Google::Cloud::Error => error
38
+ if error.instance_of? Google::Cloud::NotFoundError
39
+ raise ActiveRecord::NoDatabaseError
40
+ end
41
+ raise error
42
+ end
41
43
  end
42
44
  end
43
45
 
@@ -69,11 +71,21 @@ module ActiveRecord
69
71
  # Determines whether or not to log query binds when executing statements
70
72
  class_attribute :log_statement_binds, instance_writer: false, default: false
71
73
 
72
- def initialize connection, logger, connection_options, config
73
- @connection = connection
74
- @connection_options = connection_options
75
- super connection, logger, config
76
- @raw_connection ||= connection
74
+ def initialize config_or_deprecated_connection, deprecated_logger = nil,
75
+ deprecated_connection_options = nil, deprecated_config = nil
76
+ if config_or_deprecated_connection.is_a? Hash
77
+ @connection = ActiveRecordSpannerAdapter::Connection.new config_or_deprecated_connection
78
+ @connection.connect!
79
+ super config_or_deprecated_connection
80
+ @raw_connection ||= @connection
81
+ else
82
+ @connection = config_or_deprecated_connection
83
+ @connection_options = deprecated_connection_options
84
+ super config_or_deprecated_connection, deprecated_logger, deprecated_config
85
+ @raw_connection ||= config_or_deprecated_connection
86
+ end
87
+ # Spanner does not support unprepared statements
88
+ @prepared_statements = true
77
89
  end
78
90
 
79
91
  def max_identifier_length
@@ -197,12 +209,14 @@ module ActiveRecord
197
209
  raise "ActiveRecordSpannerAdapter does not support insert_sql with buffered_mutations transaction."
198
210
  end
199
211
 
200
- if insert.skip_duplicates? || insert.update_duplicates?
201
- raise NotImplementedError, "CloudSpanner does not support skip_duplicates and update_duplicates."
202
- end
203
-
204
212
  values_list, = insert.values_list
205
- "INSERT #{insert.into} #{values_list}"
213
+ prefix = "INSERT"
214
+ if insert.update_duplicates?
215
+ prefix += " OR UPDATE"
216
+ elsif insert.skip_duplicates?
217
+ prefix += " OR IGNORE"
218
+ end
219
+ "#{prefix} #{insert.into} #{values_list}"
206
220
  end
207
221
 
208
222
  module TypeMapBuilder
@@ -262,7 +276,9 @@ module ActiveRecord
262
276
  end
263
277
 
264
278
  def transform sql
265
- if ActiveRecord::VERSION::MAJOR >= 7
279
+ if ActiveRecord::VERSION::MAJOR >= 8
280
+ preprocess_query sql
281
+ elsif ActiveRecord::VERSION::MAJOR == 7
266
282
  transform_query sql
267
283
  else
268
284
  sql
@@ -270,12 +286,23 @@ module ActiveRecord
270
286
  end
271
287
 
272
288
  # Overwrite the standard log method to be able to translate exceptions.
273
- def log sql, name = "SQL", binds = [], type_casted_binds = [], statement_name = nil, *args
274
- super
275
- rescue ActiveRecord::StatementInvalid
276
- raise
277
- rescue StandardError => e
278
- raise translate_exception_class(e, sql, binds)
289
+ # sql, name = "SQL", binds = [], type_casted_binds = [], async: false, &block
290
+ if ActiveRecord::VERSION::MAJOR >= 8
291
+ def log sql, name = "SQL", binds = [], type_casted_binds = [], async: false, &block
292
+ super
293
+ rescue ActiveRecord::StatementInvalid
294
+ raise
295
+ rescue StandardError => e
296
+ raise translate_exception_class(e, sql, binds)
297
+ end
298
+ else
299
+ def log sql, name = "SQL", binds = [], type_casted_binds = [], statement_name = nil, *args
300
+ super
301
+ rescue ActiveRecord::StatementInvalid
302
+ raise
303
+ rescue StandardError => e
304
+ raise translate_exception_class(e, sql, binds)
305
+ end
279
306
  end
280
307
 
281
308
  def translate_exception exception, message:, sql:, binds:
@@ -31,7 +31,7 @@ module ActiveRecord
31
31
  def purge
32
32
  begin
33
33
  drop
34
- rescue ActiveRecord::NoDatabaseError # rubocop:disable Lint/HandleExceptions
34
+ rescue ActiveRecord::NoDatabaseError
35
35
  # ignored; create the database
36
36
  end
37
37
 
@@ -51,8 +51,8 @@ module ActiveRecord
51
51
  ignore_tables = ActiveRecord::SchemaDumper.ignore_tables
52
52
 
53
53
  if ignore_tables.any?
54
- index_regx = /^CREATE(.*)INDEX(.*)ON (#{ignore_tables.join "|"})\(/
55
- table_regx = /^CREATE TABLE (#{ignore_tables.join "|"})/
54
+ index_regx = /^CREATE(.*)INDEX(.*)ON (#{ignore_tables.join '|'})\(/
55
+ table_regx = /^CREATE TABLE (#{ignore_tables.join '|'})/
56
56
  end
57
57
 
58
58
  @connection.database.ddl(force: true).each do |statement|
@@ -66,7 +66,7 @@ module ActiveRecord
66
66
  end
67
67
 
68
68
  def structure_load filename, _extra_flags
69
- statements = File.read(filename).split(/;/).map(&:strip).reject(&:empty?)
69
+ statements = File.read(filename).split(";").map(&:strip).reject(&:empty?)
70
70
  ddls = statements.select { |s| s =~ /^(CREATE|ALTER|DROP|GRANT|REVOKE|ANALYZE)/ }
71
71
  @connection.execute_ddl ddls
72
72
 
@@ -9,11 +9,15 @@ module ActiveRecord
9
9
  module Spanner
10
10
  class Array < Type::Value
11
11
  attr_reader :element_type
12
+
12
13
  delegate :type, :user_input_in_time_zone, :limit, :precision, :scale, to: :element_type
13
14
 
15
+ # This constructor intentionally does not call super.
16
+ # rubocop:disable Lint/MissingSuper
14
17
  def initialize element_type
15
18
  @element_type = element_type
16
19
  end
20
+ # rubocop:enable Lint/MissingSuper
17
21
 
18
22
  def cast value
19
23
  return super if value.nil?
@@ -10,6 +10,16 @@ module ActiveRecord
10
10
  module Type
11
11
  module Spanner
12
12
  class Bytes < ActiveRecord::Type::Binary
13
+ def deserialize value
14
+ # Set this environment variable to disable de-serializing BYTES
15
+ # to a StringIO instance.
16
+ return super if ENV["SPANNER_BYTES_DESERIALIZE_DISABLED"]
17
+
18
+ return super value if value.nil?
19
+ return StringIO.new Base64.strict_decode64(value) if value.is_a? ::String
20
+ value
21
+ end
22
+
13
23
  def serialize value
14
24
  return super value if value.nil?
15
25
 
@@ -15,7 +15,11 @@ if defined?(Rails)
15
15
  end
16
16
 
17
17
  ActiveSupport.on_load :active_record do
18
- require "active_record/connection_adapters/spanner_adapter"
18
+ if Rails.version >= "7.2.0"
19
+ ActiveRecord::ConnectionAdapters.register("spanner", "ActiveRecord::ConnectionAdapters::SpannerAdapter")
20
+ else
21
+ require "active_record/connection_adapters/spanner_adapter"
22
+ end
19
23
  end
20
24
  end
21
25
  end