diff --git a/shard.yml b/shard.yml index 28ff11d..dc1d2c7 100644 --- a/shard.yml +++ b/shard.yml @@ -4,7 +4,7 @@ version: 0.8.2 dependencies: db: github: crystal-lang/crystal-db - version: ~> 0.4.0 + version: ~> 0.4.1 authors: - Ary Borenszweig diff --git a/spec/db_spec.cr b/spec/db_spec.cr new file mode 100644 index 0000000..658525c --- /dev/null +++ b/spec/db_spec.cr @@ -0,0 +1,122 @@ +require "./spec_helper" +require "db/spec" + +private class NotSupportedType +end + +private def cast_if_blob(expr, sql_type) + case sql_type + when "blob" + "cast(#{expr} as blob)" + else + expr + end +end + +DB::DriverSpecs(DB::Any).run do + support_unprepared false + + before do + File.delete(DB_FILENAME) if File.exists?(DB_FILENAME) + end + after do + File.delete(DB_FILENAME) if File.exists?(DB_FILENAME) + end + + connection_string "sqlite3:#{DB_FILENAME}" + # ? can use many ... (:memory:) + + sample_value true, "int", "1", type_safe_value: false + sample_value false, "int", "0", type_safe_value: false + sample_value 2, "int", "2", type_safe_value: false + sample_value 1_i64, "int", "1" + sample_value "hello", "text", "'hello'" + sample_value 1.5_f32, "float", "1.5", type_safe_value: false + sample_value 1.5, "float", "1.5" + sample_value Time.new(2016, 2, 15), "text", "'2016-02-15 00:00:00.000'", type_safe_value: false + sample_value Time.new(2016, 2, 15, 10, 15, 30), "text", "'2016-02-15 10:15:30.000'", type_safe_value: false + + ary = UInt8[0x53, 0x51, 0x4C, 0x69, 0x74, 0x65] + sample_value Bytes.new(ary.to_unsafe, ary.size), "blob", "X'53514C697465'" # , type_safe_value: false + + binding_syntax do |index| + "?" + end + + create_table_1column_syntax do |table_name, col1| + "create table #{table_name} (#{col1.name} #{col1.sql_type} #{col1.null ? "NULL" : "NOT NULL"})" + end + + create_table_2columns_syntax do |table_name, col1, col2| + "create table #{table_name} (#{col1.name} #{col1.sql_type} #{col1.null ? "NULL" : "NOT NULL"}, #{col2.name} #{col2.sql_type} #{col2.null ? "NULL" : "NOT NULL"})" + end + + select_1column_syntax do |table_name, col1| + "select #{cast_if_blob(col1.name, col1.sql_type)} from #{table_name}" + end + + select_2columns_syntax do |table_name, col1, col2| + "select #{cast_if_blob(col1.name, col1.sql_type)}, #{cast_if_blob(col2.name, col2.sql_type)} from #{table_name}" + end + + select_count_syntax do |table_name| + "select count(*) from #{table_name}" + end + + select_scalar_syntax do |expression, sql_type| + "select #{cast_if_blob(expression, sql_type)}" + end + + insert_1column_syntax do |table_name, col, expression| + "insert into #{table_name} (#{col.name}) values (#{expression})" + end + + insert_2columns_syntax do |table_name, col1, expr1, col2, expr2| + "insert into #{table_name} (#{col1.name}, #{col2.name}) values (#{expr1}, #{expr2})" + end + + drop_table_if_exists_syntax do |table_name| + "drop table if exists #{table_name}" + end + + it "gets last insert row id", prepared: :both do |db| + db.exec "create table person (name string, age integer)" + db.exec %(insert into person values ("foo", 10)) + res = db.exec %(insert into person values ("foo", 10)) + res.last_insert_id.should eq(2) + res.rows_affected.should eq(1) + end + + # TODO timestamp support + + it "raises on unsupported param types" do |db| + expect_raises Exception, "SQLite3::Statement does not support NotSupportedType params" do + db.query "select ?", NotSupportedType.new + end + # TODO raising exception does not close the connection and pool is exhausted + end + + it "ensures statements are closed" do |db| + db.exec %(create table if not exists a (i int not null, str text not null);) + db.exec %(insert into a (i, str) values (23, "bai bai");) + + 2.times do |i| + DB.open db.uri do |db| + begin + db.query("SELECT i, str FROM a WHERE i = ?", 23) do |rs| + rs.move_next + break + end + rescue e : SQLite3::Exception + fail("Expected no exception, but got \"#{e.message}\"") + end + + begin + db.exec("UPDATE a SET i = ? WHERE i = ?", 23, 23) + rescue e : SQLite3::Exception + fail("Expected no exception, but got \"#{e.message}\"") + end + end + end + end +end diff --git a/spec/driver_spec.cr b/spec/driver_spec.cr index 5c63f86..fae5ba1 100644 --- a/spec/driver_spec.cr +++ b/spec/driver_spec.cr @@ -1,41 +1,9 @@ require "./spec_helper" -def sql(s : String) - "#{s.inspect}" -end - -def sql(s : Bool) - "#{s ? 1 : 0}" -end - -def sql(s) - "#{s}" -end - -def sqlite_type_for(v) - case v - when String ; "text" - when Bool, Int32, Int64; "int" - when Float32, Float64 ; "float" - when Time ; "text" - else - raise "not implemented for #{typeof(v)}" - end -end - -def assert_single_read(rs, value_type, value) - rs.move_next.should be_true - rs.read(value_type).should eq(value) - rs.move_next.should be_false -end - def assert_filename(uri, filename) SQLite3::Connection.filename(URI.parse(uri)).should eq(filename) end -class NotSupportedType -end - describe Driver do it "should register sqlite3 name" do DB.driver_class("sqlite3").should eq(SQLite3::Driver) @@ -58,249 +26,4 @@ describe Driver do File.exists?(DB_FILENAME).should be_true end end - - {% for value in [1, 1_i64, "hello", 1.5, 1.5_f32] %} - it "executes and select {{value.id}}" do - with_db do |db| - db.scalar("select #{sql({{value}})}").should eq({{value}}) - - db.query "select #{sql({{value}})}" do |rs| - assert_single_read rs, typeof({{value}}), {{value}} - end - end - end - - it "executes and select nil as type of {{value.id}}" do - with_db do |db| - db.scalar("select null").should be_nil - - db.query "select null" do |rs| - assert_single_read rs, typeof({{value}} || nil), nil - end - end - end - - it "executes with bind {{value.id}}" do - with_db do |db| - db.scalar(%(select ?), {{value}}).should eq({{value}}) - end - end - - it "executes with bind nil as typeof {{value.id}}" do - with_db do |db| - db.scalar("select ?", nil).should be_nil - end - end - - it "executes with bind {{value.id}} as array" do - with_db do |db| - db.scalar(%(select ?), [{{value}}]).should eq({{value}}) - end - end - {% end %} - - it "executes and selects blob" do - with_db do |db| - slice = db.scalar(%(select X'53514C697465')).as(Bytes) - slice.to_a.should eq([0x53, 0x51, 0x4C, 0x69, 0x74, 0x65]) - end - end - - it "executes with bind blob" do - with_db do |db| - ary = UInt8[0x53, 0x51, 0x4C, 0x69, 0x74, 0x65] - slice = db.scalar(%(select cast(? as BLOB)), Bytes.new(ary.to_unsafe, ary.size)).as(Bytes) - slice.to_a.should eq(ary) - end - end - - it "gets column count" do - with_mem_db do |db| - db.exec "create table person (name string, age integer)" - - db.query "select * from person" do |rs| - rs.column_count.should eq(2) - end - end - end - - it "gets column name" do - with_mem_db do |db| - db.exec "create table person (name string, age integer)" - - db.query "select * from person" do |rs| - rs.column_name(0).should eq("name") - rs.column_name(1).should eq("age") - end - end - end - - it "gets last insert row id" do - with_mem_db do |db| - db.exec "create table person (name string, age integer)" - - db.exec %(insert into person values ("foo", 10)) - - res = db.exec %(insert into person values ("foo", 10)) - res.last_insert_id.should eq(2) - res.rows_affected.should eq(1) - end - end - - {% for value in [true, false, 1, 1_i64, "hello", 1.5, 1.5_f32] %} - it "insert/get value {{value.id}} from table" do - with_db do |db| - db.exec "create table table1 (col1 #{sqlite_type_for({{value}})})" - db.exec %(insert into table1 values (#{sql({{value}})})) - db.query_one("select col1 from table1", as: typeof({{value}})).should eq({{value}}) - end - end - - it "insert/get value {{value.id}} using bind" do - with_db do |db| - db.exec "create table table1 (col1 #{sqlite_type_for({{value}})})" - db.exec %(insert into table1 (col1) values (?)), {{value}} - db.query_one("select col1 from table1", as: typeof({{value}})).should eq({{value}}) - end - end - {% end %} - - it "insert/get blob value from table" do - with_db do |db| - ary = UInt8[0x53, 0x51, 0x4C, 0x69, 0x74, 0x65] - - db.exec "create table table1 (col1 blob)" - db.exec %(insert into table1 values (?)), Bytes.new(ary.to_unsafe, ary.size) - - slice = db.scalar("select cast(col1 as blob) from table1").as(Bytes) - slice.to_a.should eq(ary) - end - end - - it "insert/get value date from table" do - with_db do |db| - value = Time.new(2016, 7, 22, 15, 0, 0, 0) - db.exec "create table table1 (col1 #{sqlite_type_for(value)})" - db.exec %(insert into table1 values (?)), value - db.query_one("select col1 from table1", as: Time).should eq(value) - end - end - - it "raises on unsupported param types" do - with_db do |db| - expect_raises Exception, "SQLite3::Statement does not support NotSupportedType params" do - db.query "select 1", NotSupportedType.new - end - # TODO raising exception does not close the connection and pool is exhausted - end - - with_db do |db| - expect_raises Exception, "SQLite3::Statement does not support NotSupportedType params" do - db.exec "select 1", NotSupportedType.new - end - end - end - - it "gets many rows from table" do - with_mem_db do |db| - db.exec "create table person (name string, age integer)" - db.exec %(insert into person values ("foo", 10)) - db.exec %(insert into person values ("bar", 20)) - db.exec %(insert into person values ("baz", 30)) - - names = [] of String - ages = [] of Int32 - db.query "select * from person" do |rs| - rs.each do - names << rs.read(String) - ages << rs.read(Int32) - end - end - names.should eq(["foo", "bar", "baz"]) - ages.should eq([10, 20, 30]) - end - end - - it "ensures statements are closed" do - begin - DB.open "sqlite3:#{DB_FILENAME}" do |db| - db.exec %(create table if not exists a (i int not null, str text not null);) - db.exec %(insert into a (i, str) values (23, "bai bai");) - end - - 2.times do |i| - DB.open "sqlite3:#{DB_FILENAME}" do |db| - begin - db.query("SELECT i, str FROM a WHERE i = ?", 23) do |rs| - rs.move_next - break - end - rescue e : SQLite3::Exception - fail("Expected no exception, but got \"#{e.message}\"") - end - - begin - db.exec("UPDATE a SET i = ? WHERE i = ?", 23, 23) - rescue e : SQLite3::Exception - fail("Expected no exception, but got \"#{e.message}\"") - end - end - end - ensure - File.delete(DB_FILENAME) - end - end - - describe "transactions" do - it "can read inside transaction and rollback after" do - with_db do |db| - db.exec "create table person (name varchar(25))" - db.transaction do |tx| - tx.connection.scalar("select count(*) from person").should eq(0) - tx.connection.exec "insert into person (name) values (?)", "John Doe" - tx.connection.scalar("select count(*) from person").should eq(1) - tx.rollback - end - db.scalar("select count(*) from person").should eq(0) - end - end - - it "can read inside transaction or after commit" do - with_db do |db| - db.exec "create table person (name varchar(25))" - db.transaction do |tx| - tx.connection.scalar("select count(*) from person").should eq(0) - tx.connection.exec "insert into person (name) values (?)", "John Doe" - tx.connection.scalar("select count(*) from person").should eq(1) - # using other connection - db.scalar("select count(*) from person").should eq(0) - end - db.scalar("select count(*) from person").should eq(1) - end - end - end - - describe "nested transactions" do - it "can read inside transaction and rollback after" do - with_db do |db| - db.exec "create table person (name varchar(25))" - db.transaction do |tx_0| - tx_0.connection.scalar("select count(*) from person").should eq(0) - tx_0.connection.exec "insert into person (name) values (?)", "John Doe" - tx_0.transaction do |tx_1| - tx_1.connection.exec "insert into person (name) values (?)", "Sarah" - tx_1.connection.scalar("select count(*) from person").should eq(2) - tx_1.transaction do |tx_2| - tx_2.connection.exec "insert into person (name) values (?)", "Jimmy" - tx_2.connection.scalar("select count(*) from person").should eq(3) - tx_2.rollback - end - end - tx_0.connection.scalar("select count(*) from person").should eq(2) - tx_0.rollback - end - db.scalar("select count(*) from person").should eq(0) - end - end - end end diff --git a/src/sqlite3/result_set.cr b/src/sqlite3/result_set.cr index da47a6d..9a886f1 100644 --- a/src/sqlite3/result_set.cr +++ b/src/sqlite3/result_set.cr @@ -51,18 +51,34 @@ class SQLite3::ResultSet < DB::ResultSet read(Int64).to_i32 end + def read(type : Int32?.class) : Int32? + read(Int64?).try &.to_i32 + end + def read(t : Float32.class) : Float32 read(Float64).to_f32 end + def read(type : Float32?.class) : Float32? + read(Float64?).try &.to_f32 + end + def read(t : Time.class) : Time Time.parse read(String), SQLite3::DATE_FORMAT end + def read(t : Time?.class) : Time? + read(String?).try { |v| Time.parse(v, SQLite3::DATE_FORMAT) } + end + def read(t : Bool.class) : Bool read(Int64) != 0 end + def read(t : Bool?.class) : Bool? + read(Int64?).try &.!=(0) + end + def column_count LibSQLite3.column_count(self) end