test/test_database.rb in extralite-2.3 vs test/test_database.rb in extralite-2.4

- old
+ new

@@ -1,9 +1,12 @@ # frozen_string_literal: true require_relative 'helper' +require 'date' +require 'tempfile' + class DatabaseTest < MiniTest::Test def setup @db = Extralite::Database.new(':memory:') @db.query('create table if not exists t (x,y,z)') @db.query('delete from t') @@ -121,10 +124,13 @@ r = @db.query('select x, y, z from t where x = ?', 1) assert_equal [{ x: 1, y: 2, z: 3 }], r r = @db.query('select x, y, z from t where z = ?', 6) assert_equal [{ x: 4, y: 5, z: 6 }], r + + error = assert_raises(Extralite::ParameterError) { @db.query_single_value('select ?', Date.today) } + assert_equal error.message, 'Cannot bind parameter at position 1 of type Date' end def test_parameter_binding_with_index r = @db.query('select x, y, z from t where x = ?2', 0, 1) assert_equal [{ x: 1, y: 2, z: 3 }], r @@ -150,10 +156,98 @@ r = @db.query('select x, y, z from t where x = ?2', 1 => 42, 2 => 4) assert_equal [{ x: 4, y: 5, z: 6 }], r end + class Foo; end + + def test_parameter_binding_from_hash + assert_equal 42, @db.query_single_value('select :bar', foo: 41, bar: 42) + assert_equal 42, @db.query_single_value('select :bar', 'foo' => 41, 'bar' => 42) + assert_equal 42, @db.query_single_value('select ?8', 7 => 41, 8 => 42) + assert_nil @db.query_single_value('select :bar', foo: 41) + + error = assert_raises(Extralite::ParameterError) { @db.query_single_value('select ?', Foo.new => 42) } + assert_equal error.message, 'Cannot bind parameter with a key of type DatabaseTest::Foo' + + error = assert_raises(Extralite::ParameterError) { @db.query_single_value('select ?', %w[a b] => 42) } + assert_equal error.message, 'Cannot bind parameter with a key of type Array' + end + + def test_parameter_binding_from_struct + foo_bar = Struct.new(:":foo", :bar) + value = foo_bar.new(41, 42) + assert_equal 41, @db.query_single_value('select :foo', value) + assert_equal 42, @db.query_single_value('select :bar', value) + assert_nil @db.query_single_value('select :baz', value) + end + + def test_parameter_binding_from_data_class + skip "Data isn't supported in Ruby < 3.2" if RUBY_VERSION < '3.2' + + foo_bar = Data.define(:":foo", :bar) + value = foo_bar.new(":foo": 41, bar: 42) + assert_equal 42, @db.query_single_value('select :bar', value) + assert_nil @db.query_single_value('select :baz', value) + end + + def test_parameter_binding_for_blobs + sql = 'SELECT typeof(data) AS type, data FROM blobs WHERE ROWID = ?' + blob_path = File.expand_path('fixtures/image.png', __dir__) + @db.execute('CREATE TABLE blobs (data BLOB)') + + # it's a string, not a blob + @db.execute('INSERT INTO blobs VALUES (?)', 'Hello, 世界!') + result = @db.query_single_row(sql, @db.last_insert_rowid) + assert_equal 'text', result[:type] + assert_equal Encoding::UTF_8, result[:data].encoding + + data = File.binread(blob_path) + @db.execute('INSERT INTO blobs VALUES (?)', data) + result = @db.query_single_row(sql, @db.last_insert_rowid) + assert_equal 'blob', result[:type] + assert_equal data, result[:data] + + data = (+'Hello, 世界!').force_encoding(Encoding::ASCII_8BIT) + @db.execute('INSERT INTO blobs VALUES (?)', data) + result = @db.query_single_row(sql, @db.last_insert_rowid) + assert_equal 'blob', result[:type] + assert_equal Encoding::ASCII_8BIT, result[:data].encoding + assert_equal 'Hello, 世界!', result[:data].force_encoding(Encoding::UTF_8) + + data = Extralite::Blob.new('Hello, 世界!') + @db.execute('INSERT INTO blobs VALUES (?)', data) + result = @db.query_single_row(sql, @db.last_insert_rowid) + assert_equal 'blob', result[:type] + assert_equal Encoding::ASCII_8BIT, result[:data].encoding + assert_equal 'Hello, 世界!', result[:data].force_encoding(Encoding::UTF_8) + end + + def test_parameter_binding_for_simple_types + assert_nil @db.query_single_value('select ?', nil) + + # 32-bit integers + assert_equal -2** 31, @db.query_single_value('select ?', -2**31) + assert_equal 2**31 - 1, @db.query_single_value('select ?', 2**31 - 1) + + # 64-bit integers + assert_equal -2 ** 63, @db.query_single_value('select ?', -2 ** 63) + assert_equal 2**63 - 1, @db.query_single_value('select ?', 2**63 - 1) + + # floats + assert_equal Float::MIN, @db.query_single_value('select ?', Float::MIN) + assert_equal Float::MAX, @db.query_single_value('select ?', Float::MAX) + + # boolean + assert_equal 1, @db.query_single_value('select ?', true) + assert_equal 0, @db.query_single_value('select ?', false) + + # strings and symbols + assert_equal 'foo', @db.query_single_value('select ?', 'foo') + assert_equal 'foo', @db.query_single_value('select ?', :foo) + end + def test_value_casting r = @db.query_single_value("select 'abc'") assert_equal 'abc', r r = @db.query_single_value('select 123') @@ -291,11 +385,11 @@ assert_raises(Extralite::Error) { @db.limit(-999) } 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) db1.query('begin exclusive') assert_raises(Extralite::BusyError) { db2.query('begin exclusive') } @@ -386,31 +480,91 @@ def test_database_inspect db = Extralite::Database.new(':memory:') assert_match /^\#\<Extralite::Database:0x[0-9a-f]+ :memory:\>$/, db.inspect end + def test_database_inspect_on_closed_database + db = Extralite::Database.new(':memory:') + assert_match /^\#\<Extralite::Database:0x[0-9a-f]+ :memory:\>$/, db.inspect + db.close + assert_match /^\#\<Extralite::Database:0x[0-9a-f]+ \(closed\)\>$/, db.inspect + end + def test_string_encoding db = Extralite::Database.new(':memory:') v = db.query_single_value("select 'foo'") 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)') @db.query('insert into t values (4, 5, 6)') end 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 while true begin @@ -463,10 +617,11 @@ end def test_database_trace sqls = [] @db.trace { |sql| sqls << sql } + GC.start @db.query('select 1') assert_equal ['select 1'], sqls @db.query('select 2') @@ -513,12 +668,109 @@ @src.backup(@dst, 'main', 'temp') assert_equal [[1, 2, 3], [4, 5, 6]], @dst.query_ary('select * from temp.t') 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) assert_equal [[1, 2, 3], [4, 5, 6]], db.query_ary('select * from t') end end + +class GVLReleaseThresholdTest < Minitest::Test + def setup + @sql = <<~SQL + WITH RECURSIVE r(i) AS ( + VALUES(0) + UNION ALL + SELECT i FROM r + LIMIT 3000000 + ) + SELECT i FROM r WHERE i = 1; + SQL + end + + def test_default_gvl_release_threshold + db = Extralite::Database.new(':memory:') + assert_equal 1000, db.gvl_release_threshold + end + + def test_gvl_always_release + skip if !IS_LINUX + + delays = [] + running = true + t1 = Thread.new do + last = Time.now + while running + sleep 0.1 + now = Time.now + delays << (now - last) + last = now + end + end + t2 = Thread.new do + db = Extralite::Database.new(':memory:') + db.gvl_release_threshold = 1 + db.query(@sql) + ensure + running = false + end + t2.join + t1.join + + assert delays.size > 4 + assert_equal 0, delays.select { |d| d > 0.15 }.size + end + + def test_gvl_always_hold + skip if !IS_LINUX + + delays = [] + running = true + + signal = Queue.new + db = Extralite::Database.new(':memory:') + db.gvl_release_threshold = 0 + + t1 = Thread.new do + last = Time.now + while running + signal << true + sleep 0.1 + now = Time.now + delays << (now - last) + last = now + end + end + + t2 = Thread.new do + signal.pop + db.query(@sql) + ensure + running = false + end + t2.join + t1.join + + assert delays.size >= 1 + assert delays.first > 0.2 + end + + def test_gvl_mode_get_set + db = Extralite::Database.new(':memory:') + assert_equal 1000, db.gvl_release_threshold + + db.gvl_release_threshold = 42 + assert_equal 42, db.gvl_release_threshold + + db.gvl_release_threshold = 0 + assert_equal 0, db.gvl_release_threshold + + assert_raises(ArgumentError) { db.gvl_release_threshold = :foo } + + db.gvl_release_threshold = nil + assert_equal 1000, db.gvl_release_threshold + end +end \ No newline at end of file