activerecord-spanner-adapter 2.3.0 → 2.5.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -1
  3. data/.github/workflows/acceptance-tests-on-emulator.yaml +4 -2
  4. data/.github/workflows/acceptance-tests-on-production.yaml +4 -4
  5. data/.github/workflows/ci.yaml +4 -2
  6. data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +4 -2
  7. data/.github/workflows/nightly-acceptance-tests-on-production.yaml +3 -3
  8. data/.github/workflows/nightly-unit-tests.yaml +4 -2
  9. data/.github/workflows/rubocop.yaml +3 -3
  10. data/.github/workflows/samples.yaml +1 -1
  11. data/.release-please-manifest.json +1 -1
  12. data/CHANGELOG.md +19 -0
  13. data/Gemfile +3 -2
  14. data/README.md +1 -0
  15. data/acceptance/cases/migration/index_test.rb +2 -2
  16. data/acceptance/cases/tasks/database_tasks_test.rb +25 -4
  17. data/acceptance/cases/transactions/read_write_transactions_test.rb +29 -0
  18. data/acceptance/cases/type/all_types_test.rb +15 -4
  19. data/acceptance/schema/schema.rb +2 -0
  20. data/activerecord-spanner-adapter.gemspec +2 -2
  21. data/examples/snippets/batch-dml/README.md +13 -0
  22. data/examples/snippets/batch-dml/Rakefile +13 -0
  23. data/examples/snippets/batch-dml/application.rb +54 -0
  24. data/examples/snippets/batch-dml/config/database.yml +11 -0
  25. data/examples/snippets/batch-dml/db/migrate/01_create_tables.rb +23 -0
  26. data/examples/snippets/batch-dml/db/seeds.rb +8 -0
  27. data/examples/snippets/batch-dml/models/album.rb +9 -0
  28. data/examples/snippets/batch-dml/models/singer.rb +9 -0
  29. data/examples/snippets/partitioned-dml/application.rb +31 -0
  30. data/lib/active_record/connection_adapters/spanner/column.rb +20 -7
  31. data/lib/active_record/connection_adapters/spanner/database_statements.rb +79 -24
  32. data/lib/active_record/connection_adapters/spanner/errors/transaction_mutation_limit_exceeded_error.rb +25 -0
  33. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +5 -1
  34. data/lib/active_record/connection_adapters/spanner/schema_statements.rb +32 -12
  35. data/lib/active_record/connection_adapters/spanner/type_mapping.rb +26 -0
  36. data/lib/active_record/connection_adapters/spanner_adapter.rb +10 -18
  37. data/lib/active_record/type/spanner/spanner_active_record_converter.rb +1 -0
  38. data/lib/active_record/type/spanner/uuid.rb +19 -0
  39. data/lib/activerecord_spanner_adapter/base.rb +4 -0
  40. data/lib/activerecord_spanner_adapter/connection.rb +129 -11
  41. data/lib/activerecord_spanner_adapter/transaction.rb +25 -4
  42. data/lib/activerecord_spanner_adapter/version.rb +1 -1
  43. data/lib/spanner_client_ext.rb +3 -2
  44. metadata +22 -5
@@ -6,9 +6,13 @@
6
6
 
7
7
  require "google/cloud/spanner"
8
8
  require "spanner_client_ext"
9
+ require "active_record/connection_adapters/spanner/type_mapping"
9
10
  require "activerecord_spanner_adapter/information_schema"
11
+ require_relative "../active_record/connection_adapters/spanner/errors/transaction_mutation_limit_exceeded_error"
10
12
 
11
13
  module ActiveRecordSpannerAdapter
14
+ TransactionMutationLimitExceededError = Google::Cloud::Spanner::Errors::TransactionMutationLimitExceededError
15
+
12
16
  class Connection
13
17
  attr_reader :instance_id
14
18
  attr_reader :database_id
@@ -40,10 +44,16 @@ module ActiveRecordSpannerAdapter
40
44
  end
41
45
  end
42
46
 
47
+ def native_database_types
48
+ NATIVE_DATABASE_TYPES
49
+ end
50
+
43
51
  # Clears the cached information about the underlying information schemas.
44
52
  # Call this method if you drop and recreate a database with the same name
45
53
  # to prevent the cached information to be used for the new database.
46
54
  def self.reset_information_schemas!
55
+ return unless @database
56
+
47
57
  @information_schemas.each_value do |info_schema|
48
58
  info_schema.connection.disconnect!
49
59
  end
@@ -159,6 +169,12 @@ module ActiveRecordSpannerAdapter
159
169
  false
160
170
  end
161
171
 
172
+ # Returns true if this connection is currently executing a DML batch, and otherwise false.
173
+ def dml_batch?
174
+ return true if @dml_batch
175
+ false
176
+ end
177
+
162
178
  ##
163
179
  # Starts a manual DDL batch. The batch must be ended by calling either run_batch or abort_batch.
164
180
  #
@@ -173,18 +189,39 @@ module ActiveRecordSpannerAdapter
173
189
  # raise
174
190
  # end
175
191
  def start_batch_ddl
176
- if @ddl_batch
177
- raise Google::Cloud::FailedPreconditionError, "A DDL batch is already active on this connection"
192
+ if @ddl_batch && @dml_batch
193
+ raise Google::Cloud::FailedPreconditionError, "Batch is already active on this connection"
178
194
  end
179
195
  @ddl_batch = []
180
196
  end
181
197
 
198
+ ##
199
+ # Starts a manual DML batch. The batch must be ended by calling either run_batch or abort_batch.
200
+ #
201
+ # @example
202
+ # begin
203
+ # connection.start_batch_dml
204
+ # connection.execute_query "insert into `Users` (Id, Name) VALUES (1, 'Test 1')"
205
+ # connection.execute_query "insert into `Users` (Id, Name) VALUES (2, 'Test 2')"
206
+ # connection.run_batch
207
+ # rescue StandardError
208
+ # connection.abort_batch
209
+ # raise
210
+ # end
211
+ def start_batch_dml
212
+ if @ddl_batch || @dml_batch
213
+ raise Google::Cloud::FailedPreconditionError, "A batch is already active on this connection"
214
+ end
215
+ @dml_batch = []
216
+ end
217
+
182
218
  ##
183
219
  # Aborts the current batch on this connection. This is a no-op if there is no batch on this connection.
184
220
  #
185
221
  # @see start_batch_ddl
186
222
  def abort_batch
187
223
  @ddl_batch = nil
224
+ @dml_batch = nil
188
225
  end
189
226
 
190
227
  ##
@@ -193,10 +230,21 @@ module ActiveRecordSpannerAdapter
193
230
  #
194
231
  # @see start_batch_ddl
195
232
  def run_batch
196
- unless @ddl_batch
233
+ unless @ddl_batch || @dml_batch
197
234
  raise Google::Cloud::FailedPreconditionError, "There is no batch active on this connection"
198
235
  end
199
236
  # Just return if the batch is empty.
237
+ return true if @ddl_batch&.empty? || @dml_batch&.empty?
238
+ begin
239
+ if @ddl_batch
240
+ run_ddl_batch
241
+ else
242
+ run_dml_batch
243
+ end
244
+ end
245
+ end
246
+
247
+ def run_ddl_batch
200
248
  return true if @ddl_batch.empty?
201
249
  begin
202
250
  execute_ddl_statements @ddl_batch, nil, true
@@ -205,9 +253,43 @@ module ActiveRecordSpannerAdapter
205
253
  end
206
254
  end
207
255
 
256
+ def run_dml_batch
257
+ return true if @dml_batch.empty?
258
+ begin
259
+ # Execute the DML statements in the batch.
260
+ execute_dml_statements_in_batch @dml_batch
261
+ ensure
262
+ @dml_batch = nil
263
+ end
264
+ end
265
+
266
+ ##
267
+ # Executes a set of DML statements as one batch. This method raises an error if no block is given.
268
+ def dml_batch
269
+ raise Google::Cloud::FailedPreconditionError, "No block given for the DML batch" unless block_given?
270
+ begin
271
+ start_batch_dml
272
+ yield
273
+ run_batch
274
+ rescue StandardError
275
+ abort_batch
276
+ raise
277
+ ensure
278
+ @dml_batch = nil
279
+ end
280
+ end
281
+
208
282
  # DQL, DML Statements
209
283
 
210
- def execute_query sql, params: nil, types: nil, single_use_selector: nil, request_options: nil
284
+ def execute_query sql, params: nil, types: nil, single_use_selector: nil, request_options: nil, statement_type: nil
285
+ # Clear the transaction from the previous statement.
286
+ unless current_transaction&.active?
287
+ self.current_transaction = nil
288
+ end
289
+ if statement_type == :dml && dml_batch?
290
+ @dml_batch.push({ sql: sql, params: params, types: types })
291
+ return
292
+ end
211
293
  if params
212
294
  converted_params, types =
213
295
  Google::Cloud::Spanner::Convert.to_input_params_and_types(
@@ -215,11 +297,6 @@ module ActiveRecordSpannerAdapter
215
297
  )
216
298
  end
217
299
 
218
- # Clear the transaction from the previous statement.
219
- unless current_transaction&.active?
220
- self.current_transaction = nil
221
- end
222
-
223
300
  selector = transaction_selector || single_use_selector
224
301
  execute_sql_request sql, converted_params, types, selector, request_options
225
302
  end
@@ -250,6 +327,9 @@ module ActiveRecordSpannerAdapter
250
327
  end
251
328
  raise
252
329
  rescue Google::Cloud::Error => e
330
+ if TransactionMutationLimitExceededError.is_mutation_limit_error? e
331
+ raise
332
+ end
253
333
  # Check if it was the first statement in a transaction that included a BeginTransaction
254
334
  # option in the request. If so, execute an explicit BeginTransaction and then retry the
255
335
  # request without the BeginTransaction option.
@@ -274,9 +354,11 @@ module ActiveRecordSpannerAdapter
274
354
 
275
355
  # Transactions
276
356
 
277
- def begin_transaction isolation = nil
357
+ def begin_transaction isolation = nil, **options
278
358
  raise "Nested transactions are not allowed" if current_transaction&.active?
279
- self.current_transaction = Transaction.new self, isolation || @isolation_level
359
+ exclude_from_streams = options.fetch :exclude_txn_from_change_streams, false
360
+ self.current_transaction = Transaction.new self, isolation || @isolation_level,
361
+ exclude_txn_from_change_streams: exclude_from_streams
280
362
  current_transaction.begin
281
363
  current_transaction
282
364
  end
@@ -343,6 +425,42 @@ module ActiveRecordSpannerAdapter
343
425
  job.done?
344
426
  end
345
427
 
428
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
429
+ def execute_dml_statements_in_batch statements
430
+ selector = transaction_selector
431
+ response = session.batch_update selector, current_transaction&.next_sequence_number do |batch|
432
+ statements.each do |statement|
433
+ batch.batch_update statement[:sql], params: statement[:params], types: statement[:types]
434
+ end
435
+ end
436
+ batch_update_results = Google::Cloud::Spanner::BatchUpdateResults.new response
437
+ first_res = response.result_sets.first
438
+ current_transaction.grpc_transaction = first_res.metadata.transaction \
439
+ if current_transaction && first_res&.metadata&.transaction
440
+ batch_update_results.row_counts
441
+ rescue Google::Cloud::AbortedError
442
+ # Mark the current transaction as aborted to prevent any unnecessary further requests on the transaction.
443
+ current_transaction&.mark_aborted
444
+ raise
445
+ rescue Google::Cloud::Spanner::BatchUpdateError => e
446
+ # Check if the status is ABORTED, and if it is, just propagate an AbortedError.
447
+ if e.cause&.code == GRPC::Core::StatusCodes::ABORTED
448
+ current_transaction&.mark_aborted
449
+ raise e.cause
450
+ end
451
+
452
+ # Check if the request returned a transaction or not.
453
+ # BatchDML is capable of returning BOTH an error and a transaction.
454
+ if current_transaction && !current_transaction.grpc_transaction? && selector&.begin&.read_write
455
+ selector = create_transaction_after_failed_first_statement e
456
+ retry
457
+ end
458
+ # It was not the first statement, or it returned a transaction, so propagate the error.
459
+ raise
460
+ end
461
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
462
+
463
+
346
464
  ##
347
465
  # Retrieves the delay value from Google::Cloud::AbortedError or
348
466
  # GRPC::Aborted
@@ -7,14 +7,21 @@
7
7
  module ActiveRecordSpannerAdapter
8
8
  class Transaction
9
9
  attr_reader :state
10
+ attr_reader :commit_options
11
+ attr_reader :begin_transaction_selector
12
+ attr_accessor :exclude_txn_from_change_streams
10
13
 
11
- def initialize connection, isolation
14
+
15
+
16
+ def initialize connection, isolation, commit_options = nil, exclude_txn_from_change_streams: false
12
17
  @connection = connection
13
18
  @isolation = isolation
14
19
  @committable = ![:read_only, :pdml].include?(isolation) && !isolation.is_a?(Hash)
15
20
  @state = :INITIALIZED
16
21
  @sequence_number = 0
17
22
  @mutations = []
23
+ @commit_options = commit_options
24
+ @exclude_txn_from_change_streams = exclude_txn_from_change_streams
18
25
  end
19
26
 
20
27
  def active?
@@ -59,7 +66,8 @@ module ActiveRecordSpannerAdapter
59
66
  @begin_transaction_selector = Google::Cloud::Spanner::V1::TransactionSelector.new \
60
67
  begin: Google::Cloud::Spanner::V1::TransactionOptions.new(
61
68
  read_write: Google::Cloud::Spanner::V1::TransactionOptions::ReadWrite.new,
62
- isolation_level: grpc_isolation
69
+ isolation_level: grpc_isolation,
70
+ exclude_txn_from_change_streams: @exclude_txn_from_change_streams
63
71
  )
64
72
  end
65
73
  @state = :STARTED
@@ -95,14 +103,23 @@ module ActiveRecordSpannerAdapter
95
103
  @sequence_number += 1 if @committable
96
104
  end
97
105
 
106
+ # Sets the commit options for this transaction.
107
+ # This is used to set the options for the commit RPC, such as return_commit_stats and max_commit_delay.
108
+ def set_commit_options options # rubocop:disable Naming/AccessorMethodName
109
+ @commit_options = options&.dup
110
+ end
111
+
98
112
  def commit
99
113
  raise "This transaction is not active" unless active?
100
114
 
101
115
  begin
102
116
  # Start a transaction with an explicit BeginTransaction RPC if the transaction only contains mutations.
103
117
  force_begin_read_write if @committable && !@mutations.empty? && !@grpc_transaction
104
-
105
- @connection.session.commit_transaction @grpc_transaction, @mutations if @committable && @grpc_transaction
118
+ if @committable && @grpc_transaction
119
+ @connection.session.commit_transaction @grpc_transaction,
120
+ @mutations,
121
+ commit_options: commit_options
122
+ end
106
123
  @state = :COMMITTED
107
124
  rescue Google::Cloud::NotFoundError => e
108
125
  if @connection.session_not_found? e
@@ -148,6 +165,10 @@ module ActiveRecordSpannerAdapter
148
165
  @grpc_transaction = Google::Cloud::Spanner::Transaction.from_grpc grpc, @connection.session
149
166
  end
150
167
 
168
+ def grpc_transaction?
169
+ @grpc_transaction if @grpc_transaction
170
+ end
171
+
151
172
  def transaction_selector
152
173
  return unless active?
153
174
 
@@ -5,5 +5,5 @@
5
5
  # https://opensource.org/licenses/MIT.
6
6
 
7
7
  module ActiveRecordSpannerAdapter
8
- VERSION = "2.3.0".freeze
8
+ VERSION = "2.5.0".freeze
9
9
  end
@@ -23,13 +23,14 @@ module Google
23
23
  end
24
24
 
25
25
  class Session
26
- def commit_transaction transaction, mutations = []
26
+ def commit_transaction transaction, mutations = [], commit_options: nil
27
27
  ensure_service!
28
28
 
29
29
  resp = service.commit(
30
30
  path,
31
31
  mutations,
32
- transaction_id: transaction.transaction_id
32
+ transaction_id: transaction.transaction_id,
33
+ commit_options: commit_options
33
34
  )
34
35
  @last_updated_at = Time.now
35
36
  Convert.timestamp_to_time resp.commit_timestamp
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-spanner-adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.0
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Google LLC
@@ -75,16 +75,22 @@ dependencies:
75
75
  name: bundler
76
76
  requirement: !ruby/object:Gem::Requirement
77
77
  requirements:
78
- - - "~>"
78
+ - - ">="
79
79
  - !ruby/object:Gem::Version
80
80
  version: '2.0'
81
+ - - "<"
82
+ - !ruby/object:Gem::Version
83
+ version: '5.0'
81
84
  type: :development
82
85
  prerelease: false
83
86
  version_requirements: !ruby/object:Gem::Requirement
84
87
  requirements:
85
- - - "~>"
88
+ - - ">="
86
89
  - !ruby/object:Gem::Version
87
90
  version: '2.0'
91
+ - - "<"
92
+ - !ruby/object:Gem::Version
93
+ version: '5.0'
88
94
  - !ruby/object:Gem::Dependency
89
95
  name: google-style
90
96
  requirement: !ruby/object:Gem::Requirement
@@ -147,14 +153,14 @@ dependencies:
147
153
  requirements:
148
154
  - - "~>"
149
155
  - !ruby/object:Gem::Version
150
- version: '5.2'
156
+ version: '5.4'
151
157
  type: :development
152
158
  prerelease: false
153
159
  version_requirements: !ruby/object:Gem::Requirement
154
160
  requirements:
155
161
  - - "~>"
156
162
  - !ruby/object:Gem::Version
157
- version: '5.2'
163
+ version: '5.4'
158
164
  - !ruby/object:Gem::Dependency
159
165
  name: rake
160
166
  requirement: !ruby/object:Gem::Requirement
@@ -365,6 +371,14 @@ files:
365
371
  - examples/snippets/auto-generated-primary-key/db/seeds.rb
366
372
  - examples/snippets/auto-generated-primary-key/models/album.rb
367
373
  - examples/snippets/auto-generated-primary-key/models/singer.rb
374
+ - examples/snippets/batch-dml/README.md
375
+ - examples/snippets/batch-dml/Rakefile
376
+ - examples/snippets/batch-dml/application.rb
377
+ - examples/snippets/batch-dml/config/database.yml
378
+ - examples/snippets/batch-dml/db/migrate/01_create_tables.rb
379
+ - examples/snippets/batch-dml/db/seeds.rb
380
+ - examples/snippets/batch-dml/models/album.rb
381
+ - examples/snippets/batch-dml/models/singer.rb
368
382
  - examples/snippets/bin/create_emulator_instance.rb
369
383
  - examples/snippets/bit-reversed-sequence/README.md
370
384
  - examples/snippets/bit-reversed-sequence/Rakefile
@@ -538,12 +552,14 @@ files:
538
552
  - examples/solidus/README.md
539
553
  - lib/active_record/connection_adapters/spanner/column.rb
540
554
  - lib/active_record/connection_adapters/spanner/database_statements.rb
555
+ - lib/active_record/connection_adapters/spanner/errors/transaction_mutation_limit_exceeded_error.rb
541
556
  - lib/active_record/connection_adapters/spanner/quoting.rb
542
557
  - lib/active_record/connection_adapters/spanner/schema_cache.rb
543
558
  - lib/active_record/connection_adapters/spanner/schema_creation.rb
544
559
  - lib/active_record/connection_adapters/spanner/schema_definitions.rb
545
560
  - lib/active_record/connection_adapters/spanner/schema_dumper.rb
546
561
  - lib/active_record/connection_adapters/spanner/schema_statements.rb
562
+ - lib/active_record/connection_adapters/spanner/type_mapping.rb
547
563
  - lib/active_record/connection_adapters/spanner/type_metadata.rb
548
564
  - lib/active_record/connection_adapters/spanner_adapter.rb
549
565
  - lib/active_record/tasks/spanner_database_tasks.rb
@@ -551,6 +567,7 @@ files:
551
567
  - lib/active_record/type/spanner/bytes.rb
552
568
  - lib/active_record/type/spanner/spanner_active_record_converter.rb
553
569
  - lib/active_record/type/spanner/time.rb
570
+ - lib/active_record/type/spanner/uuid.rb
554
571
  - lib/activerecord-spanner-adapter.rb
555
572
  - lib/activerecord_spanner_adapter/base.rb
556
573
  - lib/activerecord_spanner_adapter/connection.rb