diff --git a/.travis.yml b/.travis.yml index 2a0f62a..7ef359b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,9 @@ language: crystal -sudo: required -dist: trusty +before_install: + - sudo apt-get autoremove sqlite3 + - sudo apt-get install python-software-properties + - sudo apt-add-repository -y ppa:travis-ci/sqlite3 + - sudo apt-get -y update + - sudo apt-cache show sqlite3 + - sudo apt-get install sqlite3=3.7.15.1-1~travis1 + - sudo sqlite3 -version \ No newline at end of file diff --git a/shard.yml b/shard.yml index c61beb0..f5fdb04 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: sqlite3 -version: 0.1.0 +version: 0.4.0 dependencies: db: diff --git a/spec/connection_spec.cr b/spec/connection_spec.cr new file mode 100644 index 0000000..2a0b0d8 --- /dev/null +++ b/spec/connection_spec.cr @@ -0,0 +1,47 @@ +require "./spec_helper" + +private def dump(source, target) + source.using_connection do |conn| + conn = conn.as(SQLite3::Connection) + target.using_connection do |backup_conn| + backup_conn = backup_conn.as(SQLite3::Connection) + conn.dump(backup_conn) + end + end +end + +describe Connection do + it "opens a database and then backs it up to another db" do + with_db do |db| + with_db("./test2.db") do |backup_db| + db.exec "create table person (name string, age integer)" + db.exec "insert into person values (\"foo\", 10)" + + dump db, backup_db + + backup_name = backup_db.scalar "select name from person" + backup_age = backup_db.scalar "select age from person" + source_name = db.scalar "select name from person" + source_age = db.scalar "select age from person" + + {backup_name, backup_age}.should eq({source_name, source_age}) + end + end + end + + it "opens a database, inserts records, dumps to an in-memory db, insers some more, then dumps to the source" do + with_db do |db| + with_mem_db do |in_memory_db| + db.exec "create table person (name string, age integer)" + db.exec "insert into person values (\"foo\", 10)" + dump db, in_memory_db + + in_memory_db.scalar("select count(*) from person").should eq(1) + in_memory_db.exec "insert into person values (\"bar\", 22)" + dump in_memory_db, db + + db.scalar("select count(*) from person").should eq(2) + end + end + end +end diff --git a/spec/driver_spec.cr b/spec/driver_spec.cr index 19da869..f966213 100644 --- a/spec/driver_spec.cr +++ b/spec/driver_spec.cr @@ -1,17 +1,5 @@ require "./spec_helper" -DB_FILENAME = "./test.db" - -def with_db(&block : DB::Database ->) - DB.open "sqlite3:#{DB_FILENAME}", &block -ensure - File.delete(DB_FILENAME) -end - -def with_mem_db(&block : DB::Database ->) - DB.open "sqlite3://%3Amemory%3A", &block -end - def sql(s : String) "#{s.inspect}" end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 0e7f82f..343fd6b 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -2,3 +2,23 @@ require "spec" require "../src/sqlite3" include SQLite3 + +DB_FILENAME = "./test.db" + +def with_db(&block : DB::Database ->) + File.delete(DB_FILENAME) rescue nil + DB.open "sqlite3:#{DB_FILENAME}", &block +ensure + File.delete(DB_FILENAME) +end + +def with_db(name, &block : DB::Database ->) + File.delete(name) rescue nil + DB.open "sqlite3:#{name}", &block +ensure + File.delete(name) +end + +def with_mem_db(&block : DB::Database ->) + DB.open "sqlite3://%3Amemory%3A", &block +end diff --git a/src/sqlite3/connection.cr b/src/sqlite3/connection.cr index 0b8d061..db676db 100644 --- a/src/sqlite3/connection.cr +++ b/src/sqlite3/connection.cr @@ -2,7 +2,8 @@ class SQLite3::Connection < DB::Connection def initialize(database) super filename = self.class.filename(database.uri) - check LibSQLite3.open_v2(filename, out @db, (LibSQLite3::Flag::READWRITE | LibSQLite3::Flag::CREATE), nil) + # TODO maybe enable Flag::URI to parse query string in the uri as additional flags + check LibSQLite3.open_v2(filename, out @db, (Flag::READWRITE | Flag::CREATE), nil) end def self.filename(uri : URI) @@ -23,6 +24,24 @@ class SQLite3::Connection < DB::Connection LibSQLite3.close_v2(self) end + # Dump the database to another SQLite3 database. This can be used for backing up a SQLite3 Database + # to disk or the opposite + def dump(to : SQLite3::Connection) + backup_item = LibSQLite3.backup_init(to.@db, "main", @db, "main") + if backup_item.null? + raise Exception.new(to.@db) + end + code = LibSQLite3.backup_step(backup_item, -1) + + if code != LibSQLite3::Code::DONE + raise Exception.new(to.@db) + end + code = LibSQLite3.backup_finish(backup_item) + if code != LibSQLite3::Code::OKAY + raise Exception.new(to.@db) + end + end + def to_unsafe @db end diff --git a/src/sqlite3/database.cr b/src/sqlite3/database.cr deleted file mode 100644 index e05a8f4..0000000 --- a/src/sqlite3/database.cr +++ /dev/null @@ -1,184 +0,0 @@ -# The Database class encapsulates single connection to an SQLite3 database. Its usage is very straightforward: -# -# ``` -# require "sqlite3" -# -# db = SQLite3::Database.new( "data.db" ) -# db.execute("select * from table") do |row| -# p row -# end -# db.close -# ``` -# -# Lower level methods are also provided. -class SQLite3::Database - # Creates a new Database object that opens the given file. - def initialize(filename) - code = LibSQLite3.open_v2(filename, out @db, (LibSQLite3::Flag::READWRITE | LibSQLite3::Flag::CREATE), nil) - if code != 0 - raise Exception.new(@db) - end - @closed = false - end - - # Creates a new Database object that opens the given file, yields it, and closes it at the end. - def self.new(filename) - db = new filename - begin - yield db - ensure - db.close - end - end - - # Executes the given SQL statement. If additional parameters are given, they are treated as bind variables, - # and are bound to the placeholders in the query. - # - # Note that if any of the values passed to this are hashes, then the key/value pairs are each bound separately, - # with the key being used as the name of the placeholder to bind the value to. - # - # Returns an `Array(Array(Value))`. - def execute(sql, *binds) - execute(sql, binds) - end - - # Executes the given SQL statement. If additional parameters are given, they are treated as bind variables, - # and are bound to the placeholders in the query. - # - # Note that if any of the values passed to this are hashes, then the key/value pairs are each bound separately, - # with the key being used as the name of the placeholder to bind the value to. - # - # Yields one `Array(Value)` for each result. - def execute(sql, *binds, &block) - execute(sql, binds) do |row| - yield row - end - end - - # Executes the given SQL statement. If additional parameters are given, they are treated as bind variables, - # and are bound to the placeholders in the query. - # - # Note that if any of the values passed to this are hashes, then the key/value pairs are each bound separately, - # with the key being used as the name of the placeholder to bind the value to. - # - # Returns an `Array(Array(Value))`. - def execute(sql, binds : Enumerable) - rows = [] of Array(Value) - execute(sql, binds) do |row| - rows << row - end - rows - end - - # Executes the given SQL statement. If additional parameters are given, they are treated as bind variables, - # and are bound to the placeholders in the query. - # - # Note that if any of the values passed to this are hashes, then the key/value pairs are each bound separately, - # with the key being used as the name of the placeholder to bind the value to. - # - # Yields one `Array(Value)` for each result. - def execute(sql, binds : Enumerable, &block) - query(sql, binds) do |result_set| - while result_set.next - yield result_set.to_a - end - end - end - - # A convenience method that returns the first row of a query result. - def get_first_row(sql, *binds) - get_first_row(sql, binds) - end - - # A convenience method that returns the first row of a query result. - def get_first_row(sql, binds : Enumerable) - query(sql, binds) do |result_set| - if result_set.next - return result_set.to_a - else - raise "no results" - end - end - end - - # A convenience method that returns the first value of the first row of a query result. - def get_first_value(sql, *binds) - get_first_value(sql, binds) - end - - # A convenience method that returns the first value of the first row of a query result. - def get_first_value(sql, binds : Enumerable) - query(sql, binds) do |result_set| - if result_set.next - return result_set[0] - else - raise "no results" - end - end - end - - # Executes a query and gives back a `ResultSet`. - def query(sql, *binds) - query(sql, binds) - end - - # Executes a query and yields a `ResultSet` that will be closed at the end of the given block. - def query(sql, *binds, &block) - query(sql, binds) do |result_set| - yield result_set - end - end - - # Executes a query and gives back a `ResultSet`. - def query(sql, binds : Enumerable) - prepare(sql).execute(binds) - end - - # Executes a query and yields a `ResultSet` that will be closed at the end of the given block. - def query(sql, binds : Enumerable, &block) - prepare(sql).execute(binds) do |result_set| - yield result_set - end - end - - # Prepares an sql statement. Returns a `Statement`. - def prepare(sql) - Statement.new(self, sql) - end - - # Obtains the unique row ID of the last row to be inserted by this Database instance. - # This is an `Int64`. - def last_insert_row_id - LibSQLite3.last_insert_rowid(self) - end - - # Quotes the given string, making it safe to use in an SQL statement. - # It replaces all instances of the single-quote character with two single-quote characters. - def quote(string) - string.gsub('\'', "''") - end - - # Returns `true` if this database instance has been closed (see `#close`). - def closed? - @closed - end - - # Closes this database. - def close - return if @closed - - @closed = true - - LibSQLite3.close_v2(@db) - end - - # :nodoc: - def finalize - close - end - - # :nodoc: - def to_unsafe - @db - end -end diff --git a/src/sqlite3/flags.cr b/src/sqlite3/flags.cr new file mode 100644 index 0000000..ccb8456 --- /dev/null +++ b/src/sqlite3/flags.cr @@ -0,0 +1,30 @@ +@[Flags] +enum SQLite3::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 +end + +module SQLite3 + # Same as doing SQLite3::Flag.flag(*values) + macro flags(*values) + ::SQLite3::Flag.flags({{*values}}) + end +end diff --git a/src/sqlite3/lib_sqlite3.cr b/src/sqlite3/lib_sqlite3.cr index 4b089b8..cb5bc8e 100644 --- a/src/sqlite3/lib_sqlite3.cr +++ b/src/sqlite3/lib_sqlite3.cr @@ -4,31 +4,10 @@ require "./type" lib LibSQLite3 type SQLite3 = Void* 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 - end + type SQLite3Backup = Void* enum Code + OKAY = 0 ROW = 100 DONE = 101 end @@ -36,11 +15,15 @@ lib LibSQLite3 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 : SQLite3::Flag, zVfs : UInt8*) : Int32 fun errcode = sqlite3_errcode(SQLite3) : Int32 fun errmsg = sqlite3_errmsg(SQLite3) : UInt8* + fun backup_init = sqlite3_backup_init(SQLite3, UInt8*, SQLite3, UInt8*) : SQLite3Backup + fun backup_step = sqlite3_backup_step(SQLite3Backup, Int8) : Code + fun backup_finish = sqlite3_backup_finish(SQLite3Backup) : Code + fun prepare_v2 = sqlite3_prepare_v2(db : SQLite3, zSql : UInt8*, nByte : Int32, ppStmt : Statement*, pzTail : UInt8**) : Int32 fun step = sqlite3_step(stmt : Statement) : Int32 fun column_count = sqlite3_column_count(stmt : Statement) : Int32 diff --git a/src/sqlite3/result_set.cr b/src/sqlite3/result_set.cr index 39e3e0b..174c403 100644 --- a/src/sqlite3/result_set.cr +++ b/src/sqlite3/result_set.cr @@ -88,7 +88,7 @@ class SQLite3::ResultSet < DB::ResultSet when Type::TEXT ; String when Type::NULL ; Nil else - raise "not implemented" + raise Exception.new(@statement.connection) end end diff --git a/src/sqlite3/type.cr b/src/sqlite3/type.cr index 4008f39..a4abe0e 100644 --- a/src/sqlite3/type.cr +++ b/src/sqlite3/type.cr @@ -1,8 +1,8 @@ # Each of the possible types of an SQLite3 column. enum SQLite3::Type INTEGER = 1 - FLOAT = 2 - BLOB = 4 - NULL = 5 - TEXT = 3 + FLOAT = 2 + BLOB = 4 + NULL = 5 + TEXT = 3 end