activerecord-spanner-adapter 1.5.1 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/acceptance-tests-on-emulator.yaml +1 -1
  3. data/.github/workflows/acceptance-tests-on-production.yaml +5 -3
  4. data/.github/workflows/ci.yaml +1 -1
  5. data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +1 -1
  6. data/.github/workflows/nightly-acceptance-tests-on-production.yaml +5 -3
  7. data/.github/workflows/nightly-unit-tests.yaml +1 -1
  8. data/.github/workflows/release-please-label.yml +1 -1
  9. data/.release-please-manifest.json +1 -1
  10. data/CHANGELOG.md +8 -0
  11. data/Gemfile +5 -2
  12. data/README.md +10 -10
  13. data/acceptance/cases/interleaved_associations/has_many_associations_using_interleaved_test.rb +6 -0
  14. data/acceptance/cases/migration/change_schema_test.rb +19 -3
  15. data/acceptance/cases/migration/schema_dumper_test.rb +10 -1
  16. data/acceptance/cases/models/interleave_test.rb +6 -0
  17. data/acceptance/cases/tasks/database_tasks_test.rb +340 -2
  18. data/acceptance/cases/transactions/optimistic_locking_test.rb +6 -0
  19. data/acceptance/cases/transactions/read_write_transactions_test.rb +24 -0
  20. data/acceptance/models/table_with_sequence.rb +10 -0
  21. data/acceptance/schema/schema.rb +65 -19
  22. data/acceptance/test_helper.rb +1 -1
  23. data/activerecord-spanner-adapter.gemspec +1 -1
  24. data/examples/snippets/bit-reversed-sequence/README.md +103 -0
  25. data/examples/snippets/bit-reversed-sequence/Rakefile +13 -0
  26. data/examples/snippets/bit-reversed-sequence/application.rb +68 -0
  27. data/examples/snippets/bit-reversed-sequence/config/database.yml +8 -0
  28. data/examples/snippets/bit-reversed-sequence/db/migrate/01_create_tables.rb +33 -0
  29. data/examples/snippets/bit-reversed-sequence/db/schema.rb +31 -0
  30. data/examples/snippets/bit-reversed-sequence/db/seeds.rb +31 -0
  31. data/examples/snippets/bit-reversed-sequence/models/album.rb +11 -0
  32. data/examples/snippets/bit-reversed-sequence/models/singer.rb +15 -0
  33. data/examples/snippets/interleaved-tables/README.md +44 -53
  34. data/examples/snippets/interleaved-tables/Rakefile +2 -2
  35. data/examples/snippets/interleaved-tables/application.rb +2 -2
  36. data/examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb +12 -18
  37. data/examples/snippets/interleaved-tables/db/schema.rb +9 -7
  38. data/examples/snippets/interleaved-tables/db/seeds.rb +1 -1
  39. data/examples/snippets/interleaved-tables/models/album.rb +3 -7
  40. data/examples/snippets/interleaved-tables/models/singer.rb +1 -1
  41. data/examples/snippets/interleaved-tables/models/track.rb +6 -7
  42. data/examples/snippets/interleaved-tables-before-7.1/README.md +167 -0
  43. data/examples/snippets/interleaved-tables-before-7.1/Rakefile +13 -0
  44. data/examples/snippets/interleaved-tables-before-7.1/application.rb +126 -0
  45. data/examples/snippets/interleaved-tables-before-7.1/config/database.yml +8 -0
  46. data/examples/snippets/interleaved-tables-before-7.1/db/migrate/01_create_tables.rb +44 -0
  47. data/examples/snippets/interleaved-tables-before-7.1/db/schema.rb +37 -0
  48. data/examples/snippets/interleaved-tables-before-7.1/db/seeds.rb +40 -0
  49. data/examples/snippets/interleaved-tables-before-7.1/models/album.rb +20 -0
  50. data/examples/snippets/interleaved-tables-before-7.1/models/singer.rb +18 -0
  51. data/examples/snippets/interleaved-tables-before-7.1/models/track.rb +28 -0
  52. data/examples/snippets/query-logs/README.md +43 -0
  53. data/examples/snippets/query-logs/Rakefile +13 -0
  54. data/examples/snippets/query-logs/application.rb +63 -0
  55. data/examples/snippets/query-logs/config/database.yml +8 -0
  56. data/examples/snippets/query-logs/db/migrate/01_create_tables.rb +21 -0
  57. data/examples/snippets/query-logs/db/schema.rb +31 -0
  58. data/examples/snippets/query-logs/db/seeds.rb +24 -0
  59. data/examples/snippets/query-logs/models/album.rb +9 -0
  60. data/examples/snippets/query-logs/models/singer.rb +9 -0
  61. data/lib/active_record/connection_adapters/spanner/column.rb +13 -0
  62. data/lib/active_record/connection_adapters/spanner/database_statements.rb +144 -35
  63. data/lib/active_record/connection_adapters/spanner/schema_cache.rb +3 -21
  64. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +11 -0
  65. data/lib/active_record/connection_adapters/spanner/schema_definitions.rb +4 -0
  66. data/lib/active_record/connection_adapters/spanner/schema_statements.rb +3 -2
  67. data/lib/active_record/connection_adapters/spanner_adapter.rb +28 -9
  68. data/lib/activerecord_spanner_adapter/base.rb +56 -19
  69. data/lib/activerecord_spanner_adapter/information_schema.rb +33 -24
  70. data/lib/activerecord_spanner_adapter/primary_key.rb +1 -1
  71. data/lib/activerecord_spanner_adapter/table/column.rb +4 -9
  72. data/lib/activerecord_spanner_adapter/version.rb +1 -1
  73. data/lib/arel/visitors/spanner.rb +3 -1
  74. metadata +33 -4
@@ -4,6 +4,7 @@
4
4
  # license that can be found in the LICENSE file or at
5
5
  # https://opensource.org/licenses/MIT.
6
6
 
7
+ require "active_record/gem_version"
7
8
  require "activerecord_spanner_adapter/relation"
8
9
 
9
10
  module ActiveRecord
@@ -14,6 +15,8 @@ module ActiveRecord
14
15
  end
15
16
 
16
17
  class Base
18
+ VERSION_7_1 = Gem::Version.create "7.1.0"
19
+
17
20
  # Creates an object (or multiple objects) and saves it to the database. This method will use mutations instead
18
21
  # of DML if there is no active transaction, or if the active transaction has been created with the option
19
22
  # isolation: :buffered_mutations.
@@ -30,7 +33,9 @@ module ActiveRecord
30
33
  return super unless spanner_adapter?
31
34
  return super if active_transaction?
32
35
 
33
- transaction isolation: :buffered_mutations do
36
+ # Only use mutations to create new records if the primary key is generated client-side.
37
+ isolation = sequence_name ? nil : :buffered_mutations
38
+ transaction isolation: isolation do
34
39
  return super
35
40
  end
36
41
  end
@@ -43,35 +48,67 @@ module ActiveRecord
43
48
  spanner_adapter? && connection&.current_spanner_transaction&.isolation == :buffered_mutations
44
49
  end
45
50
 
46
- def self._insert_record values
51
+ def self._insert_record values, returning = []
47
52
  return super unless buffered_mutations? || (primary_key && values.is_a?(Hash))
48
53
 
49
- return _buffer_record values, :insert if buffered_mutations?
54
+ # Mutations cannot be used in combination with a sequence, as mutations do not support a THEN RETURN clause.
55
+ if buffered_mutations? && sequence_name
56
+ raise StatementInvalid, "Mutations cannot be used to create records that use a sequence " \
57
+ "to generate the primary key. #{self} uses #{sequence_name}."
58
+ end
50
59
 
51
- primary_key_value =
52
- if primary_key.is_a? Array
53
- _set_composite_primary_key_values primary_key, values
54
- else
55
- _set_single_primary_key_value primary_key, values
56
- end
60
+ return _buffer_record values, :insert, returning if buffered_mutations?
61
+
62
+ primary_key_value = _set_primary_key_value values
57
63
  if ActiveRecord::VERSION::MAJOR >= 7
58
64
  im = Arel::InsertManager.new arel_table
59
65
  im.insert(values.transform_keys { |name| arel_table[name] })
60
66
  else
61
67
  im = arel_table.compile_insert _substitute_values(values)
62
68
  end
63
- connection.insert(im, "#{self} Create", primary_key || false, primary_key_value)
69
+ result = connection.insert(im, "#{self} Create", primary_key || false, primary_key_value)
70
+
71
+ _convert_primary_key result, returning
64
72
  end
65
73
 
66
- def self._upsert_record values
67
- _buffer_record values, :insert_or_update
74
+ def self._set_primary_key_value values
75
+ if primary_key.is_a? Array
76
+ _set_composite_primary_key_values primary_key, values
77
+ else
78
+ _set_single_primary_key_value primary_key, values
79
+ end
80
+ end
81
+
82
+ def self._convert_primary_key primary_key_value, returning
83
+ # Rails 7.1 and higher supports composite primary keys, and therefore require the provider to return an array
84
+ # instead of a single value in all cases. The order of the values should be equal to the order of the returning
85
+ # columns (or the primary key if no returning columns were specified).
86
+ return primary_key_value if ActiveRecord.gem_version < VERSION_7_1
87
+ return primary_key_value if primary_key_value.is_a?(Array) && primary_key_value.length == 1
88
+ return [primary_key_value] unless primary_key_value.is_a? Array
89
+
90
+ # Re-order the returned values according to the returning columns if it is not equal to the primary key of the
91
+ # table.
92
+ keys = returning || primary_key
93
+ return primary_key_value if keys == primary_key
94
+
95
+ primary_key_values_hash = Hash[primary_key.zip(primary_key_value)]
96
+ values = []
97
+ keys.each do |column|
98
+ values.append primary_key_values_hash[column]
99
+ end
100
+ values
101
+ end
102
+
103
+ def self._upsert_record values, returning
104
+ _buffer_record values, :insert_or_update, returning
68
105
  end
69
106
 
70
107
  def self.insert_all _attributes, **_kwargs
71
108
  raise NotImplementedError, "Cloud Spanner does not support skip_duplicates. Use insert! or upsert instead."
72
109
  end
73
110
 
74
- def self.insert_all! attributes, **_kwargs
111
+ def self.insert_all! attributes, returning: nil, **_kwargs
75
112
  return super unless spanner_adapter?
76
113
  return super if active_transaction? && !buffered_mutations?
77
114
 
@@ -90,7 +127,7 @@ module ActiveRecord
90
127
  end
91
128
  end
92
129
 
93
- def self.upsert_all attributes, **_kwargs
130
+ def self.upsert_all attributes, returning: nil, unique_by: nil, **_kwargs
94
131
  return super unless spanner_adapter?
95
132
  if active_transaction? && !buffered_mutations?
96
133
  raise NotImplementedError, "Cloud Spanner does not support upsert using DML. " \
@@ -102,18 +139,18 @@ module ActiveRecord
102
139
  # The mutations will be sent as one batch when the transaction is committed.
103
140
  if active_transaction?
104
141
  attributes.each do |record|
105
- _upsert_record record
142
+ _upsert_record record, returning
106
143
  end
107
144
  else
108
145
  transaction isolation: :buffered_mutations do
109
146
  attributes.each do |record|
110
- _upsert_record record
147
+ _upsert_record record, returning
111
148
  end
112
149
  end
113
150
  end
114
151
  end
115
152
 
116
- def self._buffer_record values, method
153
+ def self._buffer_record values, method, returning
117
154
  primary_key_value =
118
155
  if primary_key.is_a? Array
119
156
  _set_composite_primary_key_values primary_key, values
@@ -132,10 +169,9 @@ module ActiveRecord
132
169
  mutation = Google::Cloud::Spanner::V1::Mutation.new(
133
170
  "#{method}": write
134
171
  )
135
-
136
172
  connection.current_spanner_transaction.buffer mutation
137
173
 
138
- primary_key_value
174
+ _convert_primary_key primary_key_value, returning
139
175
  end
140
176
 
141
177
  def self._set_composite_primary_key_values primary_key, values
@@ -174,6 +210,7 @@ module ActiveRecord
174
210
  def self._set_single_primary_key_value primary_key, values
175
211
  primary_key_value = values[primary_key] || values[primary_key.to_sym]
176
212
 
213
+ return primary_key_value if sequence_name
177
214
  return primary_key_value unless prefetch_primary_key?
178
215
 
179
216
  if primary_key_value.nil?
@@ -15,6 +15,8 @@ module ActiveRecordSpannerAdapter
15
15
  class InformationSchema
16
16
  include ActiveRecord::ConnectionAdapters::Quoting
17
17
 
18
+ IsRails71OrLater = ActiveRecord.gem_version >= Gem::Version.create("7.1.0")
19
+
18
20
  attr_reader :connection
19
21
 
20
22
  def initialize connection
@@ -62,6 +64,7 @@ module ActiveRecordSpannerAdapter
62
64
  end
63
65
 
64
66
  def table_columns table_name, column_name: nil
67
+ primary_keys = table_primary_keys(table_name).map(&:name)
65
68
  sql = +"SELECT COLUMN_NAME, SPANNER_TYPE, IS_NULLABLE, GENERATION_EXPRESSION,"
66
69
  sql << " CAST(COLUMN_DEFAULT AS STRING) AS COLUMN_DEFAULT, ORDINAL_POSITION"
67
70
  sql << " FROM INFORMATION_SCHEMA.COLUMNS"
@@ -75,34 +78,40 @@ module ActiveRecordSpannerAdapter
75
78
  table_name: table_name,
76
79
  column_name: column_name
77
80
  ).map do |row|
78
- type, limit = parse_type_and_limit row["SPANNER_TYPE"]
79
- column_name = row["COLUMN_NAME"]
80
- options = column_options[column_name]
81
+ _create_column table_name, row, primary_keys, column_options
82
+ end
83
+ end
81
84
 
82
- default = row["COLUMN_DEFAULT"]
83
- default_function = row["GENERATION_EXPRESSION"]
85
+ def _create_column table_name, row, primary_keys, column_options
86
+ type, limit = parse_type_and_limit row["SPANNER_TYPE"]
87
+ column_name = row["COLUMN_NAME"]
88
+ options = column_options[column_name]
89
+ primary_key = primary_keys.include? column_name
84
90
 
85
- if /\w+\(.*\)/.match?(default)
86
- default_function ||= default
87
- default = nil
88
- end
91
+ default = row["COLUMN_DEFAULT"]
92
+ default_function = row["GENERATION_EXPRESSION"]
89
93
 
90
- if default && type == "STRING"
91
- default = unquote_string default
92
- end
94
+ if default && default.length < 200 && /\w+\(.*\)/.match?(default)
95
+ default_function ||= default
96
+ default = nil
97
+ end
93
98
 
94
- Table::Column.new \
95
- table_name,
96
- column_name,
97
- type,
98
- limit: limit,
99
- allow_commit_timestamp: options["allow_commit_timestamp"],
100
- ordinal_position: row["ORDINAL_POSITION"],
101
- nullable: row["IS_NULLABLE"] == "YES",
102
- default: default,
103
- default_function: default_function,
104
- generated: row["GENERATION_EXPRESSION"].present?
99
+ if default && type == "STRING"
100
+ default = unquote_string default
105
101
  end
102
+
103
+ Table::Column.new \
104
+ table_name,
105
+ column_name,
106
+ type,
107
+ limit: limit,
108
+ allow_commit_timestamp: options["allow_commit_timestamp"],
109
+ ordinal_position: row["ORDINAL_POSITION"],
110
+ nullable: row["IS_NULLABLE"] == "YES",
111
+ default: default,
112
+ default_function: default_function,
113
+ generated: row["GENERATION_EXPRESSION"].present?,
114
+ primary_key: primary_key
106
115
  end
107
116
 
108
117
  def table_column table_name, column_name
@@ -114,7 +123,7 @@ module ActiveRecordSpannerAdapter
114
123
  # ActiveRecord. The parent primary key columns are filtered out by default to allow interleaved tables to be
115
124
  # considered as tables with a single-column primary key by ActiveRecord. The actual primary key of the table will
116
125
  # include both the parent primary key columns and the 'own' primary key columns of a table.
117
- def table_primary_keys table_name, include_parent_keys = false
126
+ def table_primary_keys table_name, include_parent_keys = IsRails71OrLater
118
127
  sql = +"WITH TABLE_PK_COLS AS ( "
119
128
  sql << "SELECT C.TABLE_NAME, C.COLUMN_NAME, C.INDEX_NAME, C.COLUMN_ORDERING, C.ORDINAL_POSITION "
120
129
  sql << "FROM INFORMATION_SCHEMA.INDEX_COLUMNS C "
@@ -18,7 +18,7 @@ module ActiveRecord
18
18
  end
19
19
 
20
20
  def fetch_primary_and_parent_key
21
- return connection.schema_cache.primary_and_parent_keys table_name \
21
+ return connection.spanner_schema_cache.primary_and_parent_keys table_name \
22
22
  if ActiveRecord::Base != self && table_exists?
23
23
  end
24
24
 
@@ -9,8 +9,7 @@ module ActiveRecordSpannerAdapter
9
9
  class Column
10
10
  attr_accessor :table_name, :name, :type, :limit, :ordinal_position,
11
11
  :allow_commit_timestamp, :default, :default_function, :generated,
12
- :primary_key
13
- attr_writer :nullable
12
+ :primary_key, :nullable
14
13
 
15
14
  def initialize \
16
15
  table_name,
@@ -22,7 +21,8 @@ module ActiveRecordSpannerAdapter
22
21
  allow_commit_timestamp: nil,
23
22
  default: nil,
24
23
  default_function: nil,
25
- generated: nil
24
+ generated: nil,
25
+ primary_key: false
26
26
  @table_name = table_name.to_s
27
27
  @name = name.to_s
28
28
  @type = type
@@ -33,12 +33,7 @@ module ActiveRecordSpannerAdapter
33
33
  @default = default
34
34
  @default_function = default_function
35
35
  @generated = generated == true
36
- @primary_key = false
37
- end
38
-
39
- def nullable
40
- return false if primary_key
41
- @nullable
36
+ @primary_key = primary_key
42
37
  end
43
38
 
44
39
  def spanner_type
@@ -5,5 +5,5 @@
5
5
  # https://opensource.org/licenses/MIT.
6
6
 
7
7
  module ActiveRecordSpannerAdapter
8
- VERSION = "1.5.1".freeze
8
+ VERSION = "1.6.0".freeze
9
9
  end
@@ -139,7 +139,9 @@ module Arel # :nodoc: all
139
139
  # Do not generate a query parameter if the value should be set to the PENDING_COMMIT_TIMESTAMP(), as that is
140
140
  # not supported as a parameter value by Cloud Spanner.
141
141
  return collector << "PENDING_COMMIT_TIMESTAMP()" \
142
- if o.value.type.is_a?(ActiveRecord::Type::Spanner::Time) && o.value.value == :commit_timestamp
142
+ if o.value.respond_to?(:type) \
143
+ && o.value.type.is_a?(ActiveRecord::Type::Spanner::Time) \
144
+ && o.value.value == :commit_timestamp
143
145
  collector.add_bind(o.value, &bind_block)
144
146
  end
145
147
  # rubocop:enable Naming/MethodName
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-spanner-adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.1
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Google LLC
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-12-13 00:00:00.000000000 Z
11
+ date: 2023-12-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: google-cloud-spanner
@@ -33,7 +33,7 @@ dependencies:
33
33
  version: 6.0.0
34
34
  - - "<"
35
35
  - !ruby/object:Gem::Version
36
- version: '7.1'
36
+ version: '7.2'
37
37
  type: :runtime
38
38
  prerelease: false
39
39
  version_requirements: !ruby/object:Gem::Requirement
@@ -43,7 +43,7 @@ dependencies:
43
43
  version: 6.0.0
44
44
  - - "<"
45
45
  - !ruby/object:Gem::Version
46
- version: '7.1'
46
+ version: '7.2'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: autotest-suffix
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -312,6 +312,7 @@ files:
312
312
  - acceptance/models/organization.rb
313
313
  - acceptance/models/post.rb
314
314
  - acceptance/models/singer.rb
315
+ - acceptance/models/table_with_sequence.rb
315
316
  - acceptance/models/track.rb
316
317
  - acceptance/models/transaction.rb
317
318
  - acceptance/schema/schema.rb
@@ -342,6 +343,15 @@ files:
342
343
  - examples/snippets/array-data-type/db/seeds.rb
343
344
  - examples/snippets/array-data-type/models/entity_with_array_types.rb
344
345
  - examples/snippets/bin/create_emulator_instance.rb
346
+ - examples/snippets/bit-reversed-sequence/README.md
347
+ - examples/snippets/bit-reversed-sequence/Rakefile
348
+ - examples/snippets/bit-reversed-sequence/application.rb
349
+ - examples/snippets/bit-reversed-sequence/config/database.yml
350
+ - examples/snippets/bit-reversed-sequence/db/migrate/01_create_tables.rb
351
+ - examples/snippets/bit-reversed-sequence/db/schema.rb
352
+ - examples/snippets/bit-reversed-sequence/db/seeds.rb
353
+ - examples/snippets/bit-reversed-sequence/models/album.rb
354
+ - examples/snippets/bit-reversed-sequence/models/singer.rb
345
355
  - examples/snippets/bulk-insert/README.md
346
356
  - examples/snippets/bulk-insert/Rakefile
347
357
  - examples/snippets/bulk-insert/application.rb
@@ -395,6 +405,16 @@ files:
395
405
  - examples/snippets/hints/db/seeds.rb
396
406
  - examples/snippets/hints/models/album.rb
397
407
  - examples/snippets/hints/models/singer.rb
408
+ - examples/snippets/interleaved-tables-before-7.1/README.md
409
+ - examples/snippets/interleaved-tables-before-7.1/Rakefile
410
+ - examples/snippets/interleaved-tables-before-7.1/application.rb
411
+ - examples/snippets/interleaved-tables-before-7.1/config/database.yml
412
+ - examples/snippets/interleaved-tables-before-7.1/db/migrate/01_create_tables.rb
413
+ - examples/snippets/interleaved-tables-before-7.1/db/schema.rb
414
+ - examples/snippets/interleaved-tables-before-7.1/db/seeds.rb
415
+ - examples/snippets/interleaved-tables-before-7.1/models/album.rb
416
+ - examples/snippets/interleaved-tables-before-7.1/models/singer.rb
417
+ - examples/snippets/interleaved-tables-before-7.1/models/track.rb
398
418
  - examples/snippets/interleaved-tables/README.md
399
419
  - examples/snippets/interleaved-tables/Rakefile
400
420
  - examples/snippets/interleaved-tables/application.rb
@@ -442,6 +462,15 @@ files:
442
462
  - examples/snippets/partitioned-dml/db/seeds.rb
443
463
  - examples/snippets/partitioned-dml/models/album.rb
444
464
  - examples/snippets/partitioned-dml/models/singer.rb
465
+ - examples/snippets/query-logs/README.md
466
+ - examples/snippets/query-logs/Rakefile
467
+ - examples/snippets/query-logs/application.rb
468
+ - examples/snippets/query-logs/config/database.yml
469
+ - examples/snippets/query-logs/db/migrate/01_create_tables.rb
470
+ - examples/snippets/query-logs/db/schema.rb
471
+ - examples/snippets/query-logs/db/seeds.rb
472
+ - examples/snippets/query-logs/models/album.rb
473
+ - examples/snippets/query-logs/models/singer.rb
445
474
  - examples/snippets/quickstart/README.md
446
475
  - examples/snippets/quickstart/Rakefile
447
476
  - examples/snippets/quickstart/application.rb