Skip to content

Commit

Permalink
Add Database#transaction :skip_transaction option to skip creating a …
Browse files Browse the repository at this point in the history
…transaction or savepoint

Use this to support skipping transactions in
Dataset#{import,paged_each}.  Also use this to simplify other code
that used conditionals for whether or not to support transactions.
  • Loading branch information
jeremyevans committed Sep 21, 2023
1 parent 2490770 commit 89cdc61
Show file tree
Hide file tree
Showing 8 changed files with 49 additions and 17 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
=== master

* Support skipping transactions in Dataset#{import,paged_each} using :skip_transaction option (jeremyevans)

* Add Database#transaction :skip_transaction option to skip creating a transaction or savepoint (jeremyevans)

* Stop using a transaction for a single query if calling Dataset#import with a dataset (jeremyevans)

* Add paged_update_delete plugin for paged deletes and updates (jeremyevans) (#2080)
Expand Down
5 changes: 3 additions & 2 deletions lib/sequel/database/schema_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -712,8 +712,9 @@ def create_table_indexes_from_generator(name, generator, options)
e = options[:ignore_index_errors] || options[:if_not_exists]
generator.indexes.each do |index|
begin
pr = proc{index_sql_list(name, [index]).each{|sql| execute_ddl(sql)}}
supports_transactional_ddl? ? transaction(:savepoint=>:only, &pr) : pr.call
transaction(:savepoint=>:only, :skip_transaction=>supports_transactional_ddl? == false) do
index_sql_list(name, [index]).each{|sql| execute_ddl(sql)}
end
rescue Error
raise unless e
end
Expand Down
6 changes: 6 additions & 0 deletions lib/sequel/database/transactions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ def rollback_checker(opts=OPTS)
# uses :auto_savepoint, you can set this to false to not use a savepoint.
# If the value given for this option is :only, it will only create a
# savepoint if it is inside a transaction.
# :skip_transaction :: If set, do not actually open a transaction or savepoint,
# just checkout a connection and yield it.
#
# PostgreSQL specific options:
#
Expand Down Expand Up @@ -193,6 +195,10 @@ def transaction(opts=OPTS, &block)
end
else
synchronize(opts[:server]) do |conn|
if opts[:skip_transaction]
return yield(conn)
end

if opts[:savepoint] == :only
if supports_savepoints?
if _trans(conn)
Expand Down
12 changes: 7 additions & 5 deletions lib/sequel/dataset/actions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,8 @@ def get(column=(no_arg=true; nil), &block)
# This does not have an effect if +values+ is a Dataset.
# :server :: Set the server/shard to use for the transaction and insert
# queries.
# :skip_transaction :: Do not use a transaction even when using multiple
# INSERT queries.
# :slice :: Same as :commit_every, :commit_every takes precedence.
def import(columns, values, opts=OPTS)
return insert(columns, values) if values.is_a?(Dataset)
Expand Down Expand Up @@ -588,6 +590,8 @@ def multi_insert(hashes, opts=OPTS)
# if your ORDER BY expressions are not simple columns, if they contain
# qualified identifiers that would be ambiguous unqualified, if they contain
# any identifiers that are aliased in SELECT, and potentially other cases.
# :skip_transaction :: Do not use a transaction. This can be useful if you want to prevent
# a lock on the database table, at the expense of consistency.
#
# Examples:
#
Expand Down Expand Up @@ -1111,11 +1115,9 @@ def _aggregate(function, arg)
# are provided. When only a single value or statement is provided, then yield
# without using a transaction.
def _import_transaction(values, trans_opts, &block)
if values.length > 1
@db.transaction(trans_opts, &block)
else
yield
end
# OK to mutate trans_opts as it is generated by _import
trans_opts[:skip_transaction] = true if values.length <= 1
@db.transaction(trans_opts, &block)
end

# Internals of +select_hash+ and +select_hash_groups+
Expand Down
6 changes: 1 addition & 5 deletions lib/sequel/extensions/migration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -482,11 +482,7 @@ def checked_transaction(migration, &block)
@use_transactions
end

if use_trans
db.transaction(&block)
else
yield
end
db.transaction(:skip_transaction=>use_trans == false, &block)
end

# Load the migration file, raising an exception if the file does not define
Expand Down
11 changes: 7 additions & 4 deletions lib/sequel/model/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1945,8 +1945,10 @@ def checked_save_failure(opts)
end

# If transactions should be used, wrap the yield in a transaction block.
def checked_transaction(opts=OPTS)
use_transaction?(opts) ? db.transaction({:server=>this_server}.merge!(opts)){yield} : yield
def checked_transaction(opts=OPTS, &block)
h = {:server=>this_server}.merge!(opts)
h[:skip_transaction] = true unless use_transaction?(opts)
db.transaction(h, &block)
end

# Change the value of the column to given value, recording the change.
Expand Down Expand Up @@ -2148,8 +2150,9 @@ def [](*args)
# # DELETE FROM artists WHERE (id = 2)
# # ...
def destroy
pr = proc{all(&:destroy).length}
model.use_transactions ? @db.transaction(:server=>opts[:server], &pr) : pr.call
@db.transaction(:server=>opts[:server], :skip_transaction=>model.use_transactions == false) do
all(&:destroy).length
end
end

# If there is no order already defined on this dataset, order it by
Expand Down
13 changes: 13 additions & 0 deletions spec/core/dataset_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3374,6 +3374,14 @@ def supports_cte_in_subselect?; false end
'COMMIT']
end

it "should accept a :skip_transaction option to skip transactions" do
@ds.import([:x, :y], [[1, 2], [3, 4]], :skip_transaction=>true)
@db.sqls.must_equal [
"INSERT INTO items (x, y) VALUES (1, 2)",
"INSERT INTO items (x, y) VALUES (3, 4)",
]
end

it "should accept a columns array and a dataset" do
@ds2 = @ds.from(:cats).filter(:purr => true).select(:a, :b)

Expand Down Expand Up @@ -5458,6 +5466,11 @@ class << Sequel
sqls[-1].must_equal 'COMMIT'
end

it "should not use a transaction if the :skip_transaction option is given" do
@ds.paged_each(:skip_transaction=>true, &@proc)
@ds.db.sqls.must_equal ['SELECT * FROM test ORDER BY x LIMIT 1000 OFFSET 0']
end

it "should use a limit and offset to go through the dataset in chunks at a time" do
@ds.paged_each(&@proc)
@ds.db.sqls[1...-1].must_equal ['SELECT * FROM test ORDER BY x LIMIT 1000 OFFSET 0']
Expand Down
9 changes: 8 additions & 1 deletion spec/model/record_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ def o.autoincrementing_primary_key() :y end
DB.sqls.must_equal ["BEGIN", "UPDATE items SET y = NULL WHERE (id = 3)", "COMMIT"]
end

it "should use :transaction option if given" do
it "should use :transaction option if given to override use_transactions setting" do
o = @c.load(:id => 3, :x => 1, :y => nil)
o.use_transactions = true
o.save(:columns=>:y, :transaction=>false)
Expand All @@ -283,6 +283,13 @@ def o.autoincrementing_primary_key() :y end
DB.sqls.must_equal ["BEGIN", "UPDATE items SET y = NULL WHERE (id = 3)", "COMMIT"]
end

it "should respect :skip_transaction=>true option if given" do
o = @c.load(:id => 3, :x => 1, :y => nil)
o.use_transactions = true
o.save(:columns=>:y, :skip_transaction=>true)
DB.sqls.must_equal ["UPDATE items SET y = NULL WHERE (id = 3)"]
end

it "should rollback if before_save calls cancel_action and raise_on_save_failure = true" do
o = @c.load(:id => 3, :x => 1, :y => nil)
o.use_transactions = true
Expand Down

0 comments on commit 89cdc61

Please sign in to comment.