Skip to content

Commit

Permalink
Implement Database#transaction (fix #42)
Browse files Browse the repository at this point in the history
Add an API for running transactions. The default transaction mode is immediate. For more details on transaction modes see: https://www.sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions.
  • Loading branch information
noteflakes authored Dec 22, 2023
1 parent 7801411 commit 95ebdb8
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 5 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,13 @@ db.pragma(:journal_mode) #=> 'wal'
# load an extension
db.load_extension('/path/to/extension.so')

# run queries in a transaction
db.transaction do
db.execute('insert into foo values (?)', 1)
db.execute('insert into foo values (?)', 2)
db.execute('insert into foo values (?)', 3)
end

# close database
db.close
db.closed? #=> true
Expand Down Expand Up @@ -206,6 +213,23 @@ ensure
end
```

### Running transactions

In order to run multiple queries in a single transaction, use
`Database#transaction`, passing a block that runs the queries. You can specify
the [transaction
mode](https://www.sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions).
The default mode is `:immediate`:

```ruby
db.transaction { ... } # Run an immediate transaction
db.transaction(:deferred) { ... } # Run a deferred transaction
db.transaction(:exclusive) { ... } # Run an exclusive transaction
```

If an exception is raised in the given block, the transaction will be rolled
back. Otherwise, it is committed.

### Creating Backups

You can use `Database#backup` to create backup copies of a database. The
Expand Down
23 changes: 23 additions & 0 deletions lib/extralite.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,29 @@ def pragma(value)
value.is_a?(Hash) ? pragma_set(value) : pragma_get(value)
end

# Starts a transaction and runs the given block. If an exception is raised
# in the block, the transaction is rolled back. Otherwise, the transaction
# is commited after running the block.
#
# db.transaction do
# db.execute('insert into foo values (1, 2, 3)')
# raise if db.query_single_value('select x from bar') > 42
# end
#
# @param mode [Symbol, String] transaction mode (deferred, immediate or exclusive). Defaults to immediate.
# @return [Any] the given block's return value
def transaction(mode = :immediate)
execute "begin #{mode} transaction"

abort = false
yield self
rescue
abort = true
raise
ensure
execute(abort ? 'rollback' : 'commit')
end

private

def pragma_set(values)
Expand Down
63 changes: 59 additions & 4 deletions test/test_database.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# frozen_string_literal: true

require_relative 'helper'

require 'date'
require 'tempfile'

class DatabaseTest < MiniTest::Test
def setup
Expand Down Expand Up @@ -385,7 +387,7 @@ def test_database_limit
end

def test_database_busy_timeout
fn = "/tmp/extralite-#{rand(10000)}.db"
fn = Tempfile.new('extralite_test_database_busy_timeout').path
db1 = Extralite::Database.new(fn)
db2 = Extralite::Database.new(fn)

Expand Down Expand Up @@ -493,11 +495,64 @@ def test_string_encoding
assert_equal 'foo', v
assert_equal 'UTF-8', v.encoding.name
end

def test_database_transaction_commit
path = Tempfile.new('extralite_test_database_transaction_commit').path
db1 = Extralite::Database.new(path)
db2 = Extralite::Database.new(path)

db1.execute('create table foo(x)')
assert_equal ['foo'], db1.tables
assert_equal ['foo'], db2.tables

q1 = Queue.new
q2 = Queue.new
th = Thread.new do
db1.transaction do
assert_equal true, db1.transaction_active?
db1.execute('insert into foo values (42)')
q1 << true
q2.pop
end
assert_equal false, db1.transaction_active?
end
q1.pop
# transaction not yet committed
assert_equal false, db2.transaction_active?
assert_equal [], db2.query('select * from foo')

q2 << true
th.join
# transaction now committed
assert_equal [{ x: 42 }], db2.query('select * from foo')
end

def test_database_transaction_rollback
db = Extralite::Database.new(':memory:')
db.execute('create table foo(x)')

assert_equal [], db.query('select * from foo')

exception = nil
begin
db.transaction do
db.execute('insert into foo values (42)')
raise 'bar'
end
rescue => e
exception = e
end

assert_equal [], db.query('select * from foo')
assert_kind_of RuntimeError, exception
assert_equal 'bar', exception.message
end
end

class ScenarioTest < MiniTest::Test
def setup
@db = Extralite::Database.new('/tmp/extralite.db')
@fn = Tempfile.new('extralite_scenario_test').path
@db = Extralite::Database.new(@fn)
@db.query('create table if not exists t (x,y,z)')
@db.query('delete from t')
@db.query('insert into t values (1, 2, 3)')
Expand All @@ -507,7 +562,7 @@ def setup
def test_concurrent_transactions
done = false
t = Thread.new do
db = Extralite::Database.new('/tmp/extralite.db')
db = Extralite::Database.new(@fn)
db.query 'begin immediate'
sleep 0.01 until done

Expand Down Expand Up @@ -615,7 +670,7 @@ def test_backup_with_schema_names
end

def test_backup_with_fn
tmp_fn = "/tmp/#{rand(86400)}.db"
tmp_fn = Tempfile.new('extralite_test_backup_with_fn').path
@src.backup(tmp_fn)

db = Extralite::Database.new(tmp_fn)
Expand Down
3 changes: 2 additions & 1 deletion test/test_iterator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ def test_iterator_inspect
end

def test_return_from_block_issue_26
db = Extralite::Database.new('/tmp/locked.db')
fn = Tempfile.new('extralite_test_return_from_block_issue_26').path
db = Extralite::Database.new(fn)

λ = ->(sql) {
db.prepare(sql).each { |r| r.each { |_, v| return v } }
Expand Down

0 comments on commit 95ebdb8

Please sign in to comment.