activerecord-spanner-adapter 1.5.1 → 1.6.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 (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