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