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
@@ -0,0 +1,28 @@
1
+ # Copyright 2022 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ class Track < ActiveRecord::Base
8
+ # Use the `composite_primary_key` gem to create a composite primary key definition for the model.
9
+ self.primary_keys = :singerid, :albumid, :trackid
10
+
11
+ # `tracks` is defined as INTERLEAVE IN PARENT `albums`. The primary key of `albums` is ()`singerid`, `albumid`).
12
+ belongs_to :album, foreign_key: [:singerid, :albumid]
13
+
14
+ # `tracks` also has a `singerid` column should be used to associate a Track with a Singer.
15
+ belongs_to :singer, foreign_key: :singerid
16
+
17
+ # Override the default initialize method to automatically set the singer attribute when an album is given.
18
+ def initialize attributes = nil
19
+ super
20
+ self.singer ||= album&.singer
21
+ end
22
+
23
+ def album=value
24
+ super
25
+ # Ensure the singer of this track is equal to the singer of the album that is set.
26
+ self.singer = value&.singer
27
+ end
28
+ end
@@ -0,0 +1,43 @@
1
+ # Sample - Query Logs
2
+
3
+ __NOTE__: Query logs require additional configuration for Cloud Spanner. Please read the entire file.
4
+
5
+ Rails 7.0 and higher supports [Query Logs](https://api.rubyonrails.org/classes/ActiveRecord/QueryLogs.html). Query Logs
6
+ can be used to automatically annotate all queries that are executed based on the current execution context.
7
+
8
+ The Cloud Spanner ActiveRecord provider can be used in combination with Query Logs. The query logs are automatically
9
+ translated to request tags for the queries.
10
+ See https://cloud.google.com/spanner/docs/introspection/troubleshooting-with-tags for more
11
+ information about request and transaction tags in Cloud Spanner.
12
+
13
+ ## Configuration
14
+ Using Query Logs with Cloud Spanner requires some specific configuration:
15
+ 1. You must set `ActiveRecord::QueryLogs.prepend_comment = true`
16
+ 2. You must include `{ request_tag: "true" }` as the first tag in your configuration.
17
+
18
+ ```ruby
19
+ ActiveRecord::QueryLogs.prepend_comment = true
20
+ config.active_record.query_log_tags = [
21
+ {
22
+ request_tag: "true",
23
+ },
24
+ :namespaced_controller,
25
+ :action,
26
+ :job,
27
+ {
28
+ request_id: ->(context) { context[:controller]&.request&.request_id },
29
+ job_id: ->(context) { context[:job]&.job_id },
30
+ tenant_id: -> { Current.tenant&.id },
31
+ static: "value",
32
+ },
33
+ ]
34
+ ```
35
+
36
+ The sample will automatically start a Spanner Emulator in a docker container and execute the sample
37
+ against that emulator. The emulator will automatically be stopped when the application finishes.
38
+
39
+ Run the application with the command
40
+
41
+ ```bash
42
+ bundle exec rake run
43
+ ```
@@ -0,0 +1,13 @@
1
+ # Copyright 2023 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ require_relative "../config/environment"
8
+ require "sinatra/activerecord/rake"
9
+
10
+ desc "Sample showing how to use automatic query log tagging on Cloud Spanner with ActiveRecord."
11
+ task :run do
12
+ Dir.chdir("..") { sh "bundle exec rake run[query-logs]" }
13
+ end
@@ -0,0 +1,63 @@
1
+ # Copyright 2023 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ require "io/console"
8
+ require_relative "../config/environment"
9
+ require_relative "models/singer"
10
+ require_relative "models/album"
11
+
12
+ class Application
13
+ def self.run
14
+ enable_query_logs
15
+
16
+ puts ""
17
+ puts "Query all Albums and include an automatically generated request tag"
18
+ albums = Album.all
19
+ puts "Queried #{albums.length} albums using an automatically generated request tag"
20
+
21
+ puts ""
22
+ puts "Press any key to end the application"
23
+ STDIN.getch
24
+ end
25
+
26
+ def self.enable_query_logs
27
+ # Enables Query Logs in a non-Rails application. Normally, this should be done
28
+ # as described here: https://api.rubyonrails.org/classes/ActiveRecord/QueryLogs.html
29
+ ActiveRecord.query_transformers << ActiveRecord::QueryLogs
30
+
31
+ # Query log comments *MUST* be prepended to be included as a request tag.
32
+ ActiveRecord::QueryLogs.prepend_comment = true
33
+
34
+ # This block manually enables Query Logs without a full Rails application.
35
+ # This should normally not be needed in your application.
36
+ ActiveRecord::QueryLogs.taggings.merge!(
37
+ application: "example-app",
38
+ action: "run-test-application",
39
+ pid: -> { Process.pid.to_s },
40
+ socket: ->(context) { context[:connection].pool.db_config.socket },
41
+ db_host: ->(context) { context[:connection].pool.db_config.host },
42
+ database: ->(context) { context[:connection].pool.db_config.database }
43
+ )
44
+
45
+ ActiveRecord::QueryLogs.tags = [
46
+ # The first tag *MUST* be the fixed value 'request_tag:true'.
47
+ {
48
+ request_tag: "true"
49
+ },
50
+ :controller,
51
+ :action,
52
+ :job,
53
+ {
54
+ request_id: ->(context) { context[:controller]&.request&.request_id },
55
+ job_id: ->(context) { context[:job]&.job_id }
56
+ },
57
+ :db_host,
58
+ :database
59
+ ]
60
+ end
61
+ end
62
+
63
+ Application.run
@@ -0,0 +1,8 @@
1
+ development:
2
+ adapter: spanner
3
+ emulator_host: localhost:9010
4
+ project: test-project
5
+ instance: test-instance
6
+ database: testdb
7
+ pool: 5
8
+ timeout: 5000
@@ -0,0 +1,21 @@
1
+ # Copyright 2023 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ class CreateTables < ActiveRecord::Migration[6.0]
8
+ def change
9
+ connection.ddl_batch do
10
+ create_table :singers do |t|
11
+ t.string :first_name
12
+ t.string :last_name
13
+ end
14
+
15
+ create_table :albums do |t|
16
+ t.string :title
17
+ t.references :singer, index: false, foreign_key: true
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ # This file is auto-generated from the current state of the database. Instead
2
+ # of editing this file, please use the migrations feature of Active Record to
3
+ # incrementally modify your database, and then regenerate this schema definition.
4
+ #
5
+ # This file is the source Rails uses to define your schema when running `bin/rails
6
+ # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7
+ # be faster and is potentially less error prone than running all of your
8
+ # migrations from scratch. Old migrations may fail to apply correctly if those
9
+ # migrations use external dependencies or application code.
10
+ #
11
+ # It's strongly recommended that you check this file into your version control system.
12
+
13
+ ActiveRecord::Schema[7.1].define(version: 1) do
14
+ connection.start_batch_ddl
15
+
16
+ create_table "albums", force: :cascade do |t|
17
+ t.string "title"
18
+ t.integer "singer_id", limit: 8
19
+ end
20
+
21
+ create_table "singers", force: :cascade do |t|
22
+ t.string "first_name"
23
+ t.string "last_name"
24
+ end
25
+
26
+ add_foreign_key "albums", "singers"
27
+ connection.run_batch
28
+ rescue
29
+ abort_batch
30
+ raise
31
+ end
@@ -0,0 +1,24 @@
1
+ # Copyright 2023 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ require_relative "../../config/environment.rb"
8
+ require_relative "../models/singer"
9
+ require_relative "../models/album"
10
+
11
+ first_names = ["Pete", "Alice", "John", "Ethel", "Trudy", "Naomi", "Wendy", "Ruben", "Thomas", "Elly"]
12
+ last_names = ["Wendelson", "Allison", "Peterson", "Johnson", "Henderson", "Ericsson", "Aronson", "Tennet", "Courtou"]
13
+
14
+ adjectives = ["daily", "happy", "blue", "generous", "cooked", "bad", "open"]
15
+ nouns = ["windows", "potatoes", "bank", "street", "tree", "glass", "bottle"]
16
+
17
+ 5.times do
18
+ Singer.create first_name: first_names.sample, last_name: last_names.sample
19
+ end
20
+
21
+ 20.times do
22
+ singer_id = Singer.all.sample.id
23
+ Album.create title: "#{adjectives.sample} #{nouns.sample}", singer_id: singer_id
24
+ end
@@ -0,0 +1,9 @@
1
+ # Copyright 2023 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ class Album < ActiveRecord::Base
8
+ belongs_to :singer
9
+ end
@@ -0,0 +1,9 @@
1
+ # Copyright 2023 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ class Singer < ActiveRecord::Base
8
+ has_many :albums
9
+ end
@@ -10,6 +10,15 @@ module ActiveRecord
10
10
  module ConnectionAdapters
11
11
  module Spanner
12
12
  class Column < ConnectionAdapters::Column
13
+ # rubocop:disable Style/MethodDefParentheses
14
+ def initialize(name, default, sql_type_metadata = nil, null = true,
15
+ default_function = nil, collation: nil, comment: nil,
16
+ primary_key: false, **)
17
+ # rubocop:enable Style/MethodDefParentheses
18
+ super
19
+ @primary_key = primary_key
20
+ end
21
+
13
22
  def has_default? # rubocop:disable Naming/PredicateName
14
23
  super && !virtual?
15
24
  end
@@ -17,6 +26,10 @@ module ActiveRecord
17
26
  def virtual?
18
27
  sql_type_metadata.generated
19
28
  end
29
+
30
+ def primary_key?
31
+ @primary_key
32
+ end
20
33
  end
21
34
  end
22
35
  end
@@ -6,14 +6,34 @@
6
6
 
7
7
  # frozen_string_literal: true
8
8
 
9
+ require "active_record/gem_version"
10
+
9
11
  module ActiveRecord
10
12
  module ConnectionAdapters
11
13
  module Spanner
12
14
  module DatabaseStatements
15
+ VERSION_7_1_0 = Gem::Version.create "7.1.0"
16
+ RequestOptions = Google::Cloud::Spanner::V1::RequestOptions
17
+
13
18
  # DDL, DML and DQL Statements
14
19
 
15
20
  def execute sql, name = nil, binds = []
21
+ internal_execute sql, name, binds
22
+ end
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
26
+ ActiveRecord::Result.new(
27
+ result.fields.keys.map(&:to_s), result.rows.map(&:values)
28
+ )
29
+ end
30
+
31
+ def internal_execute sql, name = "SQL", binds = [],
32
+ prepare: false, async: false # rubocop:disable Lint/UnusedMethodArgument
16
33
  statement_type = sql_statement_type sql
34
+ # Call `transform` to invoke any query transformers that might have been registered.
35
+ sql = transform sql
36
+ append_request_tag_from_query_logs sql, binds
17
37
 
18
38
  if preventing_writes? && [:dml, :ddl].include?(statement_type)
19
39
  raise ActiveRecord::ReadOnlyError(
@@ -24,49 +44,127 @@ module ActiveRecord
24
44
  if statement_type == :ddl
25
45
  execute_ddl sql
26
46
  else
27
- transaction_required = statement_type == :dml
28
- materialize_transactions
29
-
30
- # First process and remove any hints in the binds that indicate that
31
- # a different read staleness should be used than the default.
32
- staleness_hint = binds.find { |b| b.is_a? Arel::Visitors::StalenessHint }
33
- if staleness_hint
34
- selector = Google::Cloud::Spanner::Session.single_use_transaction staleness_hint.value
35
- binds.delete staleness_hint
36
- end
37
- request_options = binds.find { |b| b.is_a? Google::Cloud::Spanner::V1::RequestOptions }
38
- if request_options
39
- binds.delete request_options
40
- end
47
+ execute_query_or_dml statement_type, sql, name, binds
48
+ end
49
+ end
50
+
51
+ def execute_query_or_dml statement_type, sql, name, binds
52
+ transaction_required = statement_type == :dml
53
+ materialize_transactions
54
+
55
+ # First process and remove any hints in the binds that indicate that
56
+ # a different read staleness should be used than the default.
57
+ staleness_hint = binds.find { |b| b.is_a? Arel::Visitors::StalenessHint }
58
+ if staleness_hint
59
+ selector = Google::Cloud::Spanner::Session.single_use_transaction staleness_hint.value
60
+ binds.delete staleness_hint
61
+ end
62
+ request_options = binds.find { |b| b.is_a? RequestOptions }
63
+ if request_options
64
+ binds.delete request_options
65
+ end
66
+
67
+ log_args = [sql, name]
68
+ log_args.concat [binds, type_casted_binds(binds)] if log_statement_binds
41
69
 
42
- log_args = [sql, name]
43
- log_args.concat [binds, type_casted_binds(binds)] if log_statement_binds
44
-
45
- log(*log_args) do
46
- types, params = to_types_and_params binds
47
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
48
- if transaction_required
49
- transaction do
50
- @connection.execute_query sql, params: params, types: types, request_options: request_options
51
- end
52
- else
53
- @connection.execute_query sql, params: params, types: types, single_use_selector: selector,
54
- request_options: request_options
70
+ log(*log_args) do
71
+ types, params = to_types_and_params binds
72
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
73
+ if transaction_required
74
+ transaction do
75
+ @connection.execute_query sql, params: params, types: types, request_options: request_options
55
76
  end
77
+ else
78
+ @connection.execute_query sql, params: params, types: types, single_use_selector: selector,
79
+ request_options: request_options
56
80
  end
57
81
  end
58
82
  end
59
83
  end
60
84
 
61
- def query sql, name = nil
62
- exec_query sql, name
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
92
+ end
63
93
  end
64
94
 
65
- def exec_query sql, name = "SQL", binds = [], prepare: false # rubocop:disable Lint/UnusedMethodArgument
66
- result = execute sql, name, binds
67
- ActiveRecord::Result.new(
68
- result.fields.keys.map(&:to_s), result.rows.map(&:values)
69
- )
95
+ def append_request_tag_from_query_logs_with_format sql, binds, prefix
96
+ end_of_comment = sql.index "*/", prefix.length
97
+ return unless end_of_comment
98
+
99
+ request_tag = sql[prefix.length, end_of_comment - prefix.length]
100
+ options = binds.find { |bind| bind.is_a? RequestOptions } || RequestOptions.new
101
+ if options.request_tag == ""
102
+ options.request_tag = request_tag
103
+ else
104
+ options.request_tag += "," + request_tag
105
+ end
106
+
107
+ binds.append options
108
+ end
109
+
110
+ # The method signatures for executing queries and DML statements changed between Rails 7.0 and 7.1.
111
+
112
+ if ActiveRecord.gem_version >= VERSION_7_1_0
113
+ def sql_for_insert sql, pk, binds, returning
114
+ if pk && !_has_pk_binding(pk, binds)
115
+ # Add the primary key to the columns that should be returned if there is no value specified for it.
116
+ returning ||= []
117
+ returning |= if pk.respond_to? :each
118
+ pk
119
+ else
120
+ [pk]
121
+ end
122
+ end
123
+ if returning&.any?
124
+ returning_columns_statement = returning.map { |c| quote_column_name c }.join(", ")
125
+ sql = "#{sql} THEN RETURN #{returning_columns_statement}"
126
+ end
127
+
128
+ [sql, binds]
129
+ end
130
+
131
+ def query sql, name = nil
132
+ exec_query sql, name
133
+ end
134
+ else # ActiveRecord.gem_version < VERSION_7_1_0
135
+ def query sql, name = nil
136
+ exec_query sql, name
137
+ end
138
+
139
+ def exec_query sql, name = "SQL", binds = [], prepare: false # rubocop:disable Lint/UnusedMethodArgument
140
+ result = execute sql, name, binds
141
+ ActiveRecord::Result.new(
142
+ result.fields.keys.map(&:to_s), result.rows.map(&:values)
143
+ )
144
+ end
145
+
146
+ def sql_for_insert sql, pk, binds
147
+ if pk && !_has_pk_binding(pk, binds)
148
+ # Add the primary key to the columns that should be returned if there is no value specified for it.
149
+ returning_columns_statement = if pk.respond_to? :each
150
+ pk.map { |c| quote_column_name c }.join(", ")
151
+ else
152
+ quote_column_name pk
153
+ end
154
+ sql = "#{sql} THEN RETURN #{returning_columns_statement}" if returning_columns_statement
155
+ end
156
+ super
157
+ end
158
+ end
159
+
160
+ def _has_pk_binding pk, binds
161
+ if pk.respond_to? :each
162
+ has_value = true
163
+ pk.each { |col| has_value &&= binds.any? { |bind| bind.name == col } }
164
+ has_value
165
+ else
166
+ binds.any? { |bind| bind.name == pk }
167
+ end
70
168
  end
71
169
 
72
170
  def exec_mutation mutation
@@ -219,6 +317,9 @@ module ActiveRecord
219
317
  if bind.respond_to? :type
220
318
  type = ActiveRecord::Type::Spanner::SpannerActiveRecordConverter
221
319
  .convert_active_model_type_to_spanner(bind.type)
320
+ elsif bind.class == Symbol
321
+ # This ensures that for example :environment is sent as the string 'environment' to Cloud Spanner.
322
+ type = :STRING
222
323
  end
223
324
  [
224
325
  # Generates binds for named parameters in the format `@p1, @p2, ...`
@@ -226,7 +327,15 @@ module ActiveRecord
226
327
  ]
227
328
  end.to_h
228
329
  params = binds.enum_for(:each_with_index).map do |bind, i|
229
- type = bind.respond_to?(:type) ? bind.type : ActiveModel::Type::Integer
330
+ type = if bind.respond_to? :type
331
+ bind.type
332
+ elsif bind.class == Symbol
333
+ # This ensures that for example :environment is sent as the string 'environment' to Cloud Spanner.
334
+ :STRING
335
+ else
336
+ # The Cloud Spanner default type is INT64 if no other type is known.
337
+ ActiveModel::Type::Integer
338
+ end
230
339
  bind_value = bind.respond_to?(:value) ? bind.value : bind
231
340
  value = ActiveRecord::Type::Spanner::SpannerActiveRecordConverter
232
341
  .serialize_with_transaction_isolation_level(type, bind_value, :dml)
@@ -6,37 +6,19 @@
6
6
 
7
7
  module ActiveRecord
8
8
  module ConnectionAdapters
9
- class SpannerSchemaCache < SchemaCache
9
+ class SpannerSchemaCache
10
10
  def initialize conn
11
+ @connection = conn
11
12
  @primary_and_parent_keys = {}
12
- super
13
- end
14
-
15
- def initialize_dup other
16
- @primary_and_parent_keys = @primary_and_parent_keys.dup
17
- super
18
- end
19
-
20
- def encode_with coder
21
- coder["primary_and_parent_keys"] = @primary_and_parent_keys
22
- super
23
- end
24
-
25
- def init_with coder
26
- @primary_and_parent_keys = coder["primary_and_parent_keys"]
27
- super
28
13
  end
29
14
 
30
15
  def primary_and_parent_keys table_name
31
16
  @primary_and_parent_keys[table_name] ||=
32
- if data_source_exists? table_name
33
- connection.primary_and_parent_keys table_name
34
- end
17
+ @connection.primary_and_parent_keys table_name
35
18
  end
36
19
 
37
20
  def clear!
38
21
  @primary_and_parent_keys.clear
39
- super
40
22
  end
41
23
  end
42
24
  end
@@ -39,6 +39,17 @@ module ActiveRecord
39
39
 
40
40
  primary_keys = if o.primary_keys
41
41
  o.primary_keys
42
+ elsif o.options[:primary_key]
43
+ columns = if o.options[:primary_key].is_a? Array
44
+ o.options[:primary_key]
45
+ else
46
+ [o.options[:primary_key]]
47
+ end
48
+ pk_names = []
49
+ columns.each do |c|
50
+ pk_names.append c.to_s
51
+ end
52
+ PrimaryKeyDefinition.new pk_names
42
53
  else
43
54
  pk_names = o.columns.each_with_object [] do |c, r|
44
55
  if c.type == :primary_key || c.primary_key?
@@ -42,6 +42,10 @@ module ActiveRecord
42
42
 
43
43
  super
44
44
  end
45
+
46
+ def valid_column_definition_options
47
+ super + [:type, :array, :allow_commit_timestamp, :as, :stored, :parent_key, :passed_type, :index]
48
+ end
45
49
  end
46
50
 
47
51
  class Table < ActiveRecord::ConnectionAdapters::Table
@@ -109,7 +109,7 @@ module ActiveRecord
109
109
  information_schema { |i| i.table_columns table_name }
110
110
  end
111
111
 
112
- def new_column_from_field _table_name, field
112
+ def new_column_from_field _table_name, field, _definitions = nil
113
113
  Spanner::Column.new \
114
114
  field.name,
115
115
  field.default,
@@ -118,7 +118,8 @@ module ActiveRecord
118
118
  field.allow_commit_timestamp,
119
119
  field.generated),
120
120
  field.nullable,
121
- field.default_function
121
+ field.default_function,
122
+ primary_key: field.primary_key
122
123
  end
123
124
 
124
125
  def fetch_type_metadata sql_type, ordinal_position = nil, allow_commit_timestamp = nil, generated = nil
@@ -42,14 +42,6 @@ module ActiveRecord
42
42
  end
43
43
 
44
44
  module ConnectionAdapters
45
- module AbstractPool
46
- def get_schema_cache connection
47
- self.schema_cache ||= SpannerSchemaCache.new connection
48
- schema_cache.connection = connection
49
- schema_cache
50
- end
51
- end
52
-
53
45
  class SpannerAdapter < AbstractAdapter
54
46
  ADAPTER_NAME = "spanner".freeze
55
47
  NATIVE_DATABASE_TYPES = {
@@ -78,8 +70,10 @@ module ActiveRecord
78
70
  class_attribute :log_statement_binds, instance_writer: false, default: false
79
71
 
80
72
  def initialize connection, logger, connection_options, config
81
- super connection, logger, config
73
+ @connection = connection
82
74
  @connection_options = connection_options
75
+ super connection, logger, config
76
+ @raw_connection ||= connection
83
77
  end
84
78
 
85
79
  def max_identifier_length
@@ -117,6 +111,10 @@ module ActiveRecord
117
111
  end
118
112
  alias reconnect! reset!
119
113
 
114
+ def spanner_schema_cache
115
+ @spanner_schema_cache ||= SpannerSchemaCache.new self
116
+ end
117
+
120
118
  # Spanner Connection API
121
119
  delegate :ddl_batch, :ddl_batch?, :start_batch_ddl, :abort_batch, :run_batch, to: :@connection
122
120
 
@@ -186,6 +184,10 @@ module ActiveRecord
186
184
  SecureRandom.uuid.gsub("-", "").hex & 0x7FFFFFFFFFFFFFFF
187
185
  end
188
186
 
187
+ def return_value_after_insert? column
188
+ column.auto_incremented_by_db? || column.primary_key?
189
+ end
190
+
189
191
  def arel_visitor
190
192
  Arel::Visitors::Spanner.new self
191
193
  end
@@ -259,6 +261,23 @@ module ActiveRecord
259
261
  include TypeMapBuilder
260
262
  end
261
263
 
264
+ def transform sql
265
+ if ActiveRecord::VERSION::MAJOR >= 7
266
+ transform_query sql
267
+ else
268
+ sql
269
+ end
270
+ end
271
+
272
+ # 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)
279
+ end
280
+
262
281
  def translate_exception exception, message:, sql:, binds:
263
282
  if exception.is_a? Google::Cloud::FailedPreconditionError
264
283
  case exception.message