Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions examples/snippets/batch-dml/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Sample - Batch DML

This example shows how to use [Batch DML](https://cloud.google.com/spanner/docs/dml-tasks#use-batch)
with the Spanner ActiveRecord adapter.

The sample will automatically start a Spanner Emulator in a docker container and execute the sample
against that emulator. The emulator will automatically be stopped when the application finishes.

Run the application with the command

```bash
bundle exec rake run
```
13 changes: 13 additions & 0 deletions examples/snippets/batch-dml/Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright 2025 Google LLC
#
# Use of this source code is governed by an MIT-style
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.

require_relative "../config/environment"
require "sinatra/activerecord/rake"

desc "Sample showing how to use Batch DML with the Spanner ActiveRecord provider."
task :run do
Dir.chdir("..") { sh "bundle exec rake run[batch-dml]" }
end
54 changes: 54 additions & 0 deletions examples/snippets/batch-dml/application.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Copyright 2025 Google LLC
#
# Use of this source code is governed by an MIT-style
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.

require "io/console"
require_relative "../config/environment"
require_relative "models/singer"
require_relative "models/album"

class Application
def self.run
first_names = %w[Pete Alice John Ethel Trudy Naomi Wendy Ruben Thomas Elly]
last_names = %w[Wendelson Allison Peterson Johnson Henderson Ericsson Aronson Tennet Courtou]

# Insert 5 new singers using Batch DML.
ActiveRecord::Base.transaction do
# The Base.dml_batch function starts a DML batch. All DML statements that are
# generated by ActiveRecord inside the block that is given will be added to
# the current batch. The batch is executed at the end of the block. The data
# that has been written is readable after the block ends.
ActiveRecord::Base.dml_batch do
5.times do
Singer.create first_name: first_names.sample, last_name: last_names.sample
end
end
# Data that has been inserted/update using Batch DML can be read in the same
# transaction as the one that added/updated the data. This is different from
# mutations, as mutations do not support read-your-writes.
singers = Singer.all
puts "Inserted #{singers.count} singers in one batch"
end

# Batch DML can also be used to update existing data.
ActiveRecord::Base.transaction do
# Start a DML batch.
singers = nil
ActiveRecord::Base.dml_batch do
# Queries can be executed inside a DML batch.
# These are executed directly and do not affect the DML batch.
singers = Singer.all
singers.each do |singer|
singer.picture = Base64.encode64 SecureRandom.alphanumeric(SecureRandom.random_number(10..200))
singer.save
end
end
puts "Updated #{singers.count} singers in one batch"
end
end
end


Application.run
9 changes: 9 additions & 0 deletions examples/snippets/batch-dml/config/database.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
development:
adapter: spanner
emulator_host: localhost:9010
project: test-project
instance: test-instance
database: testdb
pool: 5
timeout: 5000
schema_dump: false
23 changes: 23 additions & 0 deletions examples/snippets/batch-dml/db/migrate/01_create_tables.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright 2025 Google LLC
#
# Use of this source code is governed by an MIT-style
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.

class CreateTables < ActiveRecord::Migration[6.0]
def change
connection.ddl_batch do
create_table :singers do |t|
t.string :first_name
t.string :last_name
t.binary :picture
end

create_table :albums do |t|
t.string :title
t.numeric :marketing_budget
t.references :singer, index: false, foreign_key: true
end
end
end
end
8 changes: 8 additions & 0 deletions examples/snippets/batch-dml/db/seeds.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Copyright 2025 Google LLC
#
# Use of this source code is governed by an MIT-style
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.

# This file is intentionally kept empty, as this sample
# does not need any initial data.
9 changes: 9 additions & 0 deletions examples/snippets/batch-dml/models/album.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Copyright 2025 Google LLC
#
# Use of this source code is governed by an MIT-style
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.

class Album < ActiveRecord::Base
belongs_to :singer
end
9 changes: 9 additions & 0 deletions examples/snippets/batch-dml/models/singer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Copyright 2025 Google LLC
#
# Use of this source code is governed by an MIT-style
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.

class Singer < ActiveRecord::Base
has_many :albums
end
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@ def execute sql, name = nil, binds = []

def internal_exec_query sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false
result = internal_execute sql, name, binds, prepare: prepare, async: async, allow_retry: allow_retry
ActiveRecord::Result.new(
result.fields.keys.map(&:to_s), result.rows.map(&:values)
)
if result
ActiveRecord::Result.new(
result.fields.keys.map(&:to_s), result.rows.map(&:values)
)
else
ActiveRecord::Result.new [], []
end
end

def internal_execute sql, name = "SQL", binds = [],
Expand Down Expand Up @@ -72,11 +76,12 @@ def execute_query_or_dml statement_type, sql, name, binds
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
if transaction_required
transaction do
@connection.execute_query sql, params: params, types: types, request_options: request_options
@connection.execute_query sql, params: params, types: types, request_options: request_options,
statement_type: statement_type
end
else
@connection.execute_query sql, params: params, types: types, single_use_selector: selector,
request_options: request_options
request_options: request_options, statement_type: statement_type
end
end
end
Expand Down Expand Up @@ -142,9 +147,13 @@ def query sql, name = nil

def exec_query sql, name = "SQL", binds = [], prepare: false # rubocop:disable Lint/UnusedMethodArgument
result = execute sql, name, binds
ActiveRecord::Result.new(
result.fields.keys.map(&:to_s), result.rows.map(&:values)
)
if result.respond_to? :fields
ActiveRecord::Result.new(
result.fields.keys.map(&:to_s), result.rows.map(&:values)
)
else
ActiveRecord::Result.new [], []
end
end

def sql_for_insert sql, pk, binds
Expand Down Expand Up @@ -191,6 +200,7 @@ def update arel, name = nil, binds = []

def exec_update sql, name = "SQL", binds = []
result = execute sql, name, binds
return unless result
# Make sure that we consume the entire result stream before trying to get the stats.
# This is required because the ExecuteStreamingSql RPC is also used for (Partitioned) DML,
# and this RPC can return multiple partial result sets for DML as well. Only the last partial
Expand Down Expand Up @@ -243,6 +253,9 @@ def transaction requires_new: nil, isolation: nil, joinable: true
retry
end
raise
rescue Google::Cloud::AbortedError => err
sleep(delay_from_aborted(err) || backoff *= 1.3)
retry
end
end

Expand Down
3 changes: 2 additions & 1 deletion lib/active_record/connection_adapters/spanner_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ def spanner_schema_cache
end

# Spanner Connection API
delegate :ddl_batch, :ddl_batch?, :start_batch_ddl, :abort_batch, :run_batch,
delegate :dml_batch, :dml_batch?, :start_batch_dml,
:ddl_batch, :ddl_batch?, :start_batch_ddl, :abort_batch, :run_batch,
:isolation_level, :isolation_level=, to: :@connection

def current_spanner_transaction
Expand Down
4 changes: 4 additions & 0 deletions lib/activerecord_spanner_adapter/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ def self.buffered_mutations?
spanner_adapter? && connection&.current_spanner_transaction&.isolation == :buffered_mutations
end

def self.dml_batch(&)
connection.dml_batch(&)
end

def self._should_use_standard_insert_record? values
!(buffered_mutations? || (primary_key && values.is_a?(Hash))) || !spanner_adapter?
end
Expand Down
Loading
Loading