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
@@ -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