From fa220a1c4c86c3aee8a7e5988526fad5c25d537e Mon Sep 17 00:00:00 2001 From: Ben Jolitz Date: Mon, 14 Mar 2016 19:53:51 -0700 Subject: [PATCH] Initial support/tests for SQLite flags and db dump This commit adds a pass through for specifying SQLite3 flags. This is an absolute requirement for being able to create in-memory DB representations. --- spec/database_spec.cr | 41 ++++++++++++++++++++++++++++++++++++ src/sqlite3/database.cr | 43 +++++++++++++++++++++++++++++++++++++- src/sqlite3/lib_sqlite3.cr | 6 ++++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/spec/database_spec.cr b/spec/database_spec.cr index 6cdea3a..7a88167 100644 --- a/spec/database_spec.cr +++ b/spec/database_spec.cr @@ -8,6 +8,12 @@ ensure File.delete(DB_FILENAME) end +private def with_db(name) + yield Database.new name +ensure + File.delete(name) +end + describe Database do it "opens a database" do with_db do |db| @@ -15,6 +21,41 @@ describe Database do end end + it "opens a database and then backs it up to another db" do + with_db do |db| + db.execute "create table person (name string, age integer)" + db.execute "insert into person values (\"foo\", 10)" + with_db("./test2.db") do |backup_database| + db.dump(backup_database) + + backup_rows = backup_database.execute "select * from person" + source_rows = db.execute "select * from person" + backup_rows.should eq(source_rows) + 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| + db.execute "create table person (name string, age integer)" + db.execute "insert into person values (\"foo\", 10)" + in_memory_db = Database.new("file:memdb1?mode=memory&cache=shared", + LibSQLite3::Flag::URI | LibSQLite3::Flag::CREATE | LibSQLite3::Flag::READWRITE | + LibSQLite3::Flag::FULLMUTEX) + source_rows = db.execute "select * from person" + + db.dump(in_memory_db) + in_memory_db_rows = in_memory_db.execute "select * from person" + in_memory_db_rows.should eq(source_rows) + in_memory_db.execute "insert into person values (\"bar\", 22)" + in_memory_db.dump(db) + + in_memory_db_rows = in_memory_db.execute "select * from person" + source_rows = db.execute "select * from person" + in_memory_db_rows.should eq(source_rows) + end + end + [nil, 1, 1_i64, "hello", 1.5, 1.5_f32].each do |value| it "executes and select #{value}" do with_db(&.execute("select #{value ? value.inspect : "null"}")).should eq([[value]]) diff --git a/src/sqlite3/database.cr b/src/sqlite3/database.cr index e05a8f4..c6d6309 100644 --- a/src/sqlite3/database.cr +++ b/src/sqlite3/database.cr @@ -3,7 +3,7 @@ # ``` # require "sqlite3" # -# db = SQLite3::Database.new( "data.db" ) +# db = SQLite3::Database.new("data.db") # db.execute("select * from table") do |row| # p row # end @@ -21,6 +21,16 @@ class SQLite3::Database @closed = false end + # Allows for initialization with specific flags. Primary use case is to allow + # for sqlite3 URI opening and in memory DB operations. + def initialize(filename, flags : LibSQLite3::Flag) + code = LibSQLite3.open_v2(filename, out @db, flags, 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 @@ -31,6 +41,37 @@ class SQLite3::Database end end + # Dump the database to another SQLite3 instance. This can be used for backing up a SQLite3::Database + # to disk or the opposite + # Example: + # ``` + # source_database = SQLite3::Database.new("mydatabase.db") + # in_memory_db = SQLite3::Database.new( + # "file:memdb1?mode=memory&cache=shared", + # LibSQLite3::Flag::URI | LibSQLite3::Flag::CREATE | LibSQLite3::Flag::READWRITE | + # LibSQLite3::Flag::FULLMUTEX) + # source_database.dump(in_memory_db) + # source_database.close() + # in_memory_db.exectute { |row| + # ... + # } + # ``` + def dump(to : SQLite3::Database) + backup_item = LibSQLite3.backup_init(to.@db, "main", @db, "main") + if backup_item.nil? + 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 + # 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. # diff --git a/src/sqlite3/lib_sqlite3.cr b/src/sqlite3/lib_sqlite3.cr index 0c8b96e..b34c3c9 100644 --- a/src/sqlite3/lib_sqlite3.cr +++ b/src/sqlite3/lib_sqlite3.cr @@ -4,6 +4,7 @@ require "./type" lib LibSQLite3 type SQLite3 = Void* type Statement = Void* + type SQLite3Backup = Void* enum Flag READONLY = 0x00000001 # Ok for sqlite3_open_v2() @@ -29,6 +30,7 @@ lib LibSQLite3 end enum Code + OKAY = 0 ROW = 100 DONE = 101 end @@ -41,6 +43,10 @@ lib LibSQLite3 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