From 503868f4344417e3f3e0d670a544d4c59f569938 Mon Sep 17 00:00:00 2001 From: "Brian J. Cardiff" Date: Wed, 3 Feb 2016 21:29:19 -0300 Subject: [PATCH] update to last db design --- samples/memory.cr | 8 +++- spec/driver_spec.cr | 83 +++++++++++++++++--------------------- src/sqlite3/connection.cr | 23 +++++++---- src/sqlite3/driver.cr | 4 +- src/sqlite3/lib_sqlite3.cr | 45 +++++++++++---------- src/sqlite3/result_set2.cr | 5 +++ src/sqlite3/statement2.cr | 33 +++++++-------- 7 files changed, 104 insertions(+), 97 deletions(-) diff --git a/samples/memory.cr b/samples/memory.cr index b9782f7..c68cd08 100644 --- a/samples/memory.cr +++ b/samples/memory.cr @@ -1,10 +1,14 @@ require "db" require "../src/sqlite3" -DB.open "sqlite3", ":memory:" do |db| +DB.open "sqlite3://%3Amemory%3A" do |db| db.exec "create table contacts (name string, age integer)" db.exec "insert into contacts values (?, ?)", "John Doe", 30 - db.exec "insert into contacts values (:name, :age)", {name: "Sarah", age: 33} + + args = [] of DB::Any + args << "Sarah" + args << 33 + db.exec "insert into contacts values (?, ?)", args puts "max age:" puts db.scalar "select max(age) from contacts" # => 33 diff --git a/spec/driver_spec.cr b/spec/driver_spec.cr index 276bcc5..f8c9943 100644 --- a/spec/driver_spec.cr +++ b/spec/driver_spec.cr @@ -1,13 +1,13 @@ require "./spec_helper" def with_db(&block : DB::Database ->) - DB.open "sqlite3", DB_FILENAME, &block + DB.open "sqlite3:#{DB_FILENAME}", &block ensure File.delete(DB_FILENAME) end def with_mem_db(&block : DB::Database ->) - DB.open "sqlite3", ":memory:", &block + DB.open "sqlite3://%3Amemory%3A", &block end def sql(s : String) @@ -40,14 +40,29 @@ def assert_single_read?(rs, value_type, value) rs.move_next.should be_false end +def assert_filename(uri, filename) + SQLite3::Connection.filename(URI.parse(uri)).should eq(filename) +end + describe Driver do it "should register sqlite3 name" do DB.driver_class("sqlite3").should eq(SQLite3::Driver) end + it "should get filename from uri" do + assert_filename("sqlite3:%3Amemory%3A", ":memory:") + assert_filename("sqlite3://%3Amemory%3A", ":memory:") + + assert_filename("sqlite3:./file.db", "./file.db") + assert_filename("sqlite3://./file.db", "./file.db") + + assert_filename("sqlite3:/path/to/file.db", "/path/to/file.db") + assert_filename("sqlite3:///path/to/file.db", "/path/to/file.db") + end + it "should use database option as file to open" do with_db do |db| - db.driver_class.should eq(SQLite3::Driver) + db.driver.should be_a(SQLite3::Driver) File.exists?(DB_FILENAME).should be_true end end @@ -55,7 +70,7 @@ describe Driver do {% 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(typeof({{value}}), "select #{sql({{value}})}").should eq({{value}}) + db.scalar("select #{sql({{value}})}").should eq({{value}}) db.query "select #{sql({{value}})}" do |rs| assert_single_read rs, typeof({{value}}), {{value}} @@ -63,19 +78,9 @@ describe Driver do end end - it "executes and select {{value.id}} as nillable" do - with_db do |db| - db.scalar?(typeof({{value}}), "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?(typeof({{value}}), "select null").should be_nil + db.scalar("select null").should be_nil db.query "select null" do |rs| assert_single_read? rs, typeof({{value}}), nil @@ -85,32 +90,26 @@ describe Driver do it "executes with bind {{value.id}}" do with_db do |db| - db.scalar(typeof({{value}}), %(select ?), {{value}}).should eq({{value}}) - end - end - - it "executes with bind {{value.id}} read as nillable" do - with_db do |db| - db.scalar?(typeof({{value}}), %(select ?), {{value}}).should eq({{value}}) + 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?(typeof({{value}}), %(select ?), nil).should be_nil + db.scalar("select ?", nil).should be_nil end end it "executes with bind {{value.id}} as array" do with_db do |db| - db.scalar?(typeof({{value}}), %(select ?), [{{value}}]).should eq({{value}}) + db.scalar(%(select ?), [{{value}}]).should eq({{value}}) end end {% end %} it "executes and selects blob" do with_db do |db| - slice = db.scalar(Slice(UInt8), %(select X'53514C697465')) + slice = db.scalar(%(select X'53514C697465')) as Slice(UInt8) slice.to_a.should eq([0x53, 0x51, 0x4C, 0x69, 0x74, 0x65]) end end @@ -118,23 +117,11 @@ describe Driver do it "executes with bind blob" do with_db do |db| ary = UInt8[0x53, 0x51, 0x4C, 0x69, 0x74, 0x65] - slice = db.scalar Slice(UInt8), %(select cast(? as BLOB)), Slice.new(ary.to_unsafe, ary.size) + slice = db.scalar(%(select cast(? as BLOB)), Slice.new(ary.to_unsafe, ary.size)) as Slice(UInt8) slice.to_a.should eq(ary) end end - it "executes with named bind using symbol" do - with_db do |db| - db.scalar(String, %(select :value), {value: "hello"}).should eq("hello") - end - end - - it "executes with named bind using string" do - with_db do |db| - db.scalar(String, %(select :value), {"value": "hello"}).should eq("hello") - end - end - it "gets column count" do with_mem_db do |db| db.exec "create table person (name string, age integer)" @@ -176,12 +163,13 @@ describe Driver do it "gets last insert row id" do with_mem_db do |db| - cnn = db.connection - cnn.exec "create table person (name string, age integer)" + db.exec "create table person (name string, age integer)" - cnn.last_insert_id.should eq(0) - cnn.exec %(insert into person values ("foo", 10)) - cnn.last_insert_id.should eq(1) + 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 @@ -190,7 +178,7 @@ describe Driver do with_db do |db| db.exec "create table table1 (col1 #{sqlite_type_for({{value}})})" db.exec %(insert into table1 values (#{sql({{value}})})) - db.scalar(typeof({{value}}), "select col1 from table1").should eq({{value}}) + db.scalar("select col1 from table1").should eq({{value}}) end end {% end %} @@ -201,7 +189,8 @@ describe Driver do db.exec "create table table1 (col1 blob)" db.exec %(insert into table1 values (?)), Slice.new(ary.to_unsafe, ary.size) - slice = db.scalar(Slice(UInt8), "select col1 from table1") + + slice = db.scalar("select cast(col1 as blob) from table1") as Slice(UInt8) slice.to_a.should eq(ary) end end @@ -228,13 +217,13 @@ describe Driver do it "ensures statements are closed" do begin - DB.open "sqlite3", DB_FILENAME do |db| + 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| + DB.open "sqlite3:#{DB_FILENAME}" do |db| begin db.query("SELECT i, str FROM a WHERE i = ?", 23) do |rs| rs.move_next diff --git a/src/sqlite3/connection.cr b/src/sqlite3/connection.cr index 805179b..6659e15 100644 --- a/src/sqlite3/connection.cr +++ b/src/sqlite3/connection.cr @@ -1,21 +1,28 @@ class SQLite3::Connection < DB::Connection - def initialize(connection_string) + def initialize(database) super - check LibSQLite3.open_v2(connection_string, out @db, (LibSQLite3::Flag::READWRITE | LibSQLite3::Flag::CREATE), nil) + filename = self.class.filename(database.uri) + check LibSQLite3.open_v2(filename, out @db, (LibSQLite3::Flag::READWRITE | LibSQLite3::Flag::CREATE), nil) end - def prepare(query) + def self.filename(uri : URI) + URI.unescape (if path = uri.path + (uri.host || "") + path + else + uri.opaque.not_nil! + end) + end + + def build_statement(query) Statement2.new(self, query) end - def perform_close + def do_close + @statements_cache.values.each &.close + super LibSQLite3.close_v2(self) end - def last_insert_id : Int64 - LibSQLite3.last_insert_rowid(self) - end - def to_unsafe @db end diff --git a/src/sqlite3/driver.cr b/src/sqlite3/driver.cr index 52f5d6a..4f206e6 100644 --- a/src/sqlite3/driver.cr +++ b/src/sqlite3/driver.cr @@ -1,6 +1,6 @@ class SQLite3::Driver < DB::Driver - def build_connection - SQLite3::Connection.new(connection_string) + def build_connection(db) + SQLite3::Connection.new(db) end end diff --git a/src/sqlite3/lib_sqlite3.cr b/src/sqlite3/lib_sqlite3.cr index 0c8b96e..4b089b8 100644 --- a/src/sqlite3/lib_sqlite3.cr +++ b/src/sqlite3/lib_sqlite3.cr @@ -6,37 +6,37 @@ lib LibSQLite3 type Statement = Void* enum Flag - READONLY = 0x00000001 # Ok for sqlite3_open_v2() - READWRITE = 0x00000002 # Ok for sqlite3_open_v2() - CREATE = 0x00000004 # Ok for sqlite3_open_v2() - DELETEONCLOSE = 0x00000008 # VFS only - EXCLUSIVE = 0x00000010 # VFS only - AUTOPROXY = 0x00000020 # VFS only - URI = 0x00000040 # Ok for sqlite3_open_v2() - MEMORY = 0x00000080 # Ok for sqlite3_open_v2() - MAIN_DB = 0x00000100 # VFS only - TEMP_DB = 0x00000200 # VFS only - TRANSIENT_DB = 0x00000400 # VFS only - MAIN_JOURNAL = 0x00000800 # VFS only - TEMP_JOURNAL = 0x00001000 # VFS only - SUBJOURNAL = 0x00002000 # VFS only - MASTER_JOURNAL = 0x00004000 # VFS only - NOMUTEX = 0x00008000 # Ok for sqlite3_open_v2() - FULLMUTEX = 0x00010000 # Ok for sqlite3_open_v2() - SHAREDCACHE = 0x00020000 # Ok for sqlite3_open_v2() - PRIVATECACHE = 0x00040000 # Ok for sqlite3_open_v2() - WAL = 0x00080000 # VFS only + READONLY = 0x00000001 # Ok for sqlite3_open_v2() + READWRITE = 0x00000002 # Ok for sqlite3_open_v2() + CREATE = 0x00000004 # Ok for sqlite3_open_v2() + DELETEONCLOSE = 0x00000008 # VFS only + EXCLUSIVE = 0x00000010 # VFS only + AUTOPROXY = 0x00000020 # VFS only + URI = 0x00000040 # Ok for sqlite3_open_v2() + MEMORY = 0x00000080 # Ok for sqlite3_open_v2() + MAIN_DB = 0x00000100 # VFS only + TEMP_DB = 0x00000200 # VFS only + TRANSIENT_DB = 0x00000400 # VFS only + MAIN_JOURNAL = 0x00000800 # VFS only + TEMP_JOURNAL = 0x00001000 # VFS only + SUBJOURNAL = 0x00002000 # VFS only + MASTER_JOURNAL = 0x00004000 # VFS only + NOMUTEX = 0x00008000 # Ok for sqlite3_open_v2() + FULLMUTEX = 0x00010000 # Ok for sqlite3_open_v2() + SHAREDCACHE = 0x00020000 # Ok for sqlite3_open_v2() + PRIVATECACHE = 0x00040000 # Ok for sqlite3_open_v2() + WAL = 0x00080000 # VFS only end enum Code - ROW = 100 + ROW = 100 DONE = 101 end alias Callback = (Void*, Int32, UInt8**, UInt8**) -> Int32 fun open = sqlite3_open_v2(filename : UInt8*, db : SQLite3*) : Int32 - fun open_v2 = sqlite3_open_v2(filename : UInt8*, db : SQLite3*, flags: Flag, zVfs : UInt8*) : Int32 + fun open_v2 = sqlite3_open_v2(filename : UInt8*, db : SQLite3*, flags : Flag, zVfs : UInt8*) : Int32 fun errcode = sqlite3_errcode(SQLite3) : Int32 fun errmsg = sqlite3_errmsg(SQLite3) : UInt8* @@ -62,6 +62,7 @@ lib LibSQLite3 fun reset = sqlite3_reset(stmt : Statement) : Int32 fun column_name = sqlite3_column_name(stmt : Statement, idx : Int32) : UInt8* fun last_insert_rowid = sqlite3_last_insert_rowid(db : SQLite3) : Int64 + fun changes = sqlite3_changes(db : SQLite3) : Int32 fun finalize = sqlite3_finalize(stmt : Statement) : Int32 fun close_v2 = sqlite3_close_v2(SQLite3) : Int32 diff --git a/src/sqlite3/result_set2.cr b/src/sqlite3/result_set2.cr index 21f930d..283c70b 100644 --- a/src/sqlite3/result_set2.cr +++ b/src/sqlite3/result_set2.cr @@ -1,6 +1,11 @@ class SQLite3::ResultSet2 < DB::ResultSet @column_index = 0 + protected def do_close + super + LibSQLite3.reset(self) + end + # Advances to the next row. Returns `true` if there's a next row, # `false` otherwise. Must be called at least once to advance to the first # row. diff --git a/src/sqlite3/statement2.cr b/src/sqlite3/statement2.cr index 25fbb55..d937868 100644 --- a/src/sqlite3/statement2.cr +++ b/src/sqlite3/statement2.cr @@ -4,29 +4,30 @@ class SQLite3::Statement2 < DB::Statement check LibSQLite3.prepare_v2(@connection, sql, sql.bytesize + 1, out @stmt, nil) end - protected def begin_parameters + protected def perform_query(args : Slice(DB::Any)) LibSQLite3.reset(self) + args.each_with_index(1) do |arg, index| + bind_arg(index, arg) + end + ResultSet2.new(self) + end + + protected def perform_exec(args : Slice(DB::Any)) + rs = perform_query(args) + rs.move_next + rs.close + + rows_affected = LibSQLite3.changes(connection) + last_id = LibSQLite3.last_insert_rowid(connection) + + DB::ExecResult.new rows_affected, last_id end protected def on_close + super check LibSQLite3.finalize(self) end - protected def add_parameter(index : Int32, value) - bind_arg(index + 1, value) - end - - protected def add_parameter(name : String, value) - converted_name = ":#{name}" - index = LibSQLite3.bind_parameter_index(self, converted_name) - raise "Unknown parameter: #{name}" if index == 0 - bind_arg(index, value) - end - - protected def perform - ResultSet2.new(self) - end - private def bind_arg(index, value : Nil) check LibSQLite3.bind_null(self, index) end