activerecord-spanner-adapter 1.6.3 → 2.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 (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