diff --git a/README.md b/README.md index 7bec973..1878f1a 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/lib/extralite.rb b/lib/extralite.rb index c0d5ad2..d1fc201 100644 --- a/lib/extralite.rb +++ b/lib/extralite.rb @@ -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) diff --git a/test/test_database.rb b/test/test_database.rb index 55c5c8e..010ec0e 100644 --- a/test/test_database.rb +++ b/test/test_database.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true require_relative 'helper' + require 'date' +require 'tempfile' class DatabaseTest < MiniTest::Test def setup @@ -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) @@ -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)') @@ -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 @@ -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) diff --git a/test/test_iterator.rb b/test/test_iterator.rb index c75703b..240764c 100644 --- a/test/test_iterator.rb +++ b/test/test_iterator.rb @@ -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 } }