diff options
author | Jamis Buck <jamis@37signals.com> | 2005-01-09 15:04:16 +0000 |
---|---|---|
committer | Jamis Buck <jamis@37signals.com> | 2005-01-09 15:04:16 +0000 |
commit | c51fa9fc3d7c0ec6f59bcaf3d352fae9325857e2 (patch) | |
tree | fa6c5fdee64da937145d594348100b03225a4207 | |
download | third_party-sqlite3-ruby-c51fa9fc3d7c0ec6f59bcaf3d352fae9325857e2.tar.gz third_party-sqlite3-ruby-c51fa9fc3d7c0ec6f59bcaf3d352fae9325857e2.tar.xz third_party-sqlite3-ruby-c51fa9fc3d7c0ec6f59bcaf3d352fae9325857e2.zip |
Changed layout to support tagging and branching
33 files changed, 6937 insertions, 0 deletions
@@ -0,0 +1,27 @@ +Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * The names of its contributors may not be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. @@ -0,0 +1,36 @@ += SQLite3/Ruby Interface + +This module allows Ruby programs to interface with the SQLite3 +database engine (http://www.sqlite.org). You must have the +SQLite engine installed in order to build this module. + +Note that this module is NOT compatible with SQLite 2.x. + + +== Compilation and Installation + +Simply do the following, after installing SQLite3: + + ruby setup.rb config + ruby setup.rb setup + ruby setup.rb install + +Alternatively, you can download and install the RubyGem package for +SQLite3/Ruby (you must have RubyGems and SQLite3 installed, first): + + gem install --remote sqlite3-ruby + + +== Usage + +For help figuring out the SQLite3/Ruby interface, check out the +FAQ[http://sqlite-ruby.rubyforge.org/sqlite3/faq.html]. It includes examples of +usage. If you have any questions that you feel should be address in the +FAQ, please send them to jamis_buck@byu.edu. + + +== Contact Information + +The project page is http://rubyforge.org/projects/sqlite-ruby. There, you can find +links to mailing lists and forums that you can use to discuss this library. Additionally, +there are trackers for submitting bugs and feature requests. Feel free to use them! diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..3189556 --- /dev/null +++ b/Rakefile @@ -0,0 +1,178 @@ +require 'rubygems' +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' +require 'rake/contrib/sshpublisher' + +require "./lib/sqlite3/version" + +PACKAGE_NAME = "sqlite3-ruby" +PACKAGE_VERSION = SQLite3::Version::STRING + +SOURCE_FILES = FileList.new do |fl| + [ "ext", "lib", "test" ].each do |dir| + fl.include "#{dir}/**/*" + end + fl.include "Rakefile" + fl.exclude( /\bCVS\b/ ) +end + +PACKAGE_FILES = FileList.new do |fl| + [ "api", "doc" ].each do |dir| + fl.include "#{dir}/**/*" + end + fl.include "ChangeLog", "README", "LICENSE", "#{PACKAGE_NAME}.gemspec", "setup.rb" + fl.include SOURCE_FILES + fl.exclude( /\bCVS\b/ ) +end + +Gem.manage_gems + +def can_require( file ) + begin + require file + return true + rescue LoadError + return false + end +end + +desc "Default task" +task :default => [ :test ] + +desc "Clean generated files" +task :clean do + rm_rf "pkg" + rm_rf "api" + rm_f "ChangeLog" + rm_f "doc/faq/faq.html" + + native_files = [ "Makefile", "mkmf.log", "sqlite3_api.so", + "sqlite3_api_wrap.c", "sqlite3_api_wrap.o" ] + native_files.each { |f| rm_f "ext/sqlite3_api/#{f}" } +end + +desc "Run benchmarks vs. sqlite-ruby" +task :benchmark do + ruby "test/bm.rb" +end + +desc "Run benchmarks dl vs. native" +task :benchmark2 do + ruby "test/native-vs-dl.rb" +end + +desc "Generate the changelog using cvs2cl" +task :changelog => "ChangeLog" + +file "ChangeLog" do + unless system "cvs2cl" + warn "could not generate ChangeLog (cvs2cl missing?)" + end +end + +desc "Generate the FAQ document" +task :faq => "doc/faq/faq.html" + +file "doc/faq/faq.html" => [ "doc/faq/faq.rb", "doc/faq/faq.yml" ] do + cd( "doc/faq" ) { ruby "faq.rb > faq.html" } +end + +Rake::TestTask.new do |t| + t.test_files = [ "test/tests.rb" ] + t.verbose = true +end + +desc "Build all packages" +task :package + +package_name = "#{PACKAGE_NAME}-#{PACKAGE_VERSION}" +package_dir = "pkg" +package_dir_path = "#{package_dir}/#{package_name}" + +gz_file = "#{package_name}.tar.gz" +bz2_file = "#{package_name}.tar.bz2" +zip_file = "#{package_name}.zip" +gem_file = "#{package_name}.gem" + +task :gzip => SOURCE_FILES + [ :changelog, :faq, :rdoc, "#{package_dir}/#{gz_file}" ] +task :bzip => SOURCE_FILES + [ :changelog, :faq, :rdoc, "#{package_dir}/#{bz2_file}" ] +task :zip => SOURCE_FILES + [ :changelog, :faq, :rdoc, "#{package_dir}/#{zip_file}" ] +task :gem => SOURCE_FILES + [ :changelog, :faq, "#{package_dir}/#{gem_file}" ] + +task :package => [ :gzip, :bzip, :zip, :gem ] + +directory package_dir + +file package_dir_path do + mkdir_p package_dir_path rescue nil + PACKAGE_FILES.each do |fn| + f = File.join( package_dir_path, fn ) + if File.directory?( fn ) + mkdir_p f unless File.exist?( f ) + else + dir = File.dirname( f ) + mkdir_p dir unless File.exist?( dir ) + rm_f f + safe_ln fn, f + end + end +end + +file "#{package_dir}/#{zip_file}" => package_dir_path do + rm_f "#{package_dir}/#{zip_file}" + chdir package_dir do + sh %{zip -r #{zip_file} #{package_name}} + end +end + +file "#{package_dir}/#{gz_file}" => package_dir_path do + rm_f "#{package_dir}/#{gz_file}" + chdir package_dir do + sh %{tar czvf #{gz_file} #{package_name}} + end +end + +file "#{package_dir}/#{bz2_file}" => package_dir_path do + rm_f "#{package_dir}/#{bz2_file}" + chdir package_dir do + sh %{tar cjvf #{bz2_file} #{package_name}} + end +end + +file "#{package_dir}/#{gem_file}" => package_dir do + spec = eval(File.read(PACKAGE_NAME+".gemspec")) + Gem::Builder.new(spec).build + mv gem_file, "#{package_dir}/#{gem_file}" +end + +Rake::RDocTask.new do |rdoc| + rdoc.rdoc_dir = 'api' + rdoc.title = "SQLite3/Ruby" + rdoc.options << '--line-numbers --inline-source --main README' + rdoc.rdoc_files.include('lib/**/*.rb') + + if can_require( "rdoc/generators/template/html/jamis" ) + rdoc.template = "jamis" + end +end + +desc "Publish the API documentation" +task :pubrdoc => [ :rdoc ] do + Rake::SshDirPublisher.new( + "minam@rubyforge.org", + "/var/www/gforge-projects/sqlite-ruby/sqlite3/", + "api" ).upload +end + +desc "Publish the FAQ" +task :pubfaq => [ :faq ] do + Rake::SshFilePublisher.new( + "minam@rubyforge.org", + "/var/www/gforge-projects/sqlite-ruby/sqlite3", + "doc/faq", + "faq.html" ).upload +end + +desc "Publish the documentation" +task :pubdoc => [:pubrdoc, :pubfaq] diff --git a/doc/faq/faq.rb b/doc/faq/faq.rb new file mode 100644 index 0000000..13d29e4 --- /dev/null +++ b/doc/faq/faq.rb @@ -0,0 +1,177 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'yaml' +require 'redcloth' + +def process_faq_list( faqs ) + puts "<ul>" + faqs.each do |faq| + process_faq_list_item faq + end + puts "</ul>" +end + +def process_faq_list_item( faq ) + question = faq.keys.first + answer = faq.values.first + + print "<li>" + + question_text = RedCloth.new(question).to_html.gsub( %r{</?p>},"" ) + if answer.is_a?( Array ) + puts question_text + process_faq_list answer + else + print "<a href='##{question.id}'>#{question_text}</a>" + end + + puts "</li>" +end + +def process_faq_descriptions( faqs, path=nil ) + faqs.each do |faq| + process_faq_description faq, path + end +end + +def process_faq_description( faq, path ) + question = faq.keys.first + path = ( path ? path + " " : "" ) + question + answer = faq.values.first + + if answer.is_a?( Array ) + process_faq_descriptions( answer, path ) + else + title = RedCloth.new( path ).to_html.gsub( %r{</?p>}, "" ) + answer = RedCloth.new( answer || "" ) + + puts "<a name='#{question.id}'></a>" + puts "<div class='faq-title'>#{title}</div>" + puts "<div class='faq-answer'>#{add_api_links(answer.to_html)}</div>" + end +end + +API_OBJECTS = [ "Database", "Statement", "ResultSet", + "ParsedStatement", "Pragmas", "Translator" ].inject( "(" ) { |acc,name| + acc << "|" if acc.length > 1 + acc << name + acc + } + ")" + +def add_api_links( text ) + text.gsub( /#{API_OBJECTS}(#(\w+))?/ ) do + disp_obj = obj = $1 + + case obj + when "Pragmas": disp_obj = "Database" + end + + method = $3 + s = "<a href='http://sqlite-ruby.rubyforge.org/classes/SQLite/#{obj}.html'>#{disp_obj}" + s << "##{method}" if method + s << "</a>" + s + end +end + +faqs = YAML.load( File.read( "faq.yml" ) ) + +puts <<-EOF +<html> + <head> + <title>SQLite3/Ruby FAQ</title> + <style type="text/css"> + a, a:visited, a:active { + color: #00F; + text-decoration: none; + } + + a:hover { + text-decoration: underline; + } + + .faq-list { + color: #000; + font-family: vera-sans, verdana, arial, sans-serif; + } + + .faq-title { + background: #007; + color: #FFF; + font-family: vera-sans, verdana, arial, sans-serif; + padding-left: 1em; + padding-top: 0.5em; + padding-bottom: 0.5em; + font-weight: bold; + font-size: large; + border: 1px solid #000; + } + + .faq-answer { + margin-left: 1em; + color: #000; + font-family: vera-sans, verdana, arial, sans-serif; + } + + .faq-answer pre { + margin-left: 1em; + color: #000; + background: #FFE; + font-size: normal; + border: 1px dotted #CCC; + padding: 1em; + } + + h1 { + background: #005; + color: #FFF; + font-family: vera-sans, verdana, arial, sans-serif; + padding-left: 1em; + padding-top: 1em; + padding-bottom: 1em; + font-weight: bold; + font-size: x-large; + border: 1px solid #00F; + } + </style> + </head> + <body> + <h1>SQLite/Ruby FAQ</h1> + <div class="faq-list"> +EOF + +process_faq_list( faqs ) +puts "</div>" +process_faq_descriptions( faqs ) + +puts "</body></html>" diff --git a/doc/faq/faq.yml b/doc/faq/faq.yml new file mode 100644 index 0000000..f4bb4c1 --- /dev/null +++ b/doc/faq/faq.yml @@ -0,0 +1,426 @@ +--- +- "How do I do a database query?": + - "I just want an array of the rows...": >- + + Use the Database#execute method. If you don't give it a block, it will + return an array of all the rows: + + + <pre> + require 'sqlite3' + + db = SQLite3::Database.new( "test.db" ) + rows = db.execute( "select * from test" ) + </pre> + + - "I'd like to use a block to iterate through the rows...": >- + + Use the Database#execute method. If you give it a block, each row of the + result will be yielded to the block: + + + <pre> + require 'sqlite3' + + db = SQLite3::Database.new( "test.db" ) + db.execute( "select * from test" ) do |row| + ... + end + </pre> + + - "I need to get the column names as well as the rows...": >- + + Use the Database#execute2 method. This works just like Database#execute; + if you don't give it a block, it returns an array of rows; otherwise, it + will yield each row to the block. _However_, the first row returned is + always an array of the column names from the query: + + + <pre> + require 'sqlite3' + + db = SQLite3::Database.new( "test.db" ) + columns, *rows = db.execute2( "select * from test" ) + + # or use a block: + + columns = nil + db.execute2( "select * from test" ) do |row| + if columns.nil? + columns = row + else + # process row + end + end + </pre> + + - "I just want the first row of the result set...": >- + + Easy. Just call Database#get_first_row: + + + <pre> + row = db.get_first_row( "select * from table" ) + </pre> + + + This also supports bind variables, just like Database#execute + and friends. + + - "I just want the first value of the first row of the result set...": >- + + Also easy. Just call Database#get_first_value: + + + <pre> + count = db.get_first_value( "select count(*) from table" ) + </pre> + + + This also supports bind variables, just like Database#execute + and friends. + +- "How do I prepare a statement for repeated execution?": >- + If the same statement is going to be executed repeatedly, you can speed + things up a bit by _preparing_ the statement. You do this via the + Database#prepare method. It returns a Statement object, and you can + then invoke #execute on that to get the ResultSet: + + + <pre> + stmt = db.prepare( "select * from person" ) + + 1000.times do + stmt.execute do |result| + ... + end + end + + stmt.close + + # or, use a block + + db.prepare( "select * from person" ) do |stmt| + 1000.times do + stmt.execute do |result| + ... + end + end + end + </pre> + + + This is made more useful by the ability to bind variables to placeholders + via the Statement#bind_param and Statement#bind_params methods. (See the + next FAQ for details.) + +- "How do I use placeholders in an SQL statement?": >- + Placeholders in an SQL statement take any of the following formats: + + + * @?@ + + * @?_nnn_@ + + * @:_word_@ + + + Where _n_ is an integer, and _word_ is an alpha-numeric identifier (or + number). When the placeholder is associated with a number, that number + identifies the index of the bind variable to replace it with. When it + is an identifier, it identifies the name of the correponding bind + variable. (In the instance of the first format--a single question + mark--the placeholder is assigned a number one greater than the last + index used, or 1 if it is the first.) + + + For example, here is a query using these placeholder formats: + + + <pre> + select * + from table + where ( c = ?2 or c = ? ) + and d = :name + and e = :1 + </pre> + + + This defines 5 different placeholders: 1, 2, 3, and "name". + + + You replace these placeholders by _binding_ them to values. This can be + accomplished in a variety of ways. + + + The Database#execute, and Database#execute2 methods all accept additional + arguments following the SQL statement. These arguments are assumed to be + bind parameters, and they are bound (positionally) to their corresponding + placeholders: + + + <pre> + db.execute( "select * from table where a = ? and b = ?", + "hello", + "world" ) + </pre> + + + The above would replace the first question mark with 'hello' and the + second with 'world'. If the placeholders have an explicit index given, they + will be replaced with the bind parameter at that index (1-based). + + + If a Hash is given as a bind parameter, then its key/value pairs are bound + to the placeholders. This is how you bind by name: + + + <pre> + db.execute( "select * from table where a = :name and b = :value", + "name" => "bob", + "value" => "priceless" ) + </pre> + + + You can also bind explicitly using the Statement object itself. Just pass + additional parameters to the Statement#execute statement: + + + <pre> + db.prepare( "select * from table where a = :name and b = ?" ) do |stmt| + stmt.execute "value", "name" => "bob" + end + </pre> + + + Or do a Database#prepare to get the Statement, and then use either + Statement#bind_param or Statement#bind_params: + + + <pre> + stmt = db.prepare( "select * from table where a = :name and b = ?" ) + + stmt.bind_param( "name", "bob" ) + stmt.bind_param( 1, "value" ) + + # or + + stmt.bind_params( "value", "name" => "bob" ) + </pre> + +- "How do I discover metadata about a query?": >- + + If you ever want to know the names or types of the columns in a result + set, you can do it in several ways. + + + The first way is to ask the row object itself. Each row will have a + property "fields" that returns an array of the column names. The row + will also have a property "types" that returns an array of the column + types: + + + <pre> + rows = db.execute( "select * from table" ) + p rows[0].fields + p rows[0].types + </pre> + + + Obviously, this approach requires you to execute a statement that actually + returns data. If you don't know if the statement will return any rows, but + you still need the metadata, you can use Database#query and ask the + ResultSet object itself: + + + <pre> + db.query( "select * from table" ) do |result| + p result.columns + p result.types + ... + end + </pre> + + + Lastly, you can use Database#prepare and ask the Statement object what + the metadata are: + + + <pre> + stmt = db.prepare( "select * from table" ) + p stmt.columns + p stmt.types + </pre> + +- "I'd like the rows to be indexible by column name.": >- + By default, each row from a query is returned as an Array of values. This + means that you can only obtain values by their index. Sometimes, however, + you would like to obtain values by their column name. + + + The first way to do this is to set the Database property "results_as_hash" + to true. If you do this, then all rows will be returned as Hash objects, + with the column names as the keys. (In this case, the "fields" property + is unavailable on the row, although the "types" property remains.) + + + <pre> + db.results_as_hash = true + db.execute( "select * from table" ) do |row| + p row['column1'] + p row['column2'] + end + </pre> + + + The other way is to use Ara Howard's + "ArrayFields":http://rubyforge.org/projects/arrayfields + module. Just require "arrayfields", and all of your rows will be indexable + by column name, even though they are still arrays! + + + <pre> + require 'arrayfields' + + ... + db.execute( "select * from table" ) do |row| + p row[0] == row['column1'] + p row[1] == row['column2'] + end + </pre> + +- "I'd like the values from a query to be the correct types, instead of String.": >- + You can turn on "type translation" by setting Database#type_translation to + true: + + + <pre> + db.type_translation = true + db.execute( "select * from table" ) do |row| + p row + end + </pre> + + + By doing this, each return value for each row will be translated to its + correct type, based on its declared column type. + + + You can even declare your own translation routines, if (for example) you are + using an SQL type that is not handled by default: + + + <pre> + # assume "objects" table has the following schema: + # create table objects ( + # name varchar2(20), + # thing object + # ) + + db.type_translation = true + db.translator.add_translator( "object" ) do |type, value| + db.decode( value ) + end + + h = { :one=>:two, "three"=>"four", 5=>6 } + dump = db.encode( h ) + + db.execute( "insert into objects values ( ?, ? )", "bob", dump ) + + obj = db.get_first_value( "select thing from objects where name='bob'" ) + p obj == h + </pre> + +- "How do insert binary data into the database?": >- + Use blobs. Blobs are new features of SQLite3. You have to use bind + variables to make it work: + + + <pre> + db.execute( "insert into foo ( ?, ? )", + SQLite3::Blob.new( "\0\1\2\3\4\5" ), + SQLite3::Blob.new( "a\0b\0c\0d ) ) + </pre> + + + The blob values must be indicated explicitly by binding each parameter to + a value of type SQLite3::Blob. + +- "How do I do a DDL (insert, update, delete) statement?": >- + You can actually do inserts, updates, and deletes in exactly the same way + as selects, but in general the Database#execute method will be most + convenient: + + + <pre> + db.execute( "insert into table values ( ?, ? )", *bind_vars ) + </pre> + +- "How do I execute multiple statements in a single string?": >- + The standard query methods (Database#execute, Database#execute2, + Database#query, and Statement#execute) will only execute the first + statement in the string that is given to them. Thus, if you have a + string with multiple SQL statements, each separated by a string, + you can't use those methods to execute them all at once. + + + Instead, use Database#execute_batch: + + + <pre> + sql = <<SQL + create table the_table ( + a varchar2(30), + b varchar2(30) + ); + + insert into the_table values ( 'one', 'two' ); + insert into the_table values ( 'three', 'four' ); + insert into the_table values ( 'five', 'six' ); + SQL + + db.execute_batch( sql ) + </pre> + + + Unlike the other query methods, Database#execute_batch accepts no + block. It will also only ever return +nil+. Thus, it is really only + suitable for batch processing of DDL statements. + +- "How do I begin/end a transaction?": + Use Database#transaction to start a transaction. If you give it a block, + the block will be automatically committed at the end of the block, + unless an exception was raised, in which case the transaction will be + rolled back. (Never explicitly call Database#commit or Database#rollback + inside of a transaction block--you'll get errors when the block + terminates!) + + + <pre> + database.transaction do |db| + db.execute( "insert into table values ( 'a', 'b', 'c' )" ) + ... + end + </pre> + + + Alternatively, if you don't give a block to Database#transaction, the + transaction remains open until you explicitly call Database#commit or + Database#rollback. + + + <pre> + db.transaction + db.execute( "insert into table values ( 'a', 'b', 'c' )" ) + db.commit + </pre> + + + Note that SQLite does not allow nested transactions, so you'll get errors + if you try to open a new transaction while one is already active. Use + Database#transaction_active? to determine whether a transaction is + active or not. + +#- "How do I discover metadata about a table/index?": +# +#- "How do I do tweak database settings?": diff --git a/ext/sqlite3_api/MANIFEST b/ext/sqlite3_api/MANIFEST new file mode 100644 index 0000000..f335e44 --- /dev/null +++ b/ext/sqlite3_api/MANIFEST @@ -0,0 +1,4 @@ +extconf.rb +post-clean.rb +post-distclean.rb +sqlite3_api.i diff --git a/ext/sqlite3_api/extconf.rb b/ext/sqlite3_api/extconf.rb new file mode 100644 index 0000000..528eb2d --- /dev/null +++ b/ext/sqlite3_api/extconf.rb @@ -0,0 +1,8 @@ +require 'mkmf' + +dir_config( "sqlite3" ) + +if have_header( "sqlite3.h" ) and have_library( "sqlite3", "sqlite3_open" ) + system "swig -ruby sqlite3_api.i" + create_makefile( "sqlite3_api" ) +end diff --git a/ext/sqlite3_api/post-clean.rb b/ext/sqlite3_api/post-clean.rb new file mode 100644 index 0000000..1017f72 --- /dev/null +++ b/ext/sqlite3_api/post-clean.rb @@ -0,0 +1,3 @@ +# post-distclean.rb + +File.delete File.join( curr_srcdir, "sqlite3_api_wrap.c" ) rescue nil diff --git a/ext/sqlite3_api/post-distclean.rb b/ext/sqlite3_api/post-distclean.rb new file mode 100644 index 0000000..f8543b1 --- /dev/null +++ b/ext/sqlite3_api/post-distclean.rb @@ -0,0 +1,4 @@ +# post-distclean.rb + +# on a distclean, always do a clean, as well +eval File.read( File.join( curr_srcdir, "post-clean.rb" ) ) diff --git a/ext/sqlite3_api/sqlite3_api.i b/ext/sqlite3_api/sqlite3_api.i new file mode 100644 index 0000000..a3e5e5b --- /dev/null +++ b/ext/sqlite3_api/sqlite3_api.i @@ -0,0 +1,339 @@ +%module "SQLite3::driver::native::API" +%include "typemaps.i" +%{ +#include <sqlite3.h> +#include "ruby.h" +#include "intern.h" + +#define Init_API Init_sqlite3_api + +struct CallbackData { + VALUE proc; + VALUE proc2; + VALUE data; +}; + +typedef struct CallbackData CallbackData; +typedef void BLOB; +typedef void VALBLOB; + +int Sqlite3_ruby_busy_handler(void* data,int value) { + VALUE result; + CallbackData *cb = (CallbackData*)data; + result = rb_funcall( + cb->proc, rb_intern("call"), 2, cb->data, INT2FIX(value) ); + return FIX2INT(result); +} + +int Sqlite3_ruby_authorizer(void* data,int type, + const char* a,const char* b,const char* c,const char* d) +{ + VALUE result; + CallbackData *cb = (CallbackData*)data; + result = rb_funcall( + cb->proc, rb_intern("call"), 6, cb->data, INT2FIX(type), + ( a ? rb_str_new2(a) : Qnil ), ( b ? rb_str_new2(b) : Qnil ), + ( c ? rb_str_new2(c) : Qnil ), ( d ? rb_str_new2(d) : Qnil ) ); + return FIX2INT(result); +} + +void Sqlite3_ruby_trace(void* data, const char *sql) { + CallbackData *cb = (CallbackData*)data; + rb_funcall( cb->proc, rb_intern("call"), 2, cb->data, + sql ? rb_str_new2(sql) : Qnil ); +} + +void Sqlite3_ruby_function_step(sqlite3_context* ctx,int n, + sqlite3_value** args) +{ + CallbackData *data; + VALUE rb_args; + VALUE *rb_context; + int idx; + + data = (CallbackData*)sqlite3_user_data(ctx); + + if( data->proc2 != Qnil ) { + rb_context = (VALUE*)sqlite3_aggregate_context(ctx,sizeof(VALUE)); + if( *rb_context == 0 ) { + *rb_context = rb_hash_new(); + rb_gc_register_address( rb_context ); + } + } + + rb_args = rb_ary_new2(n+1); + rb_ary_push( rb_args, SWIG_NewPointerObj(ctx,SWIGTYPE_p_sqlite3_context,0) ); + for( idx = 0; idx < n; idx++ ) { + rb_ary_push( rb_args, SWIG_NewPointerObj(args[idx], + SWIGTYPE_p_sqlite3_value,0) ); + } + + rb_apply( data->proc, rb_intern("call"), rb_args ); +} + +void Sqlite3_ruby_function_final(sqlite3_context *ctx) { + VALUE *rb_context; + CallbackData *data; + + rb_context = (VALUE*)sqlite3_aggregate_context(ctx,sizeof(VALUE)); + if( *rb_context == 0 ) { + *rb_context = rb_hash_new(); + rb_gc_register_address( rb_context ); + } + + data = (CallbackData*)sqlite3_user_data(ctx); + + rb_funcall( data->proc2, rb_intern("call"), 1, + SWIG_NewPointerObj(ctx,SWIGTYPE_p_sqlite3_context,0) ); + + rb_gc_unregister_address( rb_context ); +} +%} + +struct CallbackData { + VALUE proc; + VALUE proc2; + VALUE data; +}; + +%typemap(in) const void *str { + $1 = (void*)RSTRING($input)->ptr; +} + +%typemap(in) (const char *filename, sqlite3**) { + $1 = STR2CSTR($input); + $2 = (sqlite3**)malloc( sizeof( sqlite3* ) ); +} + +%typemap(argout) (const char *filename, sqlite3**) { + VALUE ary; + ary = rb_ary_new2(2); + rb_ary_push( ary, $result ); + rb_ary_push( ary, SWIG_NewPointerObj( *$2, SWIGTYPE_p_sqlite3, 0 ) ); + free( $2 ); + $result = ary; +} + +%typemap(in) (const void *filename, sqlite3**) { + $1 = (void*)RSTRING($input)->ptr; + $2 = (sqlite3**)malloc( sizeof( sqlite3* ) ); +} + +%typemap(argout) (const void *filename, sqlite3**) { + VALUE ary; + ary = rb_ary_new2(2); + rb_ary_push( ary, $result ); + rb_ary_push( ary, SWIG_NewPointerObj( *$2, SWIGTYPE_p_sqlite3, 0 ) ); + free( $2 ); + $result = ary; +} + +typedef void BLOB; +%typemap(out) const BLOB * { + $result = $1 ? + rb_str_new( (char*)$1, sqlite3_column_bytes( arg1, arg2 ) ) : Qnil; +} + +typedef void VALBLOB; +%typemap(out) const VALBLOB * { + $result = $1 ? rb_str_new( (char*)$1, sqlite3_value_bytes( arg1 ) ) : Qnil; +} + +%typemap(out) const void * { + int i; + if( $1 ) { + for( i = 0; ((char*)$1)[i]; i += 2 ); + $result = rb_str_new( (char*)$1, i ); + } else $result = Qnil; +} + +%typemap(in) (const char * sql,int,sqlite3_stmt**,const char**) (sqlite3_stmt *stmt, char *errmsg) { + $1 = RSTRING($input)->ptr; + $2 = RSTRING($input)->len; + $3 = &stmt2; + $4 = &errmsg2; +} + +%typemap(argout) (const char* sql,int,sqlite3_stmt**,const char**) { + VALUE ary; + ary = rb_ary_new2(3); + rb_ary_push( ary, $result ); + rb_ary_push( ary, SWIG_NewPointerObj( stmt2, SWIGTYPE_p_sqlite3_stmt, 0 ) ); + rb_ary_push( ary, errmsg2 ? rb_str_new2( errmsg2 ) : Qnil ); + $result = ary; +} + +%typemap(in) (const void* sql,int,sqlite3_stmt**,const void**) (sqlite3_stmt *stmt, void *errmsg) { + $1 = RSTRING($input)->ptr; + $2 = RSTRING($input)->len; + $3 = &stmt2; + $4 = &errmsg2; +} + +%typemap(argout) (const void* sql,int,sqlite3_stmt**,const void**) { + VALUE ary; + int i; + + for( i = 0; ((char*)errmsg2)[i]; i += 2 ); + + ary = rb_ary_new2(3); + rb_ary_push( ary, $result ); + rb_ary_push( ary, SWIG_NewPointerObj( stmt2, SWIGTYPE_p_sqlite3_stmt, 0 ) ); + rb_ary_push( ary, errmsg2 ? rb_str_new( (char*)errmsg2, i ) : Qnil ); + $result = ary; +} + +%typemap(in) (const void *blob,int) { + $1 = (void*)RSTRING($input)->ptr; + $2 = RSTRING($input)->len; +} + +%typemap(in) (const void *blob,int,void(*free)(void*)) { + $1 = (void*)RSTRING($input)->ptr; + $2 = RSTRING($input)->len; + $3 = SQLITE_TRANSIENT; +} + +%typemap(in) (const char *text,int) { + $1 = RSTRING($input)->ptr; + $2 = RSTRING($input)->len; +} + +%typemap(in) (const char *text,int,void(*free)(void*)) { + $1 = RSTRING($input)->ptr; + $2 = RSTRING($input)->len; + $3 = SQLITE_TRANSIENT; +} + +%typemap(in) (const void *utf16,int) { + $1 = (void*)RSTRING($input)->ptr; + $2 = RSTRING($input)->len; +} + +%typemap(in) (const void *utf16,int,void(*free)(void*)) { + $1 = (void*)RSTRING($input)->ptr; + $2 = RSTRING($input)->len; + $3 = SQLITE_TRANSIENT; +} + +%typemap(out) sqlite_int64 { + $result = rb_ll2inum( $1 ); +} + +%typemap(out) const char * { + $result = $1 ? rb_str_new2($1) : Qnil; +} + +%typemap(in) sqlite_int64 { + $1 = rb_big2ll( $input ); +} + +%typemap(in) (sqlite3_context*,int data_size) { + SWIG_ConvertPtr($input,(void**)&$1, SWIGTYPE_p_sqlite3_context, 1); + $2 = 4; +} + +%typemap(out) VALUE* { + $result = *(VALUE*)$1; +} + +%constant int Sqlite3_ruby_busy_handler(void*,int); +%constant int Sqlite3_ruby_authorizer(void*,int,const char*,const char*,const char*,const char*); +%constant void Sqlite3_ruby_trace(void*,const char*); +%constant void Sqlite3_ruby_function_step(sqlite3_context* ctx,int n, + sqlite3_value** args); +%constant void Sqlite3_ruby_function_final(sqlite3_context* ctx); + +const char *sqlite3_libversion(void); +int sqlite3_close(sqlite3*); + +sqlite_int64 sqlite3_last_insert_rowid(sqlite3*); + +int sqlite3_changes(sqlite3*); +int sqlite3_total_changes(sqlite3*); +void sqlite3_interrupt(sqlite3*); + +int sqlite3_complete(const char*); +int sqlite3_complete16(const void *str); + +int sqlite3_busy_handler(sqlite3*, int(*)(void*,int), void*); +int sqlite3_busy_timeout(sqlite3*,int); +int sqlite3_set_authorizer(sqlite3*, int(*)(void*,int,const char*,const char*,const char*,const char*), void*); +int sqlite3_trace(sqlite3*, void(*)(void*,const char*), void*); + +int sqlite3_open(const char *filename, sqlite3 **); +int sqlite3_open16(const void *filename, sqlite3 **); + +int sqlite3_errcode(sqlite3*); +const char *sqlite3_errmsg(sqlite3*); +const void *sqlite3_errmsg16(sqlite3*); + +int sqlite3_prepare(sqlite3*,const char* sql,int,sqlite3_stmt**,const char**); +int sqlite3_prepare16(sqlite3*,const void* sql,int,sqlite3_stmt**,const void**); + +int sqlite3_bind_blob(sqlite3_stmt*,int,const void *blob,int,void(*free)(void*)); +int sqlite3_bind_double(sqlite3_stmt*,int,double); +int sqlite3_bind_int(sqlite3_stmt*,int,int); +int sqlite3_bind_int64(sqlite3_stmt*,int,sqlite_int64); +int sqlite3_bind_null(sqlite3_stmt*,int); +int sqlite3_bind_text(sqlite3_stmt*,int,const char*text,int,void(*free)(void*)); +int sqlite3_bind_text16(sqlite3_stmt*,int,const void*utf16,int,void(*free)(void*)); + +int sqlite3_bind_parameter_count(sqlite3_stmt*); +const char *sqlite3_bind_parameter_name(sqlite3_stmt*,int); +int sqlite3_bind_parameter_index(sqlite3_stmt*,const char*); + +int sqlite3_column_count(sqlite3_stmt*); +const char *sqlite3_column_name(sqlite3_stmt*,int); +const void *sqlite3_column_name16(sqlite3_stmt*,int); +const char *sqlite3_column_decltype(sqlite3_stmt*,int); +const void *sqlite3_column_decltype16(sqlite3_stmt*,int); + +int sqlite3_step(sqlite3_stmt*); + +int sqlite3_data_count(sqlite3_stmt*); + +const BLOB *sqlite3_column_blob(sqlite3_stmt*,int); +int sqlite3_column_bytes(sqlite3_stmt*,int); +int sqlite3_column_bytes16(sqlite3_stmt*,int); +double sqlite3_column_double(sqlite3_stmt*,int); +double sqlite3_column_int(sqlite3_stmt*,int); +sqlite_int64 sqlite3_column_int64(sqlite3_stmt*,int); +const char *sqlite3_column_text(sqlite3_stmt*,int); +const void *sqlite3_column_text16(sqlite3_stmt*,int); +int sqlite3_column_type(sqlite3_stmt*,int); + +int sqlite3_finalize(sqlite3_stmt*); +int sqlite3_reset(sqlite3_stmt*); + +int sqlite3_create_function(sqlite3*,const char*str,int,int,void*,void(*func)(sqlite3_context*,int,sqlite3_value**),void(*step)(sqlite3_context*,int,sqlite3_value**),void(*final)(sqlite3_context*)); + +int sqlite3_create_function16(sqlite3*,const void*str,int,int,void*,void(*func)(sqlite3_context*,int,sqlite3_value**),void(*step)(sqlite3_context*,int,sqlite3_value**),void(*final)(sqlite3_context*)); + +int sqlite3_aggregate_count(sqlite3_context*); + +const VALBLOB *sqlite3_value_blob(sqlite3_value*); +int sqlite3_value_bytes(sqlite3_value*); +int sqlite3_value_bytes16(sqlite3_value*); +double sqlite3_value_double(sqlite3_value*); +int sqlite3_value_int(sqlite3_value*); +sqlite_int64 sqlite3_value_int64(sqlite3_value*); +const char *sqlite3_value_text(sqlite3_value*); +const void *sqlite3_value_text16(sqlite3_value*); +const void *sqlite3_value_text16le(sqlite3_value*); +const void *sqlite3_value_text16be(sqlite3_value*); +int sqlite3_value_type(sqlite3_value*); + +void sqlite3_result_blob(sqlite3_context*,const void *blob,int,void(*free)(void*)); +void sqlite3_result_double(sqlite3_context*,double); +void sqlite3_result_error(sqlite3_context*,const char *text,int); +void sqlite3_result_error16(sqlite3_context*,const void *blob,int); +void sqlite3_result_int(sqlite3_context*,int); +void sqlite3_result_int64(sqlite3_context*,sqlite_int64); +void sqlite3_result_text(sqlite3_context*,const char* text,int,void(*free)(void*)); +void sqlite3_result_text16(sqlite3_context*,const void* utf16,int,void(*free)(void*)); +void sqlite3_result_text16le(sqlite3_context*,const void* utf16,int,void(*free)(void*)); +void sqlite3_result_text16be(sqlite3_context*,const void* utf16,int,void(*free)(void*)); +void sqlite3_result_value(sqlite3_context*,sqlite3_value*); + +VALUE *sqlite3_aggregate_context(sqlite3_context*,int data_size); diff --git a/lib/sqlite3.rb b/lib/sqlite3.rb new file mode 100644 index 0000000..ff8af02 --- /dev/null +++ b/lib/sqlite3.rb @@ -0,0 +1,33 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3/database' diff --git a/lib/sqlite3/constants.rb b/lib/sqlite3/constants.rb new file mode 100644 index 0000000..5a20e46 --- /dev/null +++ b/lib/sqlite3/constants.rb @@ -0,0 +1,81 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +module SQLite3 ; module Constants + + module TextRep + UTF8 = 1 + UTF16LE = 2 + UTF16BE = 3 + UTF16 = 4 + ANY = 5 + end + + module ColumnType + INTEGER = 1 + FLOAT = 2 + TEXT = 3 + BLOB = 4 + NULL = 5 + end + + module ErrorCode + OK = 0 # Successful result + ERROR = 1 # SQL error or missing database + INTERNAL = 2 # An internal logic error in SQLite + PERM = 3 # Access permission denied + ABORT = 4 # Callback routine requested an abort + BUSY = 5 # The database file is locked + LOCKED = 6 # A table in the database is locked + NOMEM = 7 # A malloc() failed + READONLY = 8 # Attempt to write a readonly database + INTERRUPT = 9 # Operation terminated by sqlite_interrupt() + IOERR = 10 # Some kind of disk I/O error occurred + CORRUPT = 11 # The database disk image is malformed + NOTFOUND = 12 # (Internal Only) Table or record not found + FULL = 13 # Insertion failed because database is full + CANTOPEN = 14 # Unable to open the database file + PROTOCOL = 15 # Database lock protocol error + EMPTY = 16 # (Internal Only) Database table is empty + SCHEMA = 17 # The database schema changed + TOOBIG = 18 # Too much data for one row of a table + CONSTRAINT = 19 # Abort due to contraint violation + MISMATCH = 20 # Data type mismatch + MISUSE = 21 # Library used incorrectly + NOLFS = 22 # Uses OS features not supported on host + AUTH = 23 # Authorization denied + + ROW = 100 # sqlite_step() has another row ready + DONE = 101 # sqlite_step() has finished executing + end + +end ; end diff --git a/lib/sqlite3/database.rb b/lib/sqlite3/database.rb new file mode 100644 index 0000000..65a8410 --- /dev/null +++ b/lib/sqlite3/database.rb @@ -0,0 +1,720 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'base64' +require 'sqlite3/constants' +require 'sqlite3/errors' +require 'sqlite3/pragmas' +require 'sqlite3/statement' +require 'sqlite3/translator' +require 'sqlite3/value' + +module SQLite3 + + # The Database class encapsulates a single connection to a 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 + # + # It wraps the lower-level methods provides by the selected driver, and + # includes the Pragmas module for access to various pragma convenience + # methods. + # + # The Database class provides type translation services as well, by which + # the SQLite3 data types (which are all represented as strings) may be + # converted into their corresponding types (as defined in the schemas + # for their tables). This translation only occurs when querying data from + # the database--insertions and updates are all still typeless. + # + # Furthermore, the Database class has been designed to work well with the + # ArrayFields module from Ara Howard. If you require the ArrayFields + # module before performing a query, and if you have not enabled results as + # hashes, then the results will all be indexible by field name. + class Database + include Pragmas + + class <<self + + alias :open :new + + # 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. The modified string is returned. + def quote( string ) + string.gsub( /'/, "''" ) + end + + end + + # The low-level opaque database handle that this object wraps. + attr_reader :handle + + # A reference to the underlying SQLite3 driver used by this database. + attr_reader :driver + + # A boolean that indicates whether rows in result sets should be returned + # as hashes or not. By default, rows are returned as arrays. + attr_accessor :results_as_hash + + # A boolean indicating whether or not type translation is enabled for this + # database. + attr_accessor :type_translation + + # Create a new Database object that opens the given file. If utf16 + # is +true+, the filename is interpreted as a UTF-16 encoded string. + # + # By default, the new database will return result rows as arrays + # (#results_as_hash) and has type translation disabled (#type_translation=). + def initialize( file_name, options={} ) + utf16 = options.fetch(:utf16, false) + load_driver( options[:driver] ) + + @statement_factory = options[:statement_factory] || Statement + + result, @handle = @driver.open( file_name, utf16 ) + Error.check( result, nil, "could not open database" ) + + @closed = false + @results_as_hash = options.fetch(:results_as_hash,false) + @type_translation = options.fetch(:type_translation,false) + @translator = nil + end + + # Return +true+ if the string is a valid (ie, parsable) SQL statement, and + # +false+ otherwise. If +utf16+ is +true+, then the string is a UTF-16 + # character string. + def complete?( string, utf16=false ) + @driver.complete?( string, utf16 ) + end + + # Return a string describing the last error to have occurred with this + # database. + def errmsg( utf16=false ) + @driver.errmsg( @handle, utf16 ) + end + + # Return an integer representing the last error to have occurred with this + # database. + def errcode + @driver.errcode( @handle ) + end + + # Return the type translator employed by this database instance. Each + # database instance has its own type translator; this allows for different + # type handlers to be installed in each instance without affecting other + # instances. Furthermore, the translators are instantiated lazily, so that + # if a database does not use type translation, it will not be burdened by + # the overhead of a useless type translator. (See the Translator class.) + def translator + @translator ||= Translator.new + end + + # Closes this database. + def close + unless @closed + result = @driver.close( @handle ) + Error.check( result, self ) + end + @closed = true + end + + # Returns +true+ if this database instance has been closed (see #close). + def closed? + @closed + end + + # Installs (or removes) a block that will be invoked for every SQL + # statement executed. The block receives a two parameters: the +data+ + # argument, and the SQL statement executed. If the block is +nil+, + # any existing tracer will be uninstalled. + def trace( data=nil, &block ) + @driver.trace( @handle, data, &block ) + end + + # Installs (or removes) a block that will be invoked for every access + # to the database. If the block returns 0 (or +nil+), the statement + # is allowed to proceed. Returning 1 causes an authorization error to + # occur, and returning 2 causes the access to be silently denied. + def authorizer( data=nil, &block ) + result = @driver.set_authorizer( @handle, data, &block ) + Error.check( result, self ) + end + + # Returns a Statement object representing the given SQL. This does not + # execute the statement; it merely prepares the statement for execution. + def prepare( sql ) + stmt = @statement_factory.new( self, sql ) + if block_given? + begin + yield stmt + ensure + stmt.close + end + else + return stmt + 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. + # + # The block is optional. If given, it will be invoked for each row returned + # by the query. Otherwise, any results are accumulated into an array and + # returned wholesale. + # + # See also #execute2, and #execute_batch for additional ways of + # executing statements. + def execute( sql, *bind_vars ) + prepare( sql ) do |stmt| + result = stmt.execute( *bind_vars ) + if block_given? + result.each { |row| yield row } + else + return result.inject( [] ) { |arr,row| arr << row; arr } + end + end + end + + # Executes the given SQL statement, exactly as with #execute. However, the + # first row returned (either via the block, or in the returned array) is + # always the names of the columns. Subsequent rows correspond to the data + # from the result set. + # + # Thus, even if the query itself returns no rows, this method will always + # return at least one row--the names of the columns. + # + # See also #execute, and #execute_batch for additional ways of + # executing statements. + def execute2( sql, *bind_vars ) + prepare( sql ) do |stmt| + result = stmt.execute( *bind_vars ) + if block_given? + yield result.columns + result.each { |row| yield row } + else + return result.inject( [ result.columns ] ) { |arr,row| + arr << row; arr } + end + end + end + + # Executes all SQL statements in the given string. By contrast, the other + # means of executing queries will only execute the first statement in the + # string, ignoring all subsequent statements. This will execute each one + # in turn. The same bind parameters, if given, will be applied to each + # statement. + # + # This always returns +nil+, making it unsuitable for queries that return + # rows. + def execute_batch( sql, *bind_vars ) + sql = sql.strip + until sql.empty? do + prepare( sql ) do |stmt| + stmt.execute( *bind_vars ) + sql = stmt.remainder.strip + end + end + nil + end + + # A convenience method for obtaining the first row of a result set, and + # discarding all others. It is otherwise identical to #execute. + # + # See also #get_first_value. + def get_first_row( sql, *bind_vars ) + execute( sql, *bind_vars ) { |row| return row } + nil + end + + # A convenience method for obtaining the first value of the first row of a + # result set, and discarding all other values and rows. It is otherwise + # identical to #execute. + # + # See also #get_first_row. + def get_first_value( sql, *bind_vars ) + execute( sql, *bind_vars ) { |row| return row[0] } + nil + end + + # Obtains the unique row ID of the last row to be inserted by this Database + # instance. + def last_insert_row_id + @driver.last_insert_rowid( @handle ) + end + + # Returns the number of changes made to this database instance by the last + # operation performed. Note that a "delete from table" without a where + # clause will not affect this value. + def changes + @driver.changes( @handle ) + end + + # Returns the total number of changes made to this database instance + # since it was opened. + def total_changes + @driver.total_changes( @handle ) + end + + # Interrupts the currently executing operation, causing it to abort. + def interrupt + @driver.interrupt( @handle ) + end + + # Register a busy handler with this database instance. When a requested + # resource is busy, this handler will be invoked. If the handler returns + # +false+, the operation will be aborted; otherwise, the resource will + # be requested again. + # + # The handler will be invoked with the name of the resource that was + # busy, and the number of times it has been retried. + # + # See also #busy_timeout. + def busy_handler( data=nil, &block ) # :yields: data, retries + result = @driver.busy_handler( @handle, data, &block ) + Error.check( result, self ) + end + + # Indicates that if a request for a resource terminates because that + # resource is busy, SQLite should wait for the indicated number of + # milliseconds before trying again. By default, SQLite does not retry + # busy resources. To restore the default behavior, send 0 as the + # +ms+ parameter. + # + # See also #busy_handler. + def busy_timeout( ms ) + result = @driver.busy_timeout( @handle, ms ) + Error.check( result, self ) + end + + # Creates a new function for use in SQL statements. It will be added as + # +name+, with the given +arity+. (For variable arity functions, use + # -1 for the arity.) + # + # The block should accept at least one parameter--the FunctionProxy + # instance that wraps this function invocation--and any other + # arguments it needs (up to its arity). + # + # The block does not return a value directly. Instead, it will invoke + # the FunctionProxy#set_result method on the +func+ parameter and + # indicate the return value that way. + # + # Example: + # + # db.create_function( "maim", 1 ) do |func, value| + # if value.nil? + # func.result = nil + # else + # func.result = value.split(//).sort.join + # end + # end + # + # puts db.get_first_value( "select maim(name) from table" ) + def create_function( name, arity, text_rep=Constants::TextRep::ANY, + &block ) # :yields: func, *args + # begin + callback = proc do |func,*args| + begin + block.call( FunctionProxy.new( @driver, func ), + *args.map{|v| Value.new(self,v)} ) + rescue StandardError, Exception => e + @driver.result_error( func, + "#{e.message} (#{e.class})", -1 ) + end + end + + result = @driver.create_function( @handle, name, arity, text_rep, nil, + callback, nil, nil ) + Error.check( result, self ) + + self + end + + # Creates a new aggregate function for use in SQL statements. Aggregate + # functions are functions that apply over every row in the result set, + # instead of over just a single row. (A very common aggregate function + # is the "count" function, for determining the number of rows that match + # a query.) + # + # The new function will be added as +name+, with the given +arity+. (For + # variable arity functions, use -1 for the arity.) + # + # The +step+ parameter must be a proc object that accepts as its first + # parameter a FunctionProxy instance (representing the function + # invocation), with any subsequent parameters (up to the function's arity). + # The +step+ callback will be invoked once for each row of the result set. + # + # The +finalize+ parameter must be a +proc+ object that accepts only a + # single parameter, the FunctionProxy instance representing the current + # function invocation. It should invoke FunctionProxy#set_result to + # store the result of the function. + # + # Example: + # + # db.create_aggregate( "lengths", 1 ) do + # step do |func, value| + # func[ :total ] ||= 0 + # func[ :total ] += ( value ? value.length : 0 ) + # end + # + # finalize do |func| + # func.set_result( func[ :total ] || 0 ) + # end + # end + # + # puts db.get_first_value( "select lengths(name) from table" ) + # + # See also #create_aggregate_handler for a more object-oriented approach to + # aggregate functions. + def create_aggregate( name, arity, step=nil, finalize=nil, + text_rep=Constants::TextRep::ANY, &block ) + # begin + if block + proxy = AggregateDefinitionProxy.new + proxy.instance_eval &block + step ||= proxy.step_callback + finalize ||= proxy.finalize_callback + end + + step_callback = proc do |func,*args| + ctx = @driver.aggregate_context( func ) + unless ctx[:__error] + begin + step.call( FunctionProxy.new( @driver, func, ctx ), + *args.map{|v| Value.new(self,v)} ) + rescue Exception => e + ctx[:__error] = e + end + end + end + + finalize_callback = proc do |func| + ctx = @driver.aggregate_context( func ) + unless ctx[:__error] + begin + finalize.call( FunctionProxy.new( @driver, func, ctx ) ) + rescue Exception => e + @driver.result_error( func, + "#{e.message} (#{e.class})", -1 ) + end + else + e = ctx[:__error] + @driver.result_error( func, + "#{e.message} (#{e.class})", -1 ) + end + end + + result = @driver.create_function( @handle, name, arity, text_rep, nil, + nil, step_callback, finalize_callback ) + Error.check( result, self ) + + self + end + + # This is another approach to creating an aggregate function (see + # #create_aggregate). Instead of explicitly specifying the name, + # callbacks, arity, and type, you specify a factory object + # (the "handler") that knows how to obtain all of that information. The + # handler should respond to the following messages: + # + # +arity+:: corresponds to the +arity+ parameter of #create_aggregate. This + # message is optional, and if the handler does not respond to it, + # the function will have an arity of -1. + # +name+:: this is the name of the function. The handler _must_ implement + # this message. + # +new+:: this must be implemented by the handler. It should return a new + # instance of the object that will handle a specific invocation of + # the function. + # + # The handler instance (the object returned by the +new+ message, described + # above), must respond to the following messages: + # + # +step+:: this is the method that will be called for each step of the + # aggregate function's evaluation. It should implement the same + # signature as the +step+ callback for #create_aggregate. + # +finalize+:: this is the method that will be called to finalize the + # aggregate function's evaluation. It should implement the + # same signature as the +finalize+ callback for + # #create_aggregate. + # + # Example: + # + # class LengthsAggregateHandler + # def self.arity; 1; end + # + # def initialize + # @total = 0 + # end + # + # def step( ctx, name ) + # @total += ( name ? name.length : 0 ) + # end + # + # def finalize( ctx ) + # ctx.set_result( @total ) + # end + # end + # + # db.create_aggregate_handler( LengthsAggregateHandler ) + # puts db.get_first_value( "select lengths(name) from A" ) + def create_aggregate_handler( handler ) + arity = -1 + text_rep = Constants::TextRep::ANY + + arity = handler.arity if handler.respond_to?(:arity) + text_rep = handler.text_rep if handler.respond_to?(:text_rep) + name = handler.name + + step = proc do |func,*args| + ctx = @driver.aggregate_context( func ) + unless ctx[ :__error ] + ctx[ :handler ] ||= handler.new + begin + ctx[ :handler ].step( FunctionProxy.new( @driver, func, ctx ), + *args.map{|v| Value.new(self,v)} ) + rescue Exception, StandardError => e + ctx[ :__error ] = e + end + end + end + + finalize = proc do |func| + ctx = @driver.aggregate_context( func ) + unless ctx[ :__error ] + ctx[ :handler ] ||= handler.new + begin + ctx[ :handler ].finalize( FunctionProxy.new( @driver, func, ctx ) ) + rescue Exception => e + ctx[ :__error ] = e + end + end + + if ctx[ :__error ] + e = ctx[ :__error ] + @driver.sqlite3_result_error( func, "#{e.message} (#{e.class})", -1 ) + end + end + + result = @driver.create_function( @handle, name, arity, text_rep, nil, + nil, step, finalize ) + Error.check( result, self ) + + self + end + + # Begins a new transaction. Note that nested transactions are not allowed + # by SQLite, so attempting to nest a transaction will result in a runtime + # exception. + # + # The +mode+ parameter may be either <tt>:deferred</tt> (the default), + # <tt>:immediate</tt>, or <tt>:exclusive</tt>. + # + # If a block is given, the database instance is yielded to it, and the + # transaction is committed when the block terminates. If the block + # raises an exception, a rollback will be performed instead. Note that if + # a block is given, #commit and #rollback should never be called + # explicitly or you'll get an error when the block terminates. + # + # If a block is not given, it is the caller's responsibility to end the + # transaction explicitly, either by calling #commit, or by calling + # #rollback. + def transaction( mode = :deferred ) + execute "begin #{mode.to_s} transaction" + @transaction_active = true + + if block_given? + abort = false + begin + yield self + rescue Exception + abort = true + raise + ensure + abort and rollback or commit + end + end + + true + end + + # Commits the current transaction. If there is no current transaction, + # this will cause an error to be raised. This returns +true+, in order + # to allow it to be used in idioms like + # <tt>abort? and rollback or commit</tt>. + def commit + execute "commit transaction" + @transaction_active = false + true + end + + # Rolls the current transaction back. If there is no current transaction, + # this will cause an error to be raised. This returns +true+, in order + # to allow it to be used in idioms like + # <tt>abort? and rollback or commit</tt>. + def rollback + execute "rollback transaction" + @transaction_active = false + true + end + + # Returns +true+ if there is a transaction active, and +false+ otherwise. + def transaction_active? + @transaction_active + end + + # Loads the corresponding driver, or if it is nil, attempts to locate a + # suitable driver. + def load_driver( driver ) + case driver + when Class + # do nothing--use what was given + when Symbol, String + require "sqlite3/driver/#{driver.to_s.downcase}/driver" + driver = SQLite3::Driver.const_get( driver )::Driver + else + [ "Native", "DL" ].each do |d| + begin + require "sqlite3/driver/#{d.downcase}/driver" + driver = SQLite3::Driver.const_get( d )::Driver + break + rescue SyntaxError + raise + rescue ScriptError, Exception + end + end + raise "no driver for sqlite3 found" unless driver + end + + @driver = driver.new + end + private :load_driver + + # A helper class for dealing with custom functions (see #create_function, + # #create_aggregate, and #create_aggregate_handler). It encapsulates the + # opaque function object that represents the current invocation. It also + # provides more convenient access to the API functions that operate on + # the function object. + # + # This class will almost _always_ be instantiated indirectly, by working + # with the create methods mentioned above. + class FunctionProxy + + # Create a new FunctionProxy that encapsulates the given +func+ object. + # If context is non-nil, the functions context will be set to that. If + # it is non-nil, it must quack like a Hash. If it is nil, then none of + # the context functions will be available. + def initialize( driver, func, context=nil ) + @driver = driver + @func = func + @context = context + end + + # Calls #set_result to set the result of this function. + def result=( result ) + set_result( result ) + end + + # Set the result of the function to the given value. The function will + # then return this value. + def set_result( result, utf16=false ) + @driver.result_text( @func, result, utf16 ) + end + + # Set the result of the function to the given error message. + # The function will then return that error. + def set_error( error ) + @driver.result_error( @func, error.to_s, -1 ) + end + + # (Only available to aggregate functions.) Returns the number of rows + # that the aggregate has processed so far. This will include the current + # row, and so will always return at least 1. + def count + ensure_aggregate! + @driver.aggregate_count( @func ) + end + + # Returns the value with the given key from the context. This is only + # available to aggregate functions. + def []( key ) + ensure_aggregate! + @context[ key ] + end + + # Sets the value with the given key in the context. This is only + # available to aggregate functions. + def []=( key, value ) + ensure_aggregate! + @context[ key ] = value + end + + # A function for performing a sanity check, to ensure that the function + # being invoked is an aggregate function. This is implied by the + # existence of the context variable. + def ensure_aggregate! + unless @context + raise MisuseException, "function is not an aggregate" + end + end + private :ensure_aggregate! + + end + + # A proxy used for defining the callbacks to an aggregate function. + class AggregateDefinitionProxy # :nodoc: + attr_reader :step_callback, :finalize_callback + + def step( &block ) + @step_callback = block + end + + def finalize( &block ) + @finalize_callback = block + end + end + + end + +end + diff --git a/lib/sqlite3/driver/dl/api.rb b/lib/sqlite3/driver/dl/api.rb new file mode 100644 index 0000000..f892013 --- /dev/null +++ b/lib/sqlite3/driver/dl/api.rb @@ -0,0 +1,184 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'dl/import' + +module SQLite3 ; module Driver; module DL; + + module API + extend ::DL::Importable + + library_name = case RUBY_PLATFORM.downcase + when /darwin/ + "libsqlite3.dylib" + when /linux/ + "libsqlite3.so" + when /win32/ + "sqlite3.dll" + else + abort <<-EOF +== * UNSUPPORTED PLATFORM ====================================================== +The platform '#{RUBY_PLATFORM}' is unsupported. Please help the author by +editing the following file to allow your sqlite3 library to be found, and +submitting a patch to jamis_buck@byu.edu. Thanks! + +#{__FILE__} +=========================================================================== * == + EOF + end + + if defined? SQLITE3_LIB_PATH + library_name = File.join( SQLITE3_LIB_PATH, library_name ) + end + + dlload library_name + + typealias "db", "void*" + typealias "stmt", "void*" + typealias "value", "void*" + typealias "context", "void*" + + # until Ruby/DL supports 64-bit ints, we'll just treat them as 32-bit ints + typealias "int64", "unsigned long" + + extern "const char *sqlite3_libversion()" + + extern "int sqlite3_open(const char*,db*)" + extern "int sqlite3_open16(const void*,db*)" + extern "int sqlite3_close(db)" + extern "const char* sqlite3_errmsg(db)" + extern "void* sqlite3_errmsg16(db)" + extern "int sqlite3_errcode(db)" + + extern "int sqlite3_prepare(db,const char*,int,stmt*,const char**)" + extern "int sqlite3_prepare16(db,const void*,int,stmt*,const void**)" + extern "int sqlite3_finalize(stmt)" + extern "int sqlite3_reset(stmt)" + extern "int sqlite3_step(stmt)" + + extern "int64 sqlite3_last_insert_rowid(db)" + extern "int sqlite3_changes(db)" + extern "int sqlite3_total_changes(db)" + extern "void sqlite3_interrupt(db)" + extern "ibool sqlite3_complete(const char*)" + extern "ibool sqlite3_complete16(const void*)" + + extern "int sqlite3_busy_handler(db,void*,void*)" + extern "int sqlite3_busy_timeout(db,int)" + + extern "int sqlite3_set_authorizer(db,void*,void*)" + extern "void* sqlite3_trace(db,void*,void*)" + + extern "int sqlite3_bind_blob(stmt,int,const void*,int,void*)" + extern "int sqlite3_bind_double(stmt,int,double)" + extern "int sqlite3_bind_int(stmt,int,int)" + extern "int sqlite3_bind_int64(stmt,int,int64)" + extern "int sqlite3_bind_null(stmt,int)" + extern "int sqlite3_bind_text(stmt,int,const char*,int,void*)" + extern "int sqlite3_bind_text16(stmt,int,const void*,int,void*)" + #extern "int sqlite3_bind_value(stmt,int,value)" + + extern "int sqlite3_bind_parameter_count(stmt)" + extern "const char* sqlite3_bind_parameter_name(stmt,int)" + extern "int sqlite3_bind_parameter_index(stmt,const char*)" + + extern "int sqlite3_column_count(stmt)" + extern "int sqlite3_data_count(stmt)" + + extern "const void *sqlite3_column_blob(stmt,int)" + extern "int sqlite3_column_bytes(stmt,int)" + extern "int sqlite3_column_bytes16(stmt,int)" + extern "const char *sqlite3_column_decltype(stmt,int)" + extern "void *sqlite3_column_decltype16(stmt,int)" + extern "double sqlite3_column_double(stmt,int)" + extern "int sqlite3_column_int(stmt,int)" + extern "int64 sqlite3_column_int64(stmt,int)" + extern "const char *sqlite3_column_name(stmt,int)" + extern "const void *sqlite3_column_name16(stmt,int)" + extern "const char *sqlite3_column_text(stmt,int)" + extern "const void *sqlite3_column_text16(stmt,int)" + extern "int sqlite3_column_type(stmt,int)" + + extern "int sqlite3_create_function(db,const char*,int,int,void*,void*,void*,void*)" + extern "int sqlite3_create_function16(db,const void*,int,int,void*,void*,void*,void*)" + extern "int sqlite3_aggregate_count(context)" + + extern "const void *sqlite3_value_blob(value)" + extern "int sqlite3_value_bytes(value)" + extern "int sqlite3_value_bytes16(value)" + extern "double sqlite3_value_double(value)" + extern "int sqlite3_value_int(value)" + extern "int64 sqlite3_value_int64(value)" + extern "const char* sqlite3_value_text(value)" + extern "const void* sqlite3_value_text16(value)" + extern "const void* sqlite3_value_text16le(value)" + extern "const void* sqlite3_value_text16be(value)" + extern "int sqlite3_value_type(value)" + + extern "void *sqlite3_aggregate_context(context,int)" + extern "void *sqlite3_user_data(context)" + extern "void *sqlite3_get_auxdata(context,int)" + extern "void sqlite3_set_auxdata(context,int,void*,void*)" + + extern "void sqlite3_result_blob(context,const void*,int,void*)" + extern "void sqlite3_result_double(context,double)" + extern "void sqlite3_result_error(context,const char*,int)" + extern "void sqlite3_result_error16(context,const void*,int)" + extern "void sqlite3_result_int(context,int)" + extern "void sqlite3_result_int64(context,int64)" + extern "void sqlite3_result_null(context)" + extern "void sqlite3_result_text(context,const char*,int,void*)" + extern "void sqlite3_result_text16(context,const void*,int,void*)" + extern "void sqlite3_result_text16le(context,const void*,int,void*)" + extern "void sqlite3_result_text16be(context,const void*,int,void*)" + extern "void sqlite3_result_value(context,value)" + + extern "int sqlite3_create_collation(db,const char*,int,void*,void*)" + extern "int sqlite3_create_collation16(db,const char*,int,void*,void*)" + extern "int sqlite3_collation_needed(db,void*,void*)" + extern "int sqlite3_collation_needed16(db,void*,void*)" + + # ==== CRYPTO (NOT IN PUBLIC RELEASE) ==== + if defined?( CRYPTO_API ) && CRYPTO_API + extern "int sqlite3_key(db,void*,int)" + extern "int sqlite3_rekey(db,void*,int)" + end + + # ==== EXPERIMENTAL ==== + if defined?( EXPERIMENTAL_API ) && EXPERIMENTAL_API + extern "int sqlite3_progress_handler(db,int,void*,void*)" + extern "int sqlite3_commit_hook(db,void*,void*)" + end + + end + +end ; end ; end diff --git a/lib/sqlite3/driver/dl/driver.rb b/lib/sqlite3/driver/dl/driver.rb new file mode 100644 index 0000000..ab24a13 --- /dev/null +++ b/lib/sqlite3/driver/dl/driver.rb @@ -0,0 +1,334 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3/driver/dl/api' + +module Kernel + # Allows arbitrary objects to be passed as a pointer to functions. + # (Probably not very GC safe, but by encapsulating it like this we + # can change the implementation later.) + def to_ptr + ptr = DL.malloc(DL.sizeof("L")) + ptr.set_object self + ptr + end +end + +class DL::PtrData + # The inverse of the Kernel#to_ptr operation. + def to_object + n = to_s(4).unpack("L").first + return nil if n < 1 + ObjectSpace._id2ref(n) rescue self.to_s + end + + def set_object(obj) + self[0] = [obj.object_id].pack("L") + end +end + +module SQLite3 ; module Driver ; module DL + + class Driver + STATIC = ::DL::PtrData.new(0) + TRANSIENT = ::DL::PtrData.new(-1) + + def open( filename, utf16=false ) + handle = ::DL::PtrData.new(0) + result = API.send( ( utf16 ? :sqlite3_open16 : :sqlite3_open ), + filename+"\0", handle.ref ) + [ result, handle ] + end + + def errmsg( db, utf16=false ) + if utf16 + msg = API.sqlite3_errmsg16( db ) + msg.free = nil + msg.to_s(utf16_length(msg)) + else + API.sqlite3_errmsg( db ) + end + end + + def prepare( db, sql, utf16=false ) + handle = ::DL::PtrData.new(0) + remainder = ::DL::PtrData.new(0) + + result = API.send( ( utf16 ? :sqlite3_prepare16 : :sqlite3_prepare ), + db, sql+"\0", sql.length, handle.ref, remainder.ref ) + + args = utf16 ? [ utf16_length(remainder) ] : [] + remainder = remainder.to_s( *args ) + + [ result, handle, remainder ] + end + + def complete?( sql, utf16=false ) + API.send( utf16 ? :sqlite3_complete16 : :sqlite3_complete, sql+"\0" ) + end + + def value_blob( value ) + blob = API.sqlite3_value_blob( value ) + blob.free = nil + blob.to_s( API.sqlite3_value_bytes( value ) ) + end + + def value_text( value, utf16=false ) + method = case utf16 + when nil, false then :sqlite3_value_text + when :le then :sqlite3_value_text16le + when :be then :sqlite3_value_text16be + else :sqlite3_value_text16 + end + + result = API.send( method, value ) + if utf16 + result.free = nil + size = API.sqlite3_value_bytes( value ) + result = result.to_s( size ) + end + + result + end + + def column_blob( stmt, column ) + blob = API.sqlite3_column_blob( stmt, column ) + blob.free = nil + blob.to_s( API.sqlite3_column_bytes( stmt, column ) ) + end + + def result_text( func, text, utf16=false ) + method = case utf16 + when false, nil then :sqlite3_result_text + when :le then :sqlite3_result_text16le + when :be then :sqlite3_result_text16be + else :sqlite3_result_text16 + end + + s = text.to_s + API.send( method, func, s, s.length, TRANSIENT ) + end + + def busy_handler( db, data=nil, &block ) + @busy_handler = block + + unless @busy_handler_callback + @busy_handler_callback = ::DL.callback( "IPI" ) do |cookie, timeout| + @busy_handler.call( cookie, timeout ) || 0 + end + end + + API.sqlite3_busy_handler( db, block&&@busy_handler_callback, data ) + end + + def set_authorizer( db, data=nil, &block ) + @authorizer_handler = block + + unless @authorizer_handler_callback + @authorizer_handler_callback = ::DL.callback( "IPIPPPP" + ) do |cookie,mode,a,b,c,d| + @authorizer_handler.call( cookie, mode, + a&&a.to_s, b&&b.to_s, c&&c.to_s, d&&d.to_s ) || 0 + end + end + + API.sqlite3_set_authorizer( db, block&&@authorizer_handler_callback, + data ) + end + + def trace( db, data=nil, &block ) + @trace_handler = block + + unless @trace_handler_callback + @trace_handler_callback = ::DL.callback( "IPS" ) do |cookie,sql| + @trace_handler.call( cookie ? cookie.to_object : nil, sql ) || 0 + end + end + + API.sqlite3_trace( db, block&&@trace_handler_callback, data ) + end + + def create_function( db, name, args, text, cookie, + func, step, final ) + # begin + if @func_handler_callback.nil? && func + @func_handler_callback = ::DL.callback( "0PIP" ) do |context,nargs,args| + args = args.to_s(nargs*4).unpack("L*").map {|i| ::DL::PtrData.new(i)} + data = API.sqlite3_user_data( context ).to_object + data[:func].call( context, *args ) + end + end + + if @step_handler_callback.nil? && step + @step_handler_callback = ::DL.callback( "0PIP" ) do |context,nargs,args| + args = args.to_s(nargs*4).unpack("L*").map {|i| ::DL::PtrData.new(i)} + data = API.sqlite3_user_data( context ).to_object + data[:step].call( context, *args ) + end + end + + if @final_handler_callback.nil? && final + @final_handler_callback = ::DL.callback( "0P" ) do |context| + data = API.sqlite3_user_data( context ).to_object + data[:final].call( context ) + end + end + + data = { :cookie => cookie, + :name => name, + :func => func, + :step => step, + :final => final } + + API.sqlite3_create_function( db, name, args, text, data, + ( func ? @func_handler_callback : nil ), + ( step ? @step_handler_callback : nil ), + ( final ? @final_handler_callback : nil ) ) + end + + def aggregate_context( context ) + ptr = API.sqlite3_aggregate_context( context, 4 ) + ptr.free = nil + obj = ( ptr ? ptr.to_object : nil ) + if obj.nil? + obj = Hash.new + ptr.set_object obj + end + obj + end + + def bind_blob( stmt, index, value ) + s = value.to_s + API.sqlite3_bind_blob( stmt, index, s, s.length, TRANSIENT ) + end + + def bind_text( stmt, index, value, utf16=false ) + s = value.to_s + method = ( utf16 ? :sqlite3_bind_text16 : :sqlite3_bind_text ) + API.send( method, stmt, index, s, s.length, TRANSIENT ) + end + + def column_text( stmt, column ) + result = API.sqlite3_column_text( stmt, column ) + result ? result.to_s : nil + end + + def column_name( stmt, column ) + result = API.sqlite3_column_name( stmt, column ) + result ? result.to_s : nil + end + + def column_decltype( stmt, column ) + result = API.sqlite3_column_decltype( stmt, column ) + result ? result.to_s : nil + end + + def self.api_delegate( name ) + define_method( name ) { |*args| API.send( "sqlite3_#{name}", *args ) } + end + + api_delegate :aggregate_count + api_delegate :bind_double + api_delegate :bind_int + api_delegate :bind_null + api_delegate :bind_parameter_index + api_delegate :bind_parameter_name + api_delegate :busy_timeout + api_delegate :changes + api_delegate :close + api_delegate :column_bytes + api_delegate :column_bytes16 + api_delegate :column_count + api_delegate :column_double + api_delegate :column_int + api_delegate :column_int64 + api_delegate :column_type + api_delegate :data_count + api_delegate :errcode + api_delegate :finalize + api_delegate :interrupt + api_delegate :last_insert_rowid + api_delegate :libversion + api_delegate :reset + api_delegate :result_error + api_delegate :step + api_delegate :total_changes + api_delegate :value_bytes + api_delegate :value_bytes16 + api_delegate :value_double + api_delegate :value_int + api_delegate :value_int64 + api_delegate :value_type + + # ==== EXPERIMENTAL ==== + if defined?( EXPERIMENTAL_API ) && EXPERIMENTAL_API + def progress_handler( db, n, data=nil, &block ) + @progress_handler = block + + unless @progress_handler_callback + @progress_handler_callback = ::DL.callback( "IP" ) do |cookie| + @progress_handler.call( cookie ) + end + end + + API.sqlite3_progress_handler( db, n, block&&@progress_handler_callback, + data ) + end + + def commit_hook( db, data=nil, &block ) + @commit_hook_handler = block + + unless @commit_hook_handler_callback + @commit_hook_handler_callback = ::DL.callback( "IP" ) do |cookie| + @commit_hook_handler.call( cookie ) + end + end + + API.sqlite3_commit_hook( db, block&&@commit_hook_handler_callback, + data ) + end + end + + private + + def utf16_length(ptr) + len = 0 + loop do + break if ptr[len,1] == "\0" + len += 2 + end + len + end + + end + +end ; end ; end diff --git a/lib/sqlite3/driver/native/driver.rb b/lib/sqlite3/driver/native/driver.rb new file mode 100644 index 0000000..ee5e4a7 --- /dev/null +++ b/lib/sqlite3/driver/native/driver.rb @@ -0,0 +1,218 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3_api' + +module SQLite3 ; module Driver ; module Native + + class Driver + + def initialize + @callback_data = Hash.new + end + + def complete?( sql, utf16=false ) + API.send( utf16 ? :sqlite3_complete16 : :sqlite3_complete, sql ) != 0 + end + + def busy_handler( db, data=nil, &block ) + if block + cb = API::CallbackData.new + cb.proc = block + cb.data = data + end + + API.sqlite3_busy_handler( db, + block ? API::Sqlite3_ruby_busy_handler : nil, cb ) + end + + def set_authorizer( db, data=nil, &block ) + if block + cb = API::CallbackData.new + cb.proc = block + cb.data = data + end + + API.sqlite3_set_authorizer( db, + block ? API::Sqlite3_ruby_authorizer : nil, cb ) + end + + def trace( db, data=nil, &block ) + if block + cb = API::CallbackData.new + cb.proc = block + cb.data = data + end + + API.sqlite3_trace( db, + block ? API::Sqlite3_ruby_trace : nil, cb ) + end + + def open( filename, utf16=false ) + API.send( utf16 ? :sqlite3_open16 : :sqlite3_open, filename ) + end + + def errmsg( db, utf16=false ) + API.send( utf16 ? :sqlite3_errmsg16 : :sqlite3_errmsg, db ) + end + + def prepare( db, sql, utf16=false ) + API.send( ( utf16 ? :sqlite3_prepare16 : :sqlite3_prepare ), + db, sql ) + end + + def bind_text( stmt, index, value, utf16=false ) + API.send( ( utf16 ? :sqlite3_bind_text16 : :sqlite3_bind_text ), + stmt, index, value ) + end + + def column_name( stmt, index, utf16=false ) + API.send( ( utf16 ? :sqlite3_column_name16 : :sqlite3_column_name ), + stmt, index ) + end + + def column_decltype( stmt, index, utf16=false ) + API.send( + ( utf16 ? :sqlite3_column_decltype16 : :sqlite3_column_decltype ), + stmt, index ) + end + + def column_text( stmt, index, utf16=false ) + API.send( ( utf16 ? :sqlite3_column_text16 : :sqlite3_column_text ), + stmt, index ) + end + + def create_function( db, name, args, text, cookie, func, step, final ) + if func || ( step && final ) + cb = API::CallbackData.new + cb.proc = cb.proc2 = nil + cb.data = cookie + @callback_data[ name ] = cb + else + @callback_data.delete( name ) + end + + if func + cb.proc = func + + func = API::Sqlite3_ruby_function_step + step = final = nil + elsif step && final + cb.proc = step + cb.proc2 = final + + func = nil + step = API::Sqlite3_ruby_function_step + final = API::Sqlite3_ruby_function_final + end + + API.sqlite3_create_function( db, name, args, text, cb, func, step, final ) + end + + def value_text( value, utf16=false ) + method = case utf16 + when nil, false then :sqlite3_value_text + when :le then :sqlite3_value_text16le + when :be then :sqlite3_value_text16be + else :sqlite3_value_text16 + end + + API.send( method, value ) + end + + def result_text( context, result, utf16=false ) + method = case utf16 + when nil, false then :sqlite3_result_text + when :le then :sqlite3_result_text16le + when :be then :sqlite3_result_text16be + else :sqlite3_result_text16 + end + + API.send( method, context, result.to_s ) + end + + def result_error( context, value, utf16=false ) + API.send( ( utf16 ? :sqlite3_result_error16 : :sqlite3_result_error ), + context, value ) + end + + def self.api_delegate( name ) + define_method( name ) { |*args| API.send( "sqlite3_#{name}", *args ) } + end + + api_delegate :libversion + api_delegate :close + api_delegate :last_insert_rowid + api_delegate :changes + api_delegate :total_changes + api_delegate :interrupt + api_delegate :busy_timeout + api_delegate :errcode + api_delegate :bind_blob + api_delegate :bind_double + api_delegate :bind_int + api_delegate :bind_int64 + api_delegate :bind_null + api_delegate :bind_parameter_count + api_delegate :bind_parameter_name + api_delegate :bind_parameter_index + api_delegate :column_count + api_delegate :step + api_delegate :data_count + api_delegate :column_blob + api_delegate :column_bytes + api_delegate :column_bytes16 + api_delegate :column_double + api_delegate :column_int + api_delegate :column_int64 + api_delegate :column_type + api_delegate :finalize + api_delegate :reset + api_delegate :aggregate_count + api_delegate :value_blob + api_delegate :value_bytes + api_delegate :value_bytes16 + api_delegate :value_double + api_delegate :value_int + api_delegate :value_int64 + api_delegate :value_type + api_delegate :result_blob + api_delegate :result_double + api_delegate :result_int + api_delegate :result_int64 + api_delegate :result_null + api_delegate :result_value + api_delegate :aggregate_context + + end + +end ; end ; end diff --git a/lib/sqlite3/errors.rb b/lib/sqlite3/errors.rb new file mode 100644 index 0000000..308a4c9 --- /dev/null +++ b/lib/sqlite3/errors.rb @@ -0,0 +1,84 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3/constants' + +module SQLite3 + + class Exception < ::Exception; end + + class SQLException < Exception; end + class InternalException < Exception; end + class PermissionException < Exception; end + class AbortException < Exception; end + class BusyException < Exception; end + class LockedException < Exception; end + class MemoryException < Exception; end + class ReadOnlyException < Exception; end + class InterruptException < Exception; end + class IOException < Exception; end + class CorruptException < Exception; end + class NotFoundException < Exception; end + class FullException < Exception; end + class CantOpenException < Exception; end + class ProtocolException < Exception; end + class EmptyException < Exception; end + class SchemaChangedException < Exception; end + class TooBigException < Exception; end + class ConstraintException < Exception; end + class MismatchException < Exception; end + class MisuseException < Exception; end + class UnsupportedException < Exception; end + class AuthorizationException < Exception; end + + EXCEPTIONS = [ + nil, + SQLException, InternalException, PermissionException, + AbortException, BusyException, LockedException, MemoryException, + ReadOnlyException, InterruptException, IOException, CorruptException, + NotFoundException, FullException, CantOpenException, ProtocolException, + EmptyException, SchemaChangedException, TooBigException, + ConstraintException, MismatchException, MisuseException, + UnsupportedException, AuthorizationException + ] + + module Error + def check( result, db=nil, msg=nil ) + unless result == Constants::ErrorCode::OK + msg = ( msg ? msg + ": " : "" ) + db.errmsg if db + raise EXCEPTIONS[result], msg + end + end + module_function :check + end + +end diff --git a/lib/sqlite3/pragmas.rb b/lib/sqlite3/pragmas.rb new file mode 100644 index 0000000..7247387 --- /dev/null +++ b/lib/sqlite3/pragmas.rb @@ -0,0 +1,254 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3/errors' + +module SQLite3 + + # This module is intended for inclusion solely by the Database class. It + # defines convenience methods for the various pragmas supported by SQLite3. + # + # For a detailed description of these pragmas, see the SQLite3 documentation + # at http://sqlite.org/pragma.html. + module Pragmas + + # Returns +true+ or +false+ depending on the value of the named pragma. + def get_boolean_pragma( name ) + get_first_value( "PRAGMA #{name}" ) != "0" + end + private :get_boolean_pragma + + # Sets the given pragma to the given boolean value. The value itself + # may be +true+ or +false+, or any other commonly used string or + # integer that represents truth. + def set_boolean_pragma( name, mode ) + case mode + when String + case mode.downcase + when "on", "yes", "true", "y", "t": mode = "'ON'" + when "off", "no", "false", "n", "f": mode = "'OFF'" + else + raise Exception, + "unrecognized pragma parameter #{mode.inspect}" + end + when true, 1 + mode = "ON" + when false, 0, nil + mode = "OFF" + else + raise Exception, + "unrecognized pragma parameter #{mode.inspect}" + end + + execute( "PRAGMA #{name}=#{mode}" ) + end + private :set_boolean_pragma + + # Requests the given pragma (and parameters), and if the block is given, + # each row of the result set will be yielded to it. Otherwise, the results + # are returned as an array. + def get_query_pragma( name, *parms, &block ) # :yields: row + if parms.empty? + execute( "PRAGMA #{name}", &block ) + else + args = "'" + parms.join("','") + "'" + execute( "PRAGMA #{name}( #{args} )", &block ) + end + end + private :get_query_pragma + + # Return the value of the given pragma. + def get_enum_pragma( name ) + get_first_value( "PRAGMA #{name}" ) + end + private :get_enum_pragma + + # Set the value of the given pragma to +mode+. The +mode+ parameter must + # conform to one of the values in the given +enum+ array. Each entry in + # the array is another array comprised of elements in the enumeration that + # have duplicate values. See #synchronous, #default_synchronous, + # #temp_store, and #default_temp_store for usage examples. + def set_enum_pragma( name, mode, enums ) + match = enums.find { |p| p.find { |i| i.to_s.downcase == mode.to_s.downcase } } + raise Exception, + "unrecognized #{name} #{mode.inspect}" unless match + execute( "PRAGMA #{name}='#{match.first.upcase}'" ) + end + private :set_enum_pragma + + # Returns the value of the given pragma as an integer. + def get_int_pragma( name ) + get_first_value( "PRAGMA #{name}" ).to_i + end + private :get_int_pragma + + # Set the value of the given pragma to the integer value of the +value+ + # parameter. + def set_int_pragma( name, value ) + execute( "PRAGMA #{name}=#{value.to_i}" ) + end + private :set_int_pragma + + # The enumeration of valid synchronous modes. + SYNCHRONOUS_MODES = [ [ 'full', 2 ], [ 'normal', 1 ], [ 'off', 0 ] ] + + # The enumeration of valid temp store modes. + TEMP_STORE_MODES = [ [ 'default', 0 ], [ 'file', 1 ], [ 'memory', 2 ] ] + + # Does an integrity check on the database. If the check fails, a + # SQLite3::Exception will be raised. Otherwise it + # returns silently. + def integrity_check + execute( "PRAGMA integrity_check" ) do |row| + raise Exception, row[0] if row[0] != "ok" + end + end + + def auto_vacuum + get_boolean_pragma "auto_vacuum" + end + + def auto_vacuum=( mode ) + set_boolean_pragma "auto_vacuum", mode + end + + def schema_cookie + get_int_pragma "schema_cookie" + end + + def schema_cookie=( cookie ) + set_int_pragma "schema_cookie", cookie + end + + def user_cookie + get_int_pragma "user_cookie" + end + + def user_cookie=( cookie ) + set_int_pragma "user_cookie", cookie + end + + def cache_size + get_int_pragma "cache_size" + end + + def cache_size=( size ) + set_int_pragma "cache_size", size + end + + def default_cache_size + get_int_pragma "default_cache_size" + end + + def default_cache_size=( size ) + set_int_pragma "default_cache_size", size + end + + def default_synchronous + get_enum_pragma "default_synchronous" + end + + def default_synchronous=( mode ) + set_enum_pragma "default_synchronous", mode, SYNCHRONOUS_MODES + end + + def synchronous + get_enum_pragma "synchronous" + end + + def synchronous=( mode ) + set_enum_pragma "synchronous", mode, SYNCHRONOUS_MODES + end + + def default_temp_store + get_enum_pragma "default_temp_store" + end + + def default_temp_store=( mode ) + set_enum_pragma "default_temp_store", mode, TEMP_STORE_MODES + end + + def temp_store + get_enum_pragma "temp_store" + end + + def temp_store=( mode ) + set_enum_pragma "temp_store", mode, TEMP_STORE_MODES + end + + def full_column_names + get_boolean_pragma "full_column_names" + end + + def full_column_names=( mode ) + set_boolean_pragma "full_column_names", mode + end + + def parser_trace + get_boolean_pragma "parser_trace" + end + + def parser_trace=( mode ) + set_boolean_pragma "parser_trace", mode + end + + def vdbe_trace + get_boolean_pragma "vdbe_trace" + end + + def vdbe_trace=( mode ) + set_boolean_pragma "vdbe_trace", mode + end + + def database_list( &block ) # :yields: row + get_query_pragma "database_list", &block + end + + def foreign_key_list( table, &block ) # :yields: row + get_query_pragma "foreign_key_list", table, &block + end + + def index_info( index, &block ) # :yields: row + get_query_pragma "index_info", index, &block + end + + def index_list( table, &block ) # :yields: row + get_query_pragma "index_list", table, &block + end + + def table_info( table, &block ) # :yields: row + get_query_pragma "table_info", table, &block + end + + end + +end diff --git a/lib/sqlite3/resultset.rb b/lib/sqlite3/resultset.rb new file mode 100644 index 0000000..bef2a0d --- /dev/null +++ b/lib/sqlite3/resultset.rb @@ -0,0 +1,172 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3/constants' +require 'sqlite3/errors' + +module SQLite3 + + # The ResultSet object encapsulates the enumerability of a query's output. + # It is a simple cursor over the data that the query returns. It will + # very rarely (if ever) be instantiated directly. Instead, client's should + # obtain a ResultSet instance via Statement#execute. + class ResultSet + include Enumerable + + # A trivial module for adding a +types+ accessor to an object. + module TypesContainer + attr_accessor :types + end + + # A trivial module for adding a +fields+ accessor to an object. + module FieldsContainer + attr_accessor :fields + end + + # Create a new ResultSet attached to the given database, using the + # given sql text. + def initialize( db, stmt ) + @db = db + @driver = @db.driver + @stmt = stmt + commence + end + + # A convenience method for compiling the virtual machine and stepping + # to the first row of the result set. + def commence + result = @driver.step( @stmt.handle ) + check result + @first_row = true + end + private :commence + + def check( result ) + @eof = ( result == Constants::ErrorCode::DONE ) + found = ( result == Constants::ErrorCode::ROW ) + Error.check( result, @db ) unless @eof || found + end + private :check + + # Reset the cursor, so that a result set which has reached end-of-file + # can be rewound and reiterated. + def reset( *bind_params ) + @driver.reset( @stmt.handle ) + @stmt.bind_params( *bind_params ) + @eof = false + commence + end + + # Query whether the cursor has reached the end of the result set or not. + def eof? + @eof + end + + # Obtain the next row from the cursor. If there are no more rows to be + # had, this will return +nil+. If type translation is active on the + # corresponding database, the values in the row will be translated + # according to their types. + # + # The returned value will be an array, unless Database#results_as_hash has + # been set to +true+, in which case the returned value will be a hash. + # + # For arrays, the column names are accessible via the +fields+ property, + # and the column types are accessible via the +types+ property. + # + # For hashes, the column names are the keys of the hash, and the column + # types are accessible via the +types+ property. + def next + return nil if @eof + + unless @first_row + result = @driver.step( @stmt.handle ) + check result + end + + @first_row = false + + unless @eof + row = [] + @driver.data_count( @stmt.handle ).times do |column| + case @driver.column_type( @stmt.handle, column ) + when Constants::ColumnType::NULL then + row << nil + when Constants::ColumnType::BLOB then + row << @driver.column_blob( @stmt.handle, column ) + else + row << @driver.column_text( @stmt.handle, column ) + end + end + + if @db.type_translation + row = @stmt.types.zip( row ).map do |type, value| + @db.translator.translate( type, value ) + end + end + + if @db.results_as_hash + new_row = Hash[ *( @stmt.columns.zip( row ).flatten ) ] + row.each_with_index { |value,idx| new_row[idx] = value } + row = new_row + else + row.extend FieldsContainer unless row.respond_to?(:fields) + row.fields = @stmt.columns + end + + row.extend TypesContainer + row.types = @stmt.types + + return row + end + + nil + end + + # Required by the Enumerable mixin. Provides an internal iterator over the + # rows of the result set. + def each + while row=self.next + yield row + end + end + + def types + @stmt.types + end + + def columns + @stmt.columns + end + + end + +end diff --git a/lib/sqlite3/statement.rb b/lib/sqlite3/statement.rb new file mode 100644 index 0000000..8d2e3d5 --- /dev/null +++ b/lib/sqlite3/statement.rb @@ -0,0 +1,218 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3/errors' +require 'sqlite3/resultset' + +class String + def to_blob + SQLite3::Blob.new( self ) + end +end + +module SQLite3 + + # A class for differentiating between strings and blobs, when binding them + # into statements. + class Blob < String; end + + # A statement represents a prepared-but-unexecuted SQL query. It will rarely + # (if ever) be instantiated directly by a client, and is most often obtained + # via the Database#prepare method. + class Statement + + # This is any text that followed the first valid SQL statement in the text + # with which the statement was initialized. If there was no trailing text, + # this will be the empty string. + attr_reader :remainder + + # The underlying opaque handle used to access the SQLite @driver. + attr_reader :handle + + # Create a new statement attached to the given Database instance, and which + # encapsulates the given SQL text. If the text contains more than one + # statement (i.e., separated by semicolons), then the #remainder property + # will be set to the trailing text. + def initialize( db, sql, utf16=false ) + @db = db + @driver = @db.driver + result, @handle, @remainder = @driver.prepare( @db.handle, sql ) + Error.check( result, @db ) + end + + def close + @driver.finalize( @handle ) + end + + # Binds the given variables to the corresponding placeholders in the SQL + # text. + # + # See Database#execute for a description of the valid placeholder + # syntaxes. + # + # Example: + # + # stmt = db.prepare( "select * from table where a=? and b=?" ) + # stmt.bind_params( 15, "hello" ) + # + # See also #execute, #bind_param, Statement#bind_param, and + # Statement#bind_params. + def bind_params( *bind_vars ) + index = 1 + bind_vars.each do |var| + if Hash === var + var.each { |key, val| bind_param key, val } + else + bind_param index, var + index += 1 + end + end + end + + # Binds value to the named (or positional) placeholder. If +param+ is a + # Fixnum, it is treated as an index for a positional placeholder. + # Otherwise it is used as the name of the placeholder to bind to. + # + # See also #bind_params. + def bind_param( param, value ) + if Fixnum === param + case value + when Integer then + @driver.bind_int( @handle, param, value ) + when Numeric then + @driver.bind_double( @handle, param, value.to_f ) + when Blob then + @driver.bind_blob( @handle, param, value ) + when nil then + @driver.bind_null( @handle, param ) + else + @driver.bind_text( @handle, param, value ) + end + else + index = @driver.bind_parameter_index( + @handle, param.to_s ) + raise Exception, "no such bind parameter '#{param}'" if index == 0 + bind_param index, value + end + end + + # Execute the statement. This creates a new ResultSet object for the + # statement's virtual machine. If a block was given, the new ResultSet will + # be yielded to it; otherwise, the ResultSet will be returned. + # + # Any parameters will be bound to the statement using #bind_params. + # + # Example: + # + # stmt = db.prepare( "select * from table" ) + # stmt.execute do |result| + # ... + # end + # + # See also #bind_params, #execute!. + def execute( *bind_vars ) + @driver.reset( @handle ) if @results + + bind_params *bind_vars unless bind_vars.empty? + @results = ResultSet.new( @db, self ) + + if block_given? + yield @results + else + return @results + end + end + + # Execute the statement. If no block was given, this returns an array of + # rows returned by executing the statement. Otherwise, each row will be + # yielded to the block. + # + # Any parameters will be bound to the statement using #bind_params. + # + # Example: + # + # stmt = db.prepare( "select * from table" ) + # stmt.execute! do |row| + # ... + # end + # + # See also #bind_params, #execute. + def execute!( *bind_vars ) + result = execute( *bind_vars ) + rows = [] unless block_given? + while row = result.next + if block_given? + yield row + else + rows << row + end + end + rows + end + + # Return an array of the column names for this statement. Note that this + # may execute the statement in order to obtain the metadata; this makes it + # a (potentially) expensive operation. + def columns + get_metadata unless @columns + return @columns + end + + # Return an array of the data types for each column in this statement. Note + # that this may execute the statement in order to obtain the metadata; this + # makes it a (potentially) expensive operation. + def types + get_metadata unless @types + return @types + end + + # A convenience method for obtaining the metadata about the query. Note + # that this will actually execute the SQL, which means it can be a + # (potentially) expensive operation. + def get_metadata + @columns = [] + @types = [] + + column_count = @driver.column_count( @handle ) + column_count.times do |column| + @columns << @driver.column_name( @handle, column ) + @types << @driver.column_decltype( @handle, column ) + end + + @columns.freeze + @types.freeze + end + private :get_metadata + + end + +end diff --git a/lib/sqlite3/translator.rb b/lib/sqlite3/translator.rb new file mode 100644 index 0000000..1ee315e --- /dev/null +++ b/lib/sqlite3/translator.rb @@ -0,0 +1,135 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'time' + +module SQLite3 + + # The Translator class encapsulates the logic and callbacks necessary for + # converting string data to a value of some specified type. Every Database + # instance may have a Translator instance, in order to assist in type + # translation (Database#type_translation). + # + # Further, applications may define their own custom type translation logic + # by registering translator blocks with the corresponding database's + # translator instance (Database#translator). + class Translator + + # Create a new Translator instance. It will be preinitialized with default + # translators for most SQL data types. + def initialize + @translators = Hash.new( proc { |type,value| value } ) + register_default_translators + end + + # Add a new translator block, which will be invoked to process type + # translations to the given type. The type should be an SQL datatype, and + # may include parentheses (i.e., "VARCHAR(30)"). However, any parenthetical + # information is stripped off and discarded, so type translation decisions + # are made solely on the "base" type name. + # + # The translator block itself should accept two parameters, "type" and + # "value". In this case, the "type" is the full type name (including + # parentheses), so the block itself may include logic for changing how a + # type is translated based on the additional data. The "value" parameter + # is the (string) data to convert. + # + # The block should return the translated value. + def add_translator( type, &block ) # :yields: type, value + @translators[ type_name( type ) ] = block + end + + # Translate the given string value to a value of the given type. In the + # absense of an installed translator block for the given type, the value + # itself is always returned. Further, +nil+ values are never translated, + # and are always passed straight through regardless of the type parameter. + def translate( type, value ) + unless value.nil? + @translators[ type_name( type ) ].call( type, value ) + end + end + + # A convenience method for working with type names. This returns the "base" + # type name, without any parenthetical data. + def type_name( type ) + type = $1 if type =~ /^(.*?)\(/ + type.upcase + end + private :type_name + + # Register the default translators for the current Translator instance. + # This includes translators for most major SQL data types. + def register_default_translators + [ "date", + "datetime", + "time" ].each { |type| add_translator( type ) { |t,v| Time.parse( v ) } } + + [ "decimal", + "float", + "numeric", + "double", + "real", + "dec", + "fixed" ].each { |type| add_translator( type ) { |t,v| v.to_f } } + + [ "integer", + "smallint", + "mediumint", + "int", + "bigint" ].each { |type| add_translator( type ) { |t,v| v.to_i } } + + [ "bit", + "bool", + "boolean" ].each do |type| + add_translator( type ) do |t,v| + !( v.strip.gsub(/00+/,"0") == "0" || + v.downcase == "false" || + v.downcase == "f" || + v.downcase == "no" || + v.downcase == "n" ) + end + end + + add_translator( "timestamp" ) { |type, value| Time.at( value.to_i ) } + add_translator( "tinyint" ) do |type, value| + if type =~ /\(\s*1\s*\)/ + value.to_i == 1 + else + value.to_i + end + end + end + private :register_default_translators + + end + +end diff --git a/lib/sqlite3/value.rb b/lib/sqlite3/value.rb new file mode 100644 index 0000000..fb76376 --- /dev/null +++ b/lib/sqlite3/value.rb @@ -0,0 +1,89 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'sqlite3/constants' + +module SQLite3 + + class Value + attr_reader :handle + + def initialize( db, handle ) + @driver = db.driver + @handle = handle + end + + def null? + type == :null + end + + def to_blob + @driver.value_blob( @handle ) + end + + def length( utf16=false ) + if utf16 + @driver.value_bytes16( @handle ) + else + @driver.value_bytes( @handle ) + end + end + + def to_f + @driver.value_double( @handle ) + end + + def to_i + @driver.value_int( @handle ) + end + + def to_int64 + @driver.value_int64( @handle ) + end + + def to_s( utf16=false ) + @driver.value_text( @handle, utf16 ) + end + + def type + case @driver.value_type( @handle ) + when Constants::ColumnType::INTEGER then :int + when Constants::ColumnType::FLOAT then :float + when Constants::ColumnType::TEXT then :text + when Constants::ColumnType::BLOB then :blob + when Constants::ColumnType::NULL then :null + end + end + + end + +end diff --git a/lib/sqlite3/version.rb b/lib/sqlite3/version.rb new file mode 100644 index 0000000..542cd75 --- /dev/null +++ b/lib/sqlite3/version.rb @@ -0,0 +1,45 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +module SQLite3 + + module Version + + MAJOR = 0 + MINOR = 9 + TINY = 0 + + STRING = [ MAJOR, MINOR, TINY ].join( "." ) + + end + +end diff --git a/setup.rb b/setup.rb new file mode 100644 index 0000000..5f22fc1 --- /dev/null +++ b/setup.rb @@ -0,0 +1,1333 @@ +# +# setup.rb +# +# Copyright (c) 2000-2004 Minero Aoki +# +# This program is free software. +# You can distribute/modify this program under the terms of +# the GNU LGPL, Lesser General Public License version 2.1. +# + +# +# For backward compatibility +# + +unless Enumerable.method_defined?(:map) + module Enumerable + alias map collect + end +end + +unless Enumerable.method_defined?(:detect) + module Enumerable + alias detect find + end +end + +unless Enumerable.method_defined?(:select) + module Enumerable + alias select find_all + end +end + +unless Enumerable.method_defined?(:reject) + module Enumerable + def reject + select {|i| not yield(i) } + end + end +end + +unless Enumerable.method_defined?(:inject) + module Enumerable + def inject(result) + each do |i| + result = yield(result, i) + end + result + end + end +end + +unless Enumerable.method_defined?(:any?) + module Enumerable + def any? + each do |i| + return true if yield(i) + end + false + end + end +end + +unless File.respond_to?(:read) + def File.read(fname) + open(fname) {|f| + return f.read + } + end +end + +# +# Application independent utilities +# + +def File.binread(fname) + open(fname, 'rb') {|f| + return f.read + } +end + +# for corrupted windows stat(2) +def File.dir?(path) + File.directory?((path[-1,1] == '/') ? path : path + '/') +end + +# +# Config +# + +if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg } + ARGV.delete(arg) + require arg.split(/=/, 2)[1] + $".push 'rbconfig.rb' +else + require 'rbconfig' +end + +def multipackage_install? + FileTest.directory?(File.dirname($0) + '/packages') +end + + +class ConfigTable + + c = ::Config::CONFIG + + rubypath = c['bindir'] + '/' + c['ruby_install_name'] + + major = c['MAJOR'].to_i + minor = c['MINOR'].to_i + teeny = c['TEENY'].to_i + version = "#{major}.#{minor}" + + # ruby ver. >= 1.4.4? + newpath_p = ((major >= 2) or + ((major == 1) and + ((minor >= 5) or + ((minor == 4) and (teeny >= 4))))) + + subprefix = lambda {|path| + path.sub(/\A#{Regexp.quote(c['prefix'])}/o, '$prefix') + } + + if c['rubylibdir'] + # V < 1.6.3 + stdruby = subprefix.call(c['rubylibdir']) + siteruby = subprefix.call(c['sitedir']) + versite = subprefix.call(c['sitelibdir']) + sodir = subprefix.call(c['sitearchdir']) + elsif newpath_p + # 1.4.4 <= V <= 1.6.3 + stdruby = "$prefix/lib/ruby/#{version}" + siteruby = subprefix.call(c['sitedir']) + versite = siteruby + '/' + version + sodir = "$site-ruby/#{c['arch']}" + else + # V < 1.4.4 + stdruby = "$prefix/lib/ruby/#{version}" + siteruby = "$prefix/lib/ruby/#{version}/site_ruby" + versite = siteruby + sodir = "$site-ruby/#{c['arch']}" + end + + if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg } + makeprog = arg.sub(/'/, '').split(/=/, 2)[1] + else + makeprog = 'make' + end + + common_descripters = [ + [ 'prefix', [ c['prefix'], + 'path', + 'path prefix of target environment' ] ], + [ 'std-ruby', [ stdruby, + 'path', + 'the directory for standard ruby libraries' ] ], + [ 'site-ruby-common', [ siteruby, + 'path', + 'the directory for version-independent non-standard ruby libraries' ] ], + [ 'site-ruby', [ versite, + 'path', + 'the directory for non-standard ruby libraries' ] ], + [ 'bin-dir', [ '$prefix/bin', + 'path', + 'the directory for commands' ] ], + [ 'rb-dir', [ '$site-ruby', + 'path', + 'the directory for ruby scripts' ] ], + [ 'so-dir', [ sodir, + 'path', + 'the directory for ruby extentions' ] ], + [ 'data-dir', [ '$prefix/share', + 'path', + 'the directory for shared data' ] ], + [ 'ruby-path', [ rubypath, + 'path', + 'path to set to #! line' ] ], + [ 'ruby-prog', [ rubypath, + 'name', + 'the ruby program using for installation' ] ], + [ 'make-prog', [ makeprog, + 'name', + 'the make program to compile ruby extentions' ] ], + [ 'without-ext', [ 'no', + 'yes/no', + 'does not compile/install ruby extentions' ] ] + ] + multipackage_descripters = [ + [ 'with', [ '', + 'name,name...', + 'package names that you want to install', + 'ALL' ] ], + [ 'without', [ '', + 'name,name...', + 'package names that you do not want to install', + 'NONE' ] ] + ] + if multipackage_install? + DESCRIPTER = common_descripters + multipackage_descripters + else + DESCRIPTER = common_descripters + end + + SAVE_FILE = '.config' + + def ConfigTable.each_name(&block) + keys().each(&block) + end + + def ConfigTable.keys + DESCRIPTER.map {|name, *dummy| name } + end + + def ConfigTable.each_definition(&block) + DESCRIPTER.each(&block) + end + + def ConfigTable.get_entry(name) + name, ent = DESCRIPTER.assoc(name) + ent + end + + def ConfigTable.get_entry!(name) + get_entry(name) or raise ArgumentError, "no such config: #{name}" + end + + def ConfigTable.add_entry(name, vals) + ConfigTable::DESCRIPTER.push [name,vals] + end + + def ConfigTable.remove_entry(name) + get_entry(name) or raise ArgumentError, "no such config: #{name}" + DESCRIPTER.delete_if {|n, arr| n == name } + end + + def ConfigTable.config_key?(name) + get_entry(name) ? true : false + end + + def ConfigTable.bool_config?(name) + ent = get_entry(name) or return false + ent[1] == 'yes/no' + end + + def ConfigTable.value_config?(name) + ent = get_entry(name) or return false + ent[1] != 'yes/no' + end + + def ConfigTable.path_config?(name) + ent = get_entry(name) or return false + ent[1] == 'path' + end + + + class << self + alias newobj new + end + + def ConfigTable.new + c = newobj() + c.initialize_from_table + c + end + + def ConfigTable.load + c = newobj() + c.initialize_from_file + c + end + + def initialize_from_table + @table = {} + DESCRIPTER.each do |k, (default, vname, desc, default2)| + @table[k] = default + end + end + + def initialize_from_file + raise InstallError, "#{File.basename $0} config first"\ + unless File.file?(SAVE_FILE) + @table = {} + File.foreach(SAVE_FILE) do |line| + k, v = line.split(/=/, 2) + @table[k] = v.strip + end + end + + def save + File.open(SAVE_FILE, 'w') {|f| + @table.each do |k, v| + f.printf "%s=%s\n", k, v if v + end + } + end + + def []=(k, v) + raise InstallError, "unknown config option #{k}"\ + unless ConfigTable.config_key?(k) + @table[k] = v + end + + def [](key) + return nil unless @table[key] + @table[key].gsub(%r<\$([^/]+)>) { self[$1] } + end + + def set_raw(key, val) + @table[key] = val + end + + def get_raw(key) + @table[key] + end + +end + + +module MetaConfigAPI + + def eval_file_ifexist(fname) + instance_eval File.read(fname), fname, 1 if File.file?(fname) + end + + def config_names + ConfigTable.keys + end + + def config?(name) + ConfigTable.config_key?(name) + end + + def bool_config?(name) + ConfigTable.bool_config?(name) + end + + def value_config?(name) + ConfigTable.value_config?(name) + end + + def path_config?(name) + ConfigTable.path_config?(name) + end + + def add_config(name, argname, default, desc) + ConfigTable.add_entry name,[default,argname,desc] + end + + def add_path_config(name, default, desc) + add_config name, 'path', default, desc + end + + def add_bool_config(name, default, desc) + add_config name, 'yes/no', default ? 'yes' : 'no', desc + end + + def set_config_default(name, default) + if bool_config?(name) + ConfigTable.get_entry!(name)[0] = (default ? 'yes' : 'no') + else + ConfigTable.get_entry!(name)[0] = default + end + end + + def remove_config(name) + ent = ConfigTable.get_entry(name) + ConfigTable.remove_entry name + ent + end + +end + +# +# File Operations +# + +module FileOperations + + def mkdir_p(dirname, prefix = nil) + dirname = prefix + dirname if prefix + $stderr.puts "mkdir -p #{dirname}" if verbose? + return if no_harm? + + # does not check '/'... it's too abnormal case + dirs = dirname.split(%r<(?=/)>) + if /\A[a-z]:\z/i =~ dirs[0] + disk = dirs.shift + dirs[0] = disk + dirs[0] + end + dirs.each_index do |idx| + path = dirs[0..idx].join('') + Dir.mkdir path unless File.dir?(path) + end + end + + def rm_f(fname) + $stderr.puts "rm -f #{fname}" if verbose? + return if no_harm? + + if File.exist?(fname) or File.symlink?(fname) + File.chmod 0777, fname + File.unlink fname + end + end + + def rm_rf(dn) + $stderr.puts "rm -rf #{dn}" if verbose? + return if no_harm? + + Dir.chdir dn + Dir.foreach('.') do |fn| + next if fn == '.' + next if fn == '..' + if File.dir?(fn) + verbose_off { + rm_rf fn + } + else + verbose_off { + rm_f fn + } + end + end + Dir.chdir '..' + Dir.rmdir dn + end + + def move_file(src, dest) + File.unlink dest if File.exist?(dest) + begin + File.rename src, dest + rescue + File.open(dest, 'wb') {|f| f.write File.binread(src) } + File.chmod File.stat(src).mode, dest + File.unlink src + end + end + + def install(from, dest, mode, prefix = nil) + $stderr.puts "install #{from} #{dest}" if verbose? + return if no_harm? + + realdest = prefix ? prefix + dest : dest + realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest) + str = File.binread(from) + if diff?(str, realdest) + verbose_off { + rm_f realdest if File.exist?(realdest) + } + File.open(realdest, 'wb') {|f| + f.write str + } + File.chmod mode, realdest + + File.open("#{objdir_root()}/InstalledFiles", 'a') {|f| + if prefix + f.puts realdest.sub(prefix, '') + else + f.puts realdest + end + } + end + end + + def diff?(new_content, path) + return true unless File.exist?(path) + new_content != File.binread(path) + end + + def command(str) + $stderr.puts str if verbose? + system str or raise RuntimeError, "'system #{str}' failed" + end + + def ruby(str) + command config('ruby-prog') + ' ' + str + end + + def make(task = '') + command config('make-prog') + ' ' + task + end + + def extdir?(dir) + File.exist?(dir + '/MANIFEST') + end + + def all_files_in(dirname) + Dir.open(dirname) {|d| + return d.select {|ent| File.file?("#{dirname}/#{ent}") } + } + end + + REJECT_DIRS = %w( + CVS SCCS RCS CVS.adm .svn + ) + + def all_dirs_in(dirname) + Dir.open(dirname) {|d| + return d.select {|n| File.dir?("#{dirname}/#{n}") } - %w(. ..) - REJECT_DIRS + } + end + +end + +# +# Main Installer +# + +class InstallError < StandardError; end + + +module HookUtils + + def run_hook(name) + try_run_hook "#{curr_srcdir()}/#{name}" or + try_run_hook "#{curr_srcdir()}/#{name}.rb" + end + + def try_run_hook(fname) + return false unless File.file?(fname) + begin + instance_eval File.read(fname), fname, 1 + rescue + raise InstallError, "hook #{fname} failed:\n" + $!.message + end + true + end + +end + + +module HookScriptAPI + + def get_config(key) + @config[key] + end + + alias config get_config + + def set_config(key, val) + @config[key] = val + end + + # + # srcdir/objdir (works only in the package directory) + # + + #abstract srcdir_root + #abstract objdir_root + #abstract relpath + + def curr_srcdir + "#{srcdir_root()}/#{relpath()}" + end + + def curr_objdir + "#{objdir_root()}/#{relpath()}" + end + + def srcfile(path) + "#{curr_srcdir()}/#{path}" + end + + def srcexist?(path) + File.exist?(srcfile(path)) + end + + def srcdirectory?(path) + File.dir?(srcfile(path)) + end + + def srcfile?(path) + File.file? srcfile(path) + end + + def srcentries(path = '.') + Dir.open("#{curr_srcdir()}/#{path}") {|d| + return d.to_a - %w(. ..) + } + end + + def srcfiles(path = '.') + srcentries(path).select {|fname| + File.file?(File.join(curr_srcdir(), path, fname)) + } + end + + def srcdirectories(path = '.') + srcentries(path).select {|fname| + File.dir?(File.join(curr_srcdir(), path, fname)) + } + end + +end + + +class ToplevelInstaller + + Version = '3.3.0' + Copyright = 'Copyright (c) 2000-2004 Minero Aoki' + + TASKS = [ + [ 'all', 'do config, setup, then install' ], + [ 'config', 'saves your configurations' ], + [ 'show', 'shows current configuration' ], + [ 'setup', 'compiles ruby extentions and others' ], + [ 'install', 'installs files' ], + [ 'clean', "does `make clean' for each extention" ], + [ 'distclean',"does `make distclean' for each extention" ] + ] + + def ToplevelInstaller.invoke + instance().invoke + end + + @singleton = nil + + def ToplevelInstaller.instance + @singleton ||= new(File.dirname($0)) + @singleton + end + + include MetaConfigAPI + + def initialize(ardir_root) + @config = nil + @options = { 'verbose' => true } + @ardir = File.expand_path(ardir_root) + end + + def inspect + "#<#{self.class} #{__id__()}>" + end + + def invoke + run_metaconfigs + case task = parsearg_global() + when nil, 'all' + @config = load_config('config') + parsearg_config + init_installers + exec_config + exec_setup + exec_install + else + @config = load_config(task) + __send__ "parsearg_#{task}" + init_installers + __send__ "exec_#{task}" + end + end + + def run_metaconfigs + eval_file_ifexist "#{@ardir}/metaconfig" + end + + def load_config(task) + case task + when 'config' + ConfigTable.new + when 'clean', 'distclean' + if File.exist?(ConfigTable::SAVE_FILE) + then ConfigTable.load + else ConfigTable.new + end + else + ConfigTable.load + end + end + + def init_installers + @installer = Installer.new(@config, @options, @ardir, File.expand_path('.')) + end + + # + # Hook Script API bases + # + + def srcdir_root + @ardir + end + + def objdir_root + '.' + end + + def relpath + '.' + end + + # + # Option Parsing + # + + def parsearg_global + valid_task = /\A(?:#{TASKS.map {|task,desc| task }.join '|'})\z/ + + while arg = ARGV.shift + case arg + when /\A\w+\z/ + raise InstallError, "invalid task: #{arg}" unless valid_task =~ arg + return arg + + when '-q', '--quiet' + @options['verbose'] = false + + when '--verbose' + @options['verbose'] = true + + when '-h', '--help' + print_usage $stdout + exit 0 + + when '-v', '--version' + puts "#{File.basename($0)} version #{Version}" + exit 0 + + when '--copyright' + puts Copyright + exit 0 + + else + raise InstallError, "unknown global option '#{arg}'" + end + end + + nil + end + + + def parsearg_no_options + raise InstallError, "#{task}: unknown options: #{ARGV.join ' '}"\ + unless ARGV.empty? + end + + alias parsearg_show parsearg_no_options + alias parsearg_setup parsearg_no_options + alias parsearg_clean parsearg_no_options + alias parsearg_distclean parsearg_no_options + + def parsearg_config + re = /\A--(#{ConfigTable.keys.join '|'})(?:=(.*))?\z/ + @options['config-opt'] = [] + + while i = ARGV.shift + if /\A--?\z/ =~ i + @options['config-opt'] = ARGV.dup + break + end + m = re.match(i) or raise InstallError, "config: unknown option #{i}" + name, value = m.to_a[1,2] + if value + if ConfigTable.bool_config?(name) + raise InstallError, "config: --#{name} allows only yes/no for argument"\ + unless /\A(y(es)?|n(o)?|t(rue)?|f(alse))\z/i =~ value + value = (/\Ay(es)?|\At(rue)/i =~ value) ? 'yes' : 'no' + end + else + raise InstallError, "config: --#{name} requires argument"\ + unless ConfigTable.bool_config?(name) + value = 'yes' + end + @config[name] = value + end + end + + def parsearg_install + @options['no-harm'] = false + @options['install-prefix'] = '' + while a = ARGV.shift + case a + when /\A--no-harm\z/ + @options['no-harm'] = true + when /\A--prefix=(.*)\z/ + path = $1 + path = File.expand_path(path) unless path[0,1] == '/' + @options['install-prefix'] = path + else + raise InstallError, "install: unknown option #{a}" + end + end + end + + def print_usage(out) + out.puts 'Typical Installation Procedure:' + out.puts " $ ruby #{File.basename $0} config" + out.puts " $ ruby #{File.basename $0} setup" + out.puts " # ruby #{File.basename $0} install (may require root privilege)" + out.puts + out.puts 'Detailed Usage:' + out.puts " ruby #{File.basename $0} <global option>" + out.puts " ruby #{File.basename $0} [<global options>] <task> [<task options>]" + + fmt = " %-20s %s\n" + out.puts + out.puts 'Global options:' + out.printf fmt, '-q,--quiet', 'suppress message outputs' + out.printf fmt, ' --verbose', 'output messages verbosely' + out.printf fmt, '-h,--help', 'print this message' + out.printf fmt, '-v,--version', 'print version and quit' + out.printf fmt, ' --copyright', 'print copyright and quit' + + out.puts + out.puts 'Tasks:' + TASKS.each do |name, desc| + out.printf " %-10s %s\n", name, desc + end + + out.puts + out.puts 'Options for CONFIG or ALL:' + ConfigTable.each_definition do |name, (default, arg, desc, default2)| + out.printf " %-20s %s [%s]\n", + '--'+ name + (ConfigTable.bool_config?(name) ? '' : '='+arg), + desc, + default2 || default + end + out.printf " %-20s %s [%s]\n", + '--rbconfig=path', 'your rbconfig.rb to load', "running ruby's" + + out.puts + out.puts 'Options for INSTALL:' + out.printf " %-20s %s [%s]\n", + '--no-harm', 'only display what to do if given', 'off' + out.printf " %-20s %s [%s]\n", + '--prefix', 'install path prefix', '$prefix' + + out.puts + end + + # + # Task Handlers + # + + def exec_config + @installer.exec_config + @config.save # must be final + end + + def exec_setup + @installer.exec_setup + end + + def exec_install + @installer.exec_install + end + + def exec_show + ConfigTable.each_name do |k| + v = @config.get_raw(k) + if not v or v.empty? + v = '(not specified)' + end + printf "%-10s %s\n", k, v + end + end + + def exec_clean + @installer.exec_clean + end + + def exec_distclean + @installer.exec_distclean + end + +end + + +class ToplevelInstallerMulti < ToplevelInstaller + + include HookUtils + include HookScriptAPI + include FileOperations + + def initialize(ardir) + super + @packages = all_dirs_in("#{@ardir}/packages") + raise 'no package exists' if @packages.empty? + end + + def run_metaconfigs + eval_file_ifexist "#{@ardir}/metaconfig" + @packages.each do |name| + eval_file_ifexist "#{@ardir}/packages/#{name}/metaconfig" + end + end + + def init_installers + @installers = {} + @packages.each do |pack| + @installers[pack] = Installer.new(@config, @options, + "#{@ardir}/packages/#{pack}", + "packages/#{pack}") + end + + with = extract_selection(config('with')) + without = extract_selection(config('without')) + @selected = @installers.keys.select {|name| + (with.empty? or with.include?(name)) \ + and not without.include?(name) + } + end + + def extract_selection(list) + a = list.split(/,/) + a.each do |name| + raise InstallError, "no such package: #{name}" \ + unless @installers.key?(name) + end + a + end + + def print_usage(f) + super + f.puts 'Inluded packages:' + f.puts ' ' + @packages.sort.join(' ') + f.puts + end + + # + # multi-package metaconfig API + # + + attr_reader :packages + + def declare_packages(list) + raise 'package list is empty' if list.empty? + list.each do |name| + raise "directory packages/#{name} does not exist"\ + unless File.dir?("#{@ardir}/packages/#{name}") + end + @packages = list + end + + # + # Task Handlers + # + + def exec_config + run_hook 'pre-config' + each_selected_installers {|inst| inst.exec_config } + run_hook 'post-config' + @config.save # must be final + end + + def exec_setup + run_hook 'pre-setup' + each_selected_installers {|inst| inst.exec_setup } + run_hook 'post-setup' + end + + def exec_install + run_hook 'pre-install' + each_selected_installers {|inst| inst.exec_install } + run_hook 'post-install' + end + + def exec_clean + rm_f ConfigTable::SAVE_FILE + run_hook 'pre-clean' + each_selected_installers {|inst| inst.exec_clean } + run_hook 'post-clean' + end + + def exec_distclean + rm_f ConfigTable::SAVE_FILE + run_hook 'pre-distclean' + each_selected_installers {|inst| inst.exec_distclean } + run_hook 'post-distclean' + end + + # + # lib + # + + def each_selected_installers + Dir.mkdir 'packages' unless File.dir?('packages') + @selected.each do |pack| + $stderr.puts "Processing the package `#{pack}' ..." if @options['verbose'] + Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}") + Dir.chdir "packages/#{pack}" + yield @installers[pack] + Dir.chdir '../..' + end + end + + def verbose? + @options['verbose'] + end + + def no_harm? + @options['no-harm'] + end + +end + + +class Installer + + FILETYPES = %w( bin lib ext data ) + + include HookScriptAPI + include HookUtils + include FileOperations + + def initialize(config, opt, srcroot, objroot) + @config = config + @options = opt + @srcdir = File.expand_path(srcroot) + @objdir = File.expand_path(objroot) + @currdir = '.' + end + + def inspect + "#<#{self.class} #{File.basename(@srcdir)}>" + end + + # + # Hook Script API bases + # + + def srcdir_root + @srcdir + end + + def objdir_root + @objdir + end + + def relpath + @currdir + end + + # + # configs/options + # + + def no_harm? + @options['no-harm'] + end + + def verbose? + @options['verbose'] + end + + def verbose_off + begin + save, @options['verbose'] = @options['verbose'], false + yield + ensure + @options['verbose'] = save + end + end + + # + # TASK config + # + + def exec_config + exec_task_traverse 'config' + end + + def config_dir_bin(rel) + end + + def config_dir_lib(rel) + end + + def config_dir_ext(rel) + extconf if extdir?(curr_srcdir()) + end + + def extconf + opt = @options['config-opt'].join(' ') + command "#{config('ruby-prog')} #{curr_srcdir()}/extconf.rb #{opt}" + end + + def config_dir_data(rel) + end + + # + # TASK setup + # + + def exec_setup + exec_task_traverse 'setup' + end + + def setup_dir_bin(rel) + all_files_in(curr_srcdir()).each do |fname| + adjust_shebang "#{curr_srcdir()}/#{fname}" + end + end + + def adjust_shebang(path) + return if no_harm? + tmpfile = File.basename(path) + '.tmp' + begin + File.open(path, 'rb') {|r| + File.open(tmpfile, 'wb') {|w| + first = r.gets + return unless should_modify_shebang?(first) + $stderr.puts "adjusting shebang: #{File.basename(path)}" if verbose? + w.print first.sub(SHEBANG_RE, '#!' + config('ruby-path')) + w.write r.read + } + } + move_file tmpfile, File.basename(path) + ensure + File.unlink tmpfile if File.exist?(tmpfile) + end + end + + def should_modify_shebang?(line) + File.basename(config('ruby-path')) == 'ruby' or + shebang_command(line) == 'ruby' + end + + def shebang_command(line) + cmd, arg = *line.sub(/\A\#!/, '').strip.split(/\s+/, 2) + cmd + end + + def setup_dir_lib(rel) + end + + def setup_dir_ext(rel) + make if extdir?(curr_srcdir()) + end + + def setup_dir_data(rel) + end + + # + # TASK install + # + + def exec_install + exec_task_traverse 'install' + end + + def install_dir_bin(rel) + install_files collect_filenames_auto(), "#{config('bin-dir')}/#{rel}", 0755 + end + + def install_dir_lib(rel) + install_files ruby_scripts(), "#{config('rb-dir')}/#{rel}", 0644 + end + + def install_dir_ext(rel) + return unless extdir?(curr_srcdir()) + install_files ruby_extentions('.'), + "#{config('so-dir')}/#{File.dirname(rel)}", + 0555 + end + + def install_dir_data(rel) + install_files collect_filenames_auto(), "#{config('data-dir')}/#{rel}", 0644 + end + + def install_files(list, dest, mode) + mkdir_p dest, @options['install-prefix'] + list.each do |fname| + install fname, dest, mode, @options['install-prefix'] + end + end + + def ruby_scripts + collect_filenames_auto().select {|n| /\.rb\z/ =~ n } + end + + # picked up many entries from cvs-1.11.1/src/ignore.c + reject_patterns = %w( + core RCSLOG tags TAGS .make.state + .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb + *~ *.old *.bak *.BAK *.orig *.rej _$* *$ + + *.org *.in .* + ) + mapping = { + '.' => '\.', + '$' => '\$', + '#' => '\#', + '*' => '.*' + } + REJECT_PATTERNS = Regexp.new('\A(?:' + + reject_patterns.map {|pat| + pat.gsub(/[\.\$\#\*]/) {|ch| mapping[ch] } + }.join('|') + + ')\z') + + def collect_filenames_auto + mapdir((existfiles() - hookfiles()).reject {|fname| + REJECT_PATTERNS =~ fname + }) + end + + def existfiles + all_files_in(curr_srcdir()) | all_files_in('.') + end + + def hookfiles + %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt| + %w( config setup install clean ).map {|t| sprintf(fmt, t) } + }.flatten + end + + def mapdir(filelist) + filelist.map {|fname| + if File.exist?(fname) # objdir + fname + else # srcdir + File.join(curr_srcdir(), fname) + end + } + end + + def ruby_extentions(dir) + _ruby_extentions(dir) or + raise InstallError, "no ruby extention exists: 'ruby #{$0} setup' first" + end + + DLEXT = /\.#{ ::Config::CONFIG['DLEXT'] }\z/ + + def _ruby_extentions(dir) + Dir.open(dir) {|d| + return d.select {|fname| DLEXT =~ fname } + } + end + + # + # TASK clean + # + + def exec_clean + exec_task_traverse 'clean' + rm_f ConfigTable::SAVE_FILE + rm_f 'InstalledFiles' + end + + def clean_dir_bin(rel) + end + + def clean_dir_lib(rel) + end + + def clean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'clean' if File.file?('Makefile') + end + + def clean_dir_data(rel) + end + + # + # TASK distclean + # + + def exec_distclean + exec_task_traverse 'distclean' + rm_f ConfigTable::SAVE_FILE + rm_f 'InstalledFiles' + end + + def distclean_dir_bin(rel) + end + + def distclean_dir_lib(rel) + end + + def distclean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'distclean' if File.file?('Makefile') + end + + # + # lib + # + + def exec_task_traverse(task) + run_hook "pre-#{task}" + FILETYPES.each do |type| + if config('without-ext') == 'yes' and type == 'ext' + $stderr.puts 'skipping ext/* by user option' if verbose? + next + end + traverse task, type, "#{task}_dir_#{type}" + end + run_hook "post-#{task}" + end + + def traverse(task, rel, mid) + dive_into(rel) { + run_hook "pre-#{task}" + __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '') + all_dirs_in(curr_srcdir()).each do |d| + traverse task, "#{rel}/#{d}", mid + end + run_hook "post-#{task}" + } + end + + def dive_into(rel) + return unless File.dir?("#{@srcdir}/#{rel}") + + dir = File.basename(rel) + Dir.mkdir dir unless File.dir?(dir) + prevdir = Dir.pwd + Dir.chdir dir + $stderr.puts '---> ' + rel if verbose? + @currdir = rel + yield + Dir.chdir prevdir + $stderr.puts '<--- ' + rel if verbose? + @currdir = File.dirname(rel) + end + +end + + +if $0 == __FILE__ + begin + if multipackage_install? + ToplevelInstallerMulti.invoke + else + ToplevelInstaller.invoke + end + rescue + raise if $DEBUG + $stderr.puts $!.message + $stderr.puts "Try 'ruby #{$0} --help' for detailed usage." + exit 1 + end +end diff --git a/sqlite3-ruby-win32.gemspec b/sqlite3-ruby-win32.gemspec new file mode 100644 index 0000000..6aa7b1c --- /dev/null +++ b/sqlite3-ruby-win32.gemspec @@ -0,0 +1,28 @@ +require "./lib/sqlite3/version" + +Gem::Specification.new do |s| + + s.name = 'sqlite3-ruby' + s.version = SQLite3::Version::STRING + s.platform = Gem::Platform::WIN32 + s.required_ruby_version = ">=1.8.0" + + s.summary = "SQLite3/Ruby is a module to allow Ruby scripts to interface with a SQLite database." + + s.files = Dir.glob("{doc,ext,lib,test}/**/*").delete_if { |item| item.include?( "CVS" ) } + s.files.concat [ "LICENSE", "README", "ChangeLog" ] + + s.require_path = 'lib' + s.autorequire = 'sqlite3' + + s.has_rdoc = true + s.extra_rdoc_files = [ "README" ] + s.rdoc_options = [ "--main", "README" ] + + s.test_suite_file = "test/tests.rb" + + s.author = "Jamis Buck" + s.email = "jgb3@email.byu.edu" + s.homepage = "http://sqlite-ruby.rubyforge.org/sqlite3" + +end diff --git a/sqlite3-ruby.gemspec b/sqlite3-ruby.gemspec new file mode 100644 index 0000000..d866f0f --- /dev/null +++ b/sqlite3-ruby.gemspec @@ -0,0 +1,30 @@ +require "./lib/sqlite3/version" + +Gem::Specification.new do |s| + + s.name = 'sqlite3-ruby' + s.version = SQLite3::Version::STRING + s.platform = Gem::Platform::RUBY + s.required_ruby_version = ">=1.8.0" + + s.summary = "SQLite3/Ruby is a module to allow Ruby scripts to interface with a SQLite3 database." + + s.files = Dir.glob("{doc,ext,lib,test}/**/*").delete_if { |item| item.include?( "CVS" ) } + s.files.concat [ "LICENSE", "README", "ChangeLog" ] + + s.require_path = 'lib' + s.autorequire = 'sqlite3' + + s.extensions << 'ext/sqlite3_api/extconf.rb' + + s.has_rdoc = true + s.extra_rdoc_files = [ "README" ] + s.rdoc_options = [ "--main", "README" ] + + s.test_suite_file = "test/tests.rb" + + s.author = "Jamis Buck" + s.email = "jgb3@email.byu.edu" + s.homepage = "http://sqlite-ruby.rubyforge.org/sqlite3" + +end diff --git a/test/bm.rb b/test/bm.rb new file mode 100644 index 0000000..aacb4a1 --- /dev/null +++ b/test/bm.rb @@ -0,0 +1,140 @@ +require 'benchmark' + +N = 1000 + +$VERBOSE=nil + +puts "file require" +Benchmark.bm( 7 ) do |x| + x.report('sqlite') do + N.times do + $".delete_if { |i| i =~ /sqlite/ } + require 'sqlite' + end + end + x.report('sqlite3') do + N.times do + $".delete_if { |i| i =~ /sqlite3/ } + require 'sqlite3' + end + end +end + +puts +puts "database creation..." +Benchmark.bm( 7 ) do |x| + x.report('sqlite') do + N.times do + File.delete "test.db" rescue nil + SQLite::Database.open( "test.db" ).close + end + end + x.report('sqlite3') do + N.times do + File.delete "test.db" rescue nil + SQLite3::Database.open( "test.db" ).close + end + end +end +File.delete "test.db" rescue nil + +SQLite::Database.open( "test.db" ).close +SQLite3::Database.open( "test3.db" ).close + +puts +puts "database open..." +Benchmark.bm( 7 ) do |x| + x.report('sqlite') do + N.times do + SQLite::Database.open( "test.db" ).close + end + end + x.report('sqlite3') do + N.times do + SQLite3::Database.open( "test3.db" ).close + end + end +end +File.delete "test.db" rescue nil +File.delete "test3.db" rescue nil + +db = SQLite::Database.open( "test.db" ) +db3 = SQLite3::Database.open( "test3.db" ) + +db.execute "create table foo (a,b)" +db3.execute "create table foo (a,b)" + +puts +puts "insertions" +Benchmark.bm( 7 ) do |x| + x.report('sqlite') do + db.transaction do + N.times do |i| + db.execute "insert into foo values (#{i}, #{i+1})" + end + end + end + x.report('sqlite3') do + db3.transaction do + N.times do |i| + db3.execute "insert into foo values (#{i}, #{i+1})" + end + end + end +end + +puts +puts "insertions using prepared statement" +Benchmark.bm( 7 ) do |x| + x.report('sqlite') do + db.transaction do + stmt = db.prepare "insert into foo values (?,?)" + N.times { |i| stmt.execute i, i+1 } + end + end + x.report('sqlite3') do + db3.transaction do + db3.prepare( "insert into foo values (?,?)" ) do |stmt| + N.times { |i| stmt.execute i, i+1 } + end + end + end +end + +db.close +db3.close +File.delete "test.db" rescue nil +File.delete "test3.db" rescue nil + +db = SQLite::Database.open( "test.db" ) +db3 = SQLite3::Database.open( "test3.db" ) + +db.execute "create table foo (a,b)" +db.execute "insert into foo values (1,2)" +db.execute "insert into foo values (3,4)" +db.execute "insert into foo values (5,6)" + +db3.execute "create table foo (a,b)" +db3.execute "insert into foo values (1,2)" +db3.execute "insert into foo values (3,4)" +db3.execute "insert into foo values (5,6)" + +puts +puts "queries" +Benchmark.bm( 7 ) do |x| + x.report('sqlite') do + N.times do + db.execute "select * from foo" + end + end + x.report('sqlite3') do + N.times do + db3.execute "select * from foo" + end + end +end + +db.close +db3.close +File.delete "test.db" rescue nil +File.delete "test3.db" rescue nil diff --git a/test/driver/dl/tc_driver.rb b/test/driver/dl/tc_driver.rb new file mode 100644 index 0000000..3dc59e3 --- /dev/null +++ b/test/driver/dl/tc_driver.rb @@ -0,0 +1,322 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +$:.unshift "../../../lib" + +require 'sqlite3/constants' +require 'sqlite3/driver/dl/driver' +require 'test/unit' + +class TC_DL_Driver < Test::Unit::TestCase + + def utf16ify( str ) + chars = str.split(//) + chars.zip(["\0"] * chars.length).flatten.join + end + + def setup + @driver = SQLite3::Driver::DL::Driver.new + @dbname = "test.db" + @db = nil + end + + def teardown + @driver.close( @db ) rescue nil + File.delete @dbname rescue nil + end + + def test_open + result, @db = @driver.open( @dbname ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + assert File.exist?( @dbname ) + end + + def test_open_utf16 + name = utf16ify( @dbname ) + result, @db = @driver.open( name, true ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + assert File.exist?( @dbname ) + end + + def test_errmsg + result, @db = @driver.open( @dbname ) + msg = @driver.errmsg( @db ) + assert_equal msg, "not an error" + end + + def test_errmsg16 + result, @db = @driver.open( @dbname ) + msg = @driver.errmsg( @db, true ) + assert_equal msg, utf16ify( "not an error" ) + end + + def test_prepare + result, @db = @driver.open( @dbname ) + sql = "create table foo ( a, b )" + result, handle, remainder = @driver.prepare( @db, sql ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + assert_equal "", remainder + @driver.finalize( handle ) + end + + def test_prepare_error + result, @db = @driver.open( @dbname ) + sql = "create tble foo ( a, b )" + result, handle, remainder = @driver.prepare( @db, sql ) + assert_equal SQLite3::Constants::ErrorCode::ERROR, result + end + + def test_prepare_remainder + result, @db = @driver.open( @dbname ) + sql = "create table foo ( a, b ); select * from foo" + result, handle, remainder = @driver.prepare( @db, sql ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + assert_equal " select * from foo", remainder + @driver.finalize( handle ) + end + + def test_prepare16 + result, @db = @driver.open( @dbname ) + sql = utf16ify( "create table foo ( a, b )" ) + result, handle, remainder = @driver.prepare( @db, sql, true ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + assert_equal "", remainder + @driver.finalize( handle ) + end + + def test_prepare16_remainder + result, @db = @driver.open( @dbname ) + sql = utf16ify( "create table foo ( a, b ); select * from foo" ) + result, handle, remainder = @driver.prepare( @db, sql, true ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + assert_equal utf16ify( " select * from foo" ), remainder + @driver.finalize( handle ) + end + + def test_complete + assert @driver.complete?( "select * from foo;" ) + end + + def test_complete_fail + assert !@driver.complete?( "select * from foo" ) + end + + def test_complete16 + assert @driver.complete?( utf16ify("select * from foo;"), true ) + end + + def create_foo + result, @db = @driver.open( @dbname ) + sql = "create table foo ( a, b )" + result, handle, = @driver.prepare( @db, sql ) + @driver.step( handle ) + @driver.finalize( handle ) + end + + def populate_foo + create_foo + sql = "insert into foo values ( 100, 200 )" + result, handle, = @driver.prepare( @db, sql ) + @driver.step( handle ) + @driver.finalize( handle ) + end + + def test_step + populate_foo + sql = "select * from foo" + result, handle, = @driver.prepare( @db, sql ) + result = @driver.step( handle ) + assert_equal SQLite3::Constants::ErrorCode::ROW, result + result = @driver.step( handle ) + assert_equal SQLite3::Constants::ErrorCode::DONE, result + @driver.finalize( handle ) + end + + def test_step_fail + populate_foo + sql = "select * from" + result, handle, = @driver.prepare( @db, sql ) + result = @driver.step( handle ) + assert_equal SQLite3::Constants::ErrorCode::MISUSE, result + @driver.finalize( handle ) + end + + def test_bind_blob + create_foo + sql = "insert into foo (b) values (?)" + result, handle, = @driver.prepare( @db, sql ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + result = @driver.bind_blob( handle, 1, "a\0b\1c\2d\0e" ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + result = @driver.step( handle ) + assert_equal SQLite3::Constants::ErrorCode::DONE, result + result = @driver.finalize( handle ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + sql = "select b from foo" + result, handle, = @driver.prepare( @db, sql ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + result = @driver.step( handle ) + assert_equal SQLite3::Constants::ErrorCode::ROW, result + assert_equal "a\0b\1c\2d\0e", @driver.column_blob( handle, 0 ) + result = @driver.finalize( handle ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + end + + def test_bind_double + create_foo + sql = "insert into foo (b) values (?)" + result, handle, = @driver.prepare( @db, sql ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + result = @driver.bind_double( handle, 1, 3.14 ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + result = @driver.step( handle ) + assert_equal SQLite3::Constants::ErrorCode::DONE, result + result = @driver.finalize( handle ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + sql = "select b from foo" + result, handle, = @driver.prepare( @db, sql ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + result = @driver.step( handle ) + assert_equal SQLite3::Constants::ErrorCode::ROW, result + assert_equal 3.14, @driver.column_double( handle, 0 ) + result = @driver.finalize( handle ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + end + + def test_bind_int + create_foo + sql = "insert into foo (b) values (?)" + result, handle, = @driver.prepare( @db, sql ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + result = @driver.bind_int( handle, 1, 14 ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + result = @driver.step( handle ) + assert_equal SQLite3::Constants::ErrorCode::DONE, result + result = @driver.finalize( handle ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + sql = "select b from foo" + result, handle, = @driver.prepare( @db, sql ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + result = @driver.step( handle ) + assert_equal SQLite3::Constants::ErrorCode::ROW, result + assert_equal 14, @driver.column_int( handle, 0 ) + result = @driver.finalize( handle ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + end + + def test_bind_null + create_foo + sql = "insert into foo (b) values (?)" + result, handle, = @driver.prepare( @db, sql ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + result = @driver.bind_null( handle, 1 ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + result = @driver.step( handle ) + assert_equal SQLite3::Constants::ErrorCode::DONE, result + result = @driver.finalize( handle ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + sql = "select b from foo" + result, handle, = @driver.prepare( @db, sql ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + result = @driver.step( handle ) + assert_equal SQLite3::Constants::ErrorCode::ROW, result + assert_equal SQLite3::Constants::ColumnType::NULL, + @driver.column_type( handle, 0 ) + result = @driver.finalize( handle ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + end + + def test_bind_text + create_foo + sql = "insert into foo (b) values (?)" + result, handle, = @driver.prepare( @db, sql ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + result = @driver.bind_text( handle, 1, "hello, world" ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + result = @driver.step( handle ) + assert_equal SQLite3::Constants::ErrorCode::DONE, result + result = @driver.finalize( handle ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + sql = "select b from foo" + result, handle, = @driver.prepare( @db, sql ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + result = @driver.step( handle ) + assert_equal SQLite3::Constants::ErrorCode::ROW, result + assert_equal "hello, world", @driver.column_text( handle, 0 ) + result = @driver.finalize( handle ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + end + + def test_bind_text16 + create_foo + sql = "insert into foo (b) values (?)" + result, handle, = @driver.prepare( @db, sql ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + result = @driver.bind_text( handle, 1, utf16ify("hello, world"), true ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + result = @driver.step( handle ) + assert_equal SQLite3::Constants::ErrorCode::DONE, result + result = @driver.finalize( handle ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + sql = "select b from foo" + result, handle, = @driver.prepare( @db, sql ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + result = @driver.step( handle ) + assert_equal SQLite3::Constants::ErrorCode::ROW, result + assert_equal "hello, world", @driver.column_text( handle, 0 ) + result = @driver.finalize( handle ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + end + + def test_bind_parameter_index + create_foo + sql = "insert into foo (b) values (:hello)" + result, handle, = @driver.prepare( @db, sql ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + result = @driver.bind_parameter_index( handle, ":hello" ) + assert_equal 1, result + result = @driver.bind_parameter_index( handle, ":foo" ) + assert_equal 0, result + @driver.finalize( handle ) + end + + def test_bind_parameter_name + create_foo + sql = "insert into foo (a,b) values (?,:foo)" + result, handle, = @driver.prepare( @db, sql ) + assert_equal SQLite3::Constants::ErrorCode::OK, result + assert_nil nil, @driver.bind_parameter_name(handle,1) + assert_equal ":foo", @driver.bind_parameter_name(handle,2) + @driver.finalize( handle ) + end + +end diff --git a/test/mocks.rb b/test/mocks.rb new file mode 100644 index 0000000..6843e52 --- /dev/null +++ b/test/mocks.rb @@ -0,0 +1,99 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +require 'flexmock' + +class FlexMockWithArgs < FlexMock + attr_reader :mock_args + attr_reader :mock_blocks + + def initialize + super + @mock_args = Hash.new { |h,k| h[k] = [] } + @mock_blocks = Hash.new { |h,k| h[k] = [] } + end + + def method_missing( sym, *args, &block ) + @mock_args[sym] << args + @mock_blocks[sym] << block + super + end +end + +class Driver < FlexMockWithArgs + def self.instance + @@instance + end + + def initialize + super + @@instance = self + mock_handle( :open ) { [0,"cookie"] } + mock_handle( :close ) { 0 } + mock_handle( :complete? ) { 0 } + mock_handle( :errmsg ) { "" } + mock_handle( :errcode ) { 0 } + mock_handle( :trace ) { nil } + mock_handle( :set_authorizer ) { 0 } + mock_handle( :prepare ) { [0,"stmt", "remainder"] } + mock_handle( :finalize ) { 0 } + mock_handle( :changes ) { 14 } + mock_handle( :total_changes ) { 28 } + mock_handle( :interrupt ) { 0 } + end +end + +class Statement < FlexMockWithArgs + def self.instance + @@instance + end + + attr_reader :handle + attr_reader :sql + attr_reader :last_result + + def initialize( handle, sql ) + super() + @@instance = self + @handle = handle + @sql = sql + mock_handle( :close ) { 0 } + mock_handle( :remainder ) { "" } + mock_handle( :execute ) do + @last_result = FlexMockWithArgs.new + @last_result.mock_handle( :each ) { @last_result.mock_blocks[:each].last.call ["foo"] } + @last_result.mock_handle( :inject ) { |a,| @last_result.mock_blocks[:inject].last.call a, ["foo"] } + @last_result.mock_handle( :columns ) { ["name"] } + @last_result + end + end +end diff --git a/test/native-vs-dl.rb b/test/native-vs-dl.rb new file mode 100644 index 0000000..45e7e28 --- /dev/null +++ b/test/native-vs-dl.rb @@ -0,0 +1,126 @@ +$:.unshift "../lib", "../ext/sqlite3_api" + +require 'sqlite3' + +require 'benchmark' + +N = 1000 + +$VERBOSE=nil + +puts "database creation..." +Benchmark.bm( 7 ) do |x| + x.report('dl') do + N.times do + File.delete "test.db" rescue nil + SQLite3::Database.open( "test.db", :driver => "DL" ).close + end + end + x.report('native') do + N.times do + File.delete "test.db" rescue nil + SQLite3::Database.open( "test.db", :driver => "Native" ).close + end + end +end +File.delete "test.db" rescue nil + +SQLite3::Database.open( "test.db" ).close + +puts +puts "database open..." +Benchmark.bm( 7 ) do |x| + x.report('dl') do + N.times do + SQLite3::Database.open( "test.db", :driver => "DL" ).close + end + end + x.report('native') do + N.times do + SQLite3::Database.open( "test.db", :driver => "Native" ).close + end + end +end +File.delete "test.db" rescue nil + +dl = SQLite3::Database.open( "test-dl.db", :driver => "DL" ) +native = SQLite3::Database.open( "test-native.db", :driver => "Native" ) + +dl.execute "create table foo (a,b)" +native.execute "create table foo (a,b)" + +puts +puts "insertions" +Benchmark.bm( 7 ) do |x| + x.report('dl') do + dl.transaction do + N.times do |i| + dl.execute "insert into foo values (#{i}, #{i+1})" + end + end + end + x.report('native') do + native.transaction do + N.times do |i| + native.execute "insert into foo values (#{i}, #{i+1})" + end + end + end +end + +puts +puts "insertions using prepared statement" +Benchmark.bm( 7 ) do |x| + x.report('dl') do + dl.transaction do + dl.prepare "insert into foo values (?,?)" do |stmt| + N.times { |i| stmt.execute i, i+1 } + end + end + end + x.report('native') do + native.transaction do + native.prepare( "insert into foo values (?,?)" ) do |stmt| + N.times { |i| stmt.execute i, i+1 } + end + end + end +end + +dl.close +native.close +File.delete "test-dl.db" rescue nil +File.delete "test-native.db" rescue nil + +dl = SQLite3::Database.open( "test-dl.db", :driver => "DL" ) +native = SQLite3::Database.open( "test-native.db", :driver => "Native" ) + +dl.execute "create table foo (a,b)" +dl.execute "insert into foo values (1,2)" +dl.execute "insert into foo values (3,4)" +dl.execute "insert into foo values (5,6)" + +native.execute "create table foo (a,b)" +native.execute "insert into foo values (1,2)" +native.execute "insert into foo values (3,4)" +native.execute "insert into foo values (5,6)" + +puts +puts "queries" +Benchmark.bm( 7 ) do |x| + x.report('dl') do + N.times do + dl.execute "select * from foo" + end + end + x.report('native') do + N.times do + native.execute "select * from foo" + end + end +end + +dl.close +native.close +File.delete "test-dl.db" rescue nil +File.delete "test-native.db" rescue nil diff --git a/test/tc_database.rb b/test/tc_database.rb new file mode 100644 index 0000000..1aa2400 --- /dev/null +++ b/test/tc_database.rb @@ -0,0 +1,216 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + +$:.unshift "../lib" + +require 'sqlite3/database' +require 'test/unit' + +require 'mocks' + +class TC_Database_Init < Test::Unit::TestCase + def test_new + db = SQLite3::Database.new( "foo.db", :driver => Driver ) + assert_equal 1, Driver.instance.mock_count(:open) + assert !db.closed? + assert_equal [["foo.db",false]], Driver.instance.mock_args[:open] + assert !db.results_as_hash + assert !db.type_translation + end + + def test_open + db = SQLite3::Database.open( "foo.db", :driver => Driver ) + assert_equal 1, Driver.instance.mock_count(:open) + assert !db.closed? + assert_equal [["foo.db",false]], Driver.instance.mock_args[:open] + assert !db.results_as_hash + assert !db.type_translation + end + + def test_with_type_translation + db = SQLite3::Database.open( "foo.db", :driver => Driver, + :type_translation => true ) + assert db.type_translation + end + + def test_with_results_as_hash + db = SQLite3::Database.open( "foo.db", :driver => Driver, + :results_as_hash => true ) + assert db.results_as_hash + end + + def test_with_type_translation_and_results_as_hash + db = SQLite3::Database.open( "foo.db", :driver => Driver, + :results_as_hash => true, + :type_translation => true ) + assert db.results_as_hash + assert db.type_translation + end +end + +class TC_Database < Test::Unit::TestCase + def setup + @db = SQLite3::Database.open( "foo.db", + :driver => Driver, :statement_factory => Statement ) + end + + def test_quote + assert_equal "''one''two''three''", SQLite3::Database.quote( + "'one'two'three'" ) + end + + def test_complete + @db.complete? "foo" + assert_equal 1, Driver.instance.mock_count( :complete? ) + end + + def test_errmsg + @db.errmsg + assert_equal 1, Driver.instance.mock_count( :errmsg ) + end + + def test_errcode + @db.errcode + assert_equal 1, Driver.instance.mock_count( :errcode ) + end + + def test_translator + translator = @db.translator + assert_instance_of SQLite3::Translator, translator + end + + def test_close + @db.close + assert_equal 1, Driver.instance.mock_count( :close ) + assert @db.closed? + @db.close + assert_equal 1, Driver.instance.mock_count( :close ) + end + + def test_trace + @db.trace( 15 ) { "foo" } + driver = Driver.instance + assert_equal 1, driver.mock_count( :trace ) + assert_equal [[ "cookie", 15 ]], driver.mock_args[:trace] + assert_equal 1, driver.mock_blocks[:trace].length + end + + def test_authorizer + @db.authorizer( 15 ) { "foo" } + driver = Driver.instance + assert_equal 1, driver.mock_count( :set_authorizer ) + assert_equal [[ "cookie", 15 ]], driver.mock_args[:set_authorizer] + assert_equal 1, driver.mock_blocks[:set_authorizer].length + end + + def test_prepare_no_block + assert_nothing_raised { @db.prepare( "foo" ) } + assert_equal 0, Statement.instance.mock_count( :close ) + end + + def test_prepare_with_block + called = false + @db.prepare( "foo" ) { |stmt| called = true } + assert called + assert_equal 1, Statement.instance.mock_count( :close ) + end + + def test_execute_no_block + result = @db.execute( "foo", "bar", "baz" ) + stmt = Statement.instance + assert_equal [["foo"]], result + assert_equal [["bar", "baz"]], stmt.mock_args[:execute] + end + + def test_execute_with_block + called = false + @db.execute( "foo", "bar", "baz" ) do |row| + called = true + assert_equal ["foo"], row + end + + stmt = Statement.instance + assert called + assert_equal [["bar", "baz"]], stmt.mock_args[:execute] + end + + def test_execute2_no_block + result = @db.execute2( "foo", "bar", "baz" ) + stmt = Statement.instance + assert_equal [["name"],["foo"]], result + assert_equal [["bar", "baz"]], stmt.mock_args[:execute] + end + + def test_execute2_with_block + called = false + parts = [ ["name"],["foo"] ] + @db.execute2( "foo", "bar", "baz" ) do |row| + called = true + assert_equal parts.shift, row + end + + stmt = Statement.instance + assert called + assert_equal [["bar", "baz"]], stmt.mock_args[:execute] + end + + def test_execute_batch + @db.execute_batch( "foo", "bar", "baz" ) + stmt = Statement.instance + assert_equal [["bar", "baz"]], stmt.mock_args[:execute] + end + + def test_get_first_row + result = @db.get_first_row( "foo", "bar", "baz" ) + assert_equal ["foo"], result + end + + def test_get_first_value + result = @db.get_first_value( "foo", "bar", "baz" ) + assert_equal "foo", result + end + + def test_changes + assert_equal 14, @db.changes + assert_equal 1, Driver.instance.mock_count(:changes) + end + + def test_total_changes + assert_equal 28, @db.total_changes + assert_equal 1, Driver.instance.mock_count(:total_changes) + end + + def test_interrupt + @db.interrupt + assert_equal 1, Driver.instance.mock_count(:interrupt) + end +end diff --git a/test/tc_integration.rb b/test/tc_integration.rb new file mode 100644 index 0000000..2dd6016 --- /dev/null +++ b/test/tc_integration.rb @@ -0,0 +1,838 @@ +$:.unshift "../lib" +$:.unshift "../ext/sqlite3_api" + +require 'test/unit' +require 'benchmark' +require 'sqlite3/database' + +class String + def to_utf16 + result = "" + self.each_byte { |b| result << b.chr << "\0" } + result + end + + def from_utf16 + result = "" + length.times do |i| + result << self[i,1] if i % 2 == 0 && self[i] != 0 + end + result + end +end + +module Integration + + drivers_to_test = ( ENV["SQLITE3_DRIVERS"] || "Native" ).split(',') + drivers_to_test.each do |driver| + + # == TC_OpenClose ========================================================= + + test_case = Class.new( Test::Unit::TestCase ) do + define_method( "test_create_close" ) do + begin + db = SQLite3::Database.new( "test-create.db", + :driver => driver ) + assert File.exist?( "test-create.db" ) + assert_nothing_raised { db.close } + ensure + File.delete( "test-create.db" ) rescue nil + end + end + + define_method( "test_open_close" ) do + begin + File.open( "test-open.db", "w" ) { |f| } + assert File.exist?( "test-open.db" ) + db = SQLite3::Database.new( "test-open.db", + :driver => driver ) + assert_nothing_raised { db.close } + ensure + File.delete( "test-open.db" ) rescue nil + end + end + + define_method( "test_bad_open" ) do + assert_raise( SQLite3::CantOpenException ) do + SQLite3::Database.new( ".", :driver => driver ) + end + end + end + const_set( "TC_OpenClose_#{driver}", test_case ) + + # == TC_Database ========================================================== + + test_case = Class.new( Test::Unit::TestCase ) do + define_method( "setup" ) do + @db = SQLite3::Database.new( "test.db", :driver=>driver ) + @db.transaction do + @db.execute "create table foo ( a integer primary key, b text )" + @db.execute "insert into foo ( b ) values ( 'foo' )" + @db.execute "insert into foo ( b ) values ( 'bar' )" + @db.execute "insert into foo ( b ) values ( 'baz' )" + end + end + + define_method( "teardown" ) do + @db.close + File.delete( "test.db" ) + end + + define_method( "test_complete_fail" ) do + assert !@db.complete?( "select * from foo" ) + end + define_method( "test_complete_success" ) do + assert @db.complete?( "select * from foo;" ) + end + + define_method( "test_complete_fail_utf16" ) do + assert !@db.complete?( "select * from foo".to_utf16+"\0\0", true ) + end + define_method( "test_complete_success_utf16" ) do + assert @db.complete?( "select * from foo;".to_utf16+"\0\0", true ) + end + + define_method( "test_errmsg" ) do + assert_equal "not an error", @db.errmsg + end + + define_method( "test_errmsg_utf16" ) do + assert_equal "not an error".to_utf16, @db.errmsg(true) + end + + define_method( "test_errcode" ) do + assert_equal 0, @db.errcode + end + + define_method( "test_trace" ) do + result = nil + @db.trace( "data" ) { |data,sql| result = [ data, sql ]; 0 } + @db.execute "select * from foo" + assert_equal ["data","select * from foo"], result + end + + define_method( "test_authorizer_okay" ) do + @db.authorizer( "data" ) { |data,type,a,b,c,d| 0 } + rows = @db.execute "select * from foo" + assert_equal 3, rows.length + end + + define_method( "test_authorizer_error" ) do + @db.authorizer( "data" ) { |data,type,a,b,c,d| 1 } + assert_raise( SQLite3::AuthorizationException ) do + @db.execute "select * from foo" + end + end + + define_method( "test_authorizer_silent" ) do + @db.authorizer( "data" ) { |data,type,a,b,c,d| 2 } + rows = @db.execute "select * from foo" + assert rows.empty? + end + + define_method( "test_prepare_invalid_syntax" ) do + assert_raise( SQLite3::SQLException ) do + @db.prepare "select from foo" + end + end + + define_method( "test_prepare_invalid_column" ) do + assert_raise( SQLite3::SQLException ) do + @db.prepare "select k from foo" + end + end + + define_method( "test_prepare_invalid_table" ) do + assert_raise( SQLite3::SQLException ) do + @db.prepare "select * from barf" + end + end + + define_method( "test_prepare_no_block" ) do + stmt = @db.prepare "select * from foo" + assert stmt.respond_to?(:execute) + stmt.close + end + + define_method( "test_prepare_with_block" ) do + called = false + @db.prepare "select * from foo" do |stmt| + called = true + assert stmt.respond_to?(:execute) + end + assert called + end + + define_method( "test_execute_no_block_no_bind_no_match" ) do + rows = @db.execute( "select * from foo where a > 100" ) + assert rows.empty? + end + + define_method( "test_execute_with_block_no_bind_no_match" ) do + called = false + @db.execute( "select * from foo where a > 100" ) do |row| + called = true + end + assert !called + end + + define_method( "test_execute_no_block_with_bind_no_match" ) do + rows = @db.execute( "select * from foo where a > ?", 100 ) + assert rows.empty? + end + + define_method( "test_execute_with_block_with_bind_no_match" ) do + called = false + @db.execute( "select * from foo where a > ?", 100 ) do |row| + called = true + end + assert !called + end + + define_method( "test_execute_no_block_no_bind_with_match" ) do + rows = @db.execute( "select * from foo where a = 1" ) + assert_equal 1, rows.length + end + + define_method( "test_execute_with_block_no_bind_with_match" ) do + called = 0 + @db.execute( "select * from foo where a = 1" ) do |row| + called += 1 + end + assert_equal 1, called + end + + define_method( "test_execute_no_block_with_bind_with_match" ) do + rows = @db.execute( "select * from foo where a = ?", 1 ) + assert_equal 1, rows.length + end + + define_method( "test_execute_with_block_with_bind_with_match" ) do + called = 0 + @db.execute( "select * from foo where a = ?", 1 ) do |row| + called += 1 + end + assert_equal 1, called + end + + define_method( "test_execute2_no_block_no_bind_no_match" ) do + columns, *rows = @db.execute2( "select * from foo where a > 100" ) + assert rows.empty? + assert [ "a", "b" ], columns + end + + define_method( "test_execute2_with_block_no_bind_no_match" ) do + called = 0 + @db.execute2( "select * from foo where a > 100" ) do |row| + assert [ "a", "b" ], row unless called == 0 + called += 1 + end + assert_equal 1, called + end + + define_method( "test_execute2_no_block_with_bind_no_match" ) do + columns, *rows = @db.execute2( "select * from foo where a > ?", 100 ) + assert rows.empty? + assert [ "a", "b" ], columns + end + + define_method( "test_execute2_with_block_with_bind_no_match" ) do + called = 0 + @db.execute2( "select * from foo where a > ?", 100 ) do |row| + assert [ "a", "b" ], row unless called == 0 + called += 1 + end + assert_equal 1, called + end + + define_method( "test_execute2_no_block_no_bind_with_match" ) do + columns, *rows = @db.execute2( "select * from foo where a = 1" ) + assert_equal 1, rows.length + assert [ "a", "b" ], columns + end + + define_method( "test_execute2_with_block_no_bind_with_match" ) do + called = 0 + @db.execute2( "select * from foo where a = 1" ) do |row| + assert [ "a", "b" ], row unless called == 0 + called += 1 + end + assert_equal 2, called + end + + define_method( "test_execute2_no_block_with_bind_with_match" ) do + columns, *rows = @db.execute2( "select * from foo where a = ?", 1 ) + assert_equal 1, rows.length + assert [ "a", "b" ], columns + end + + define_method( "test_execute2_with_block_with_bind_with_match" ) do + called = 0 + @db.execute2( "select * from foo where a = ?", 1 ) do |row| + called += 1 + end + assert_equal 2, called + end + + define_method( "test_execute_batch_empty" ) do + assert_nothing_raised { @db.execute_batch "" } + end + + define_method( "test_execute_batch_no_bind" ) do + @db.transaction do + @db.execute_batch <<-SQL + create table bar ( a, b, c ); + insert into bar values ( 'one', 2, 'three' ); + insert into bar values ( 'four', 5, 'six' ); + insert into bar values ( 'seven', 8, 'nine' ); + SQL + end + rows = @db.execute( "select * from bar" ) + assert_equal 3, rows.length + end + + define_method( "test_execute_batch_with_bind" ) do + @db.execute_batch( <<-SQL, 1 ) + create table bar ( a, b, c ); + insert into bar values ( 'one', 2, ? ); + insert into bar values ( 'four', 5, ? ); + insert into bar values ( 'seven', 8, ? ); + SQL + rows = @db.execute( "select * from bar" ).map { |a,b,c| c } + assert_equal %w{1 1 1}, rows + end + + define_method( "test_get_first_row_no_bind_no_match" ) do + result = @db.get_first_row( "select * from foo where a=100" ) + assert_nil result + end + + define_method( "test_get_first_row_no_bind_with_match" ) do + result = @db.get_first_row( "select * from foo where a=1" ) + assert_equal [ "1", "foo" ], result + end + + define_method( "test_get_first_row_with_bind_no_match" ) do + result = @db.get_first_row( "select * from foo where a=?", 100 ) + assert_nil result + end + + define_method( "test_get_first_row_with_bind_with_match" ) do + result = @db.get_first_row( "select * from foo where a=?", 1 ) + assert_equal [ "1", "foo" ], result + end + + define_method( "test_get_first_value_no_bind_no_match" ) do + result = @db.get_first_value( "select b, a from foo where a=100" ) + assert_nil result + end + + define_method( "test_get_first_value_no_bind_with_match" ) do + result = @db.get_first_value( "select b, a from foo where a=1" ) + assert_equal "foo", result + end + + define_method( "test_get_first_value_with_bind_no_match" ) do + result = @db.get_first_value( "select b, a from foo where a=?", 100 ) + assert_nil result + end + + define_method( "test_get_first_value_with_bind_with_match" ) do + result = @db.get_first_value( "select b, a from foo where a=?", 1 ) + assert_equal "foo", result + end + + define_method( "test_last_insert_row_id" ) do + @db.execute "insert into foo ( b ) values ( 'test' )" + assert_equal 4, @db.last_insert_row_id + @db.execute "insert into foo ( b ) values ( 'again' )" + assert_equal 5, @db.last_insert_row_id + end + + define_method( "test_changes" ) do + @db.execute "insert into foo ( b ) values ( 'test' )" + assert_equal 1, @db.changes + @db.execute "delete from foo where 1=1" + assert_equal 4, @db.changes + end + + define_method( "test_total_changes" ) do + assert_equal 3, @db.total_changes + @db.execute "insert into foo ( b ) values ( 'test' )" + @db.execute "delete from foo where 1=1" + assert_equal 8, @db.total_changes + end + + define_method( "test_transaction_nest" ) do + assert_raise( SQLite3::SQLException ) do + @db.transaction do + @db.transaction do + end + end + end + end + + define_method( "test_transaction_rollback" ) do + @db.transaction + @db.execute_batch <<-SQL + insert into foo (b) values ( 'test1' ); + insert into foo (b) values ( 'test2' ); + insert into foo (b) values ( 'test3' ); + insert into foo (b) values ( 'test4' ); + SQL + assert_equal 7, @db.get_first_value("select count(*) from foo").to_i + @db.rollback + assert_equal 3, @db.get_first_value("select count(*) from foo").to_i + end + + define_method( "test_transaction_commit" ) do + @db.transaction + @db.execute_batch <<-SQL + insert into foo (b) values ( 'test1' ); + insert into foo (b) values ( 'test2' ); + insert into foo (b) values ( 'test3' ); + insert into foo (b) values ( 'test4' ); + SQL + assert_equal 7, @db.get_first_value("select count(*) from foo").to_i + @db.commit + assert_equal 7, @db.get_first_value("select count(*) from foo").to_i + end + + define_method( "test_transaction_rollback_in_block" ) do + assert_raise( SQLite3::SQLException ) do + @db.transaction do + @db.rollback + end + end + end + + define_method( "test_transaction_commit_in_block" ) do + assert_raise( SQLite3::SQLException ) do + @db.transaction do + @db.commit + end + end + end + + define_method( "test_transaction_active" ) do + assert !@db.transaction_active? + @db.transaction + assert @db.transaction_active? + @db.commit + assert !@db.transaction_active? + end + + define_method( "no_tests_at" ) do |file,line,method| + warn "[(#{self.class}):#{file}:#{line}] no tests for #{method}" + end + + define_method( "test_interrupt" ) do + @db.create_function( "abort", 1 ) do |func,x| + @db.interrupt + func.result = x + end + + assert_raise( SQLite3::SQLException ) do + @db.execute "select abort(a) from foo" + end + end + + define_method( "test_busy_handler_outwait" ) do + begin + db2 = SQLite3::Database.open( "test.db", :driver=>driver ) + handler_call_count = 0 + db2.busy_handler do |data,count| + handler_call_count += 1 + sleep 0.1 + 1 + end + + t = Thread.new do + @db.transaction( :exclusive ) do + sleep 0.1 + end + end + + assert_nothing_raised do + db2.execute "insert into foo (b) values ( 'from 2' )" + end + + assert_equal 1, handler_call_count + + t.join + ensure + db2.close if db2 + end + end + + define_method( "test_busy_handler_impatient" ) do + begin + db2 = SQLite3::Database.open( "test.db", :driver=>driver ) + handler_call_count = 0 + db2.busy_handler do |data,count| + handler_call_count += 1 + sleep 0.1 + 0 + end + + t = Thread.new do + @db.transaction( :exclusive ) do + sleep 0.2 + end + end + + assert_raise( SQLite3::BusyException ) do + db2.execute "insert into foo (b) values ( 'from 2' )" + end + + assert_equal 1, handler_call_count + + t.join + ensure + db2.close if db2 + end + end + + define_method( "test_busy_timeout" ) do + begin + db2 = SQLite3::Database.open( "test.db", :driver=>driver ) + db2.busy_timeout 300 + + t = Thread.new do + @db.transaction( :exclusive ) do + sleep 0.1 + end + end + + time = Benchmark.measure do + assert_raise( SQLite3::BusyException ) do + db2.execute "insert into foo (b) values ( 'from 2' )" + end + end + assert time.real*1000 >= 300 + + t.join + ensure + db2.close if db2 + end + end + + define_method( "test_create_function" ) do + @db.create_function( "munge", 1 ) do |func,x| + func.result = ">>>#{x}<<<" + end + + value = @db.get_first_value( "select munge(b) from foo where a=1" ) + assert_match( />>>.*<<</, value ) + end + + define_method( "test_create_aggregate_without_block" ) do + step = proc do |ctx,a| + ctx[:sum] ||= 0 + ctx[:sum] += a.to_i + end + + final = proc { |ctx| ctx.result = ctx[:sum] } + + @db.create_aggregate( "accumulate", 1, step, final ) + + value = @db.get_first_value( "select accumulate(a) from foo" ) + assert_equal "6", value + end + + define_method( "test_create_aggregate_with_block" ) do + @db.create_aggregate( "accumulate", 1 ) do + step do |ctx,a| + ctx[:sum] ||= 0 + ctx[:sum] += a.to_i + end + + finalize { |ctx| ctx.result = ctx[:sum] } + end + + value = @db.get_first_value( "select accumulate(a) from foo" ) + assert_equal "6", value + end + + define_method( "test_create_aggregate_with_no_data" ) do + @db.create_aggregate( "accumulate", 1 ) do + step do |ctx,a| + ctx[:sum] ||= 0 + ctx[:sum] += a.to_i + end + + finalize { |ctx| ctx.result = ctx[:sum] || 0 } + end + + value = @db.get_first_value( + "select accumulate(a) from foo where a = 100" ) + assert_equal "0", value + end + + define_method( "test_create_aggregate_handler" ) do + handler = Class.new do + class << self + define_method( "arity" ) { 1 } + define_method( "text_rep" ) { SQLite3::Constants::TextRep::ANY } + define_method( "name" ) { "multiply" } + end + define_method( "step" ) do |ctx,a| + ctx[:buffer] ||= 1 + ctx[:buffer] *= a.to_i + end + define_method( "finalize" ) { |ctx| ctx.result = ctx[:buffer] } + end + + @db.create_aggregate_handler( handler ) + value = @db.get_first_value( "select multiply(a) from foo" ) + assert_equal "6", value + end + end + const_set( "TC_Database_#{driver}", test_case ) + + # == TC_Statement ========================================================= + + test_case = Class.new( Test::Unit::TestCase ) do + define_method( "setup" ) do + @db = SQLite3::Database.new( "test.db", :driver=>driver ) + @db.transaction do + @db.execute "create table foo ( a integer primary key, b text )" + @db.execute "insert into foo ( b ) values ( 'foo' )" + @db.execute "insert into foo ( b ) values ( 'bar' )" + @db.execute "insert into foo ( b ) values ( 'baz' )" + end + @stmt = @db.prepare( "select * from foo where a in ( ?, :named )" ) + end + + define_method( "teardown" ) do + @stmt.close + @db.close + File.delete( "test.db" ) + end + + define_method( "test_remainder_empty" ) do + assert_equal "", @stmt.remainder + end + + define_method( "test_remainder_nonempty" ) do + called = false + @db.prepare( "select * from foo;\n blah" ) do |stmt| + called = true + assert_equal "\n blah", stmt.remainder + end + assert called + end + + define_method( "test_bind_params_empty" ) do + assert_nothing_raised { @stmt.bind_params } + assert @stmt.execute!.empty? + end + + define_method( "test_bind_params_array" ) do + @stmt.bind_params 1, 2 + assert_equal 2, @stmt.execute!.length + end + + define_method( "test_bind_params_hash" ) do + @stmt.bind_params ":named" => 2 + assert_equal 1, @stmt.execute!.length + end + + define_method( "test_bind_params_mixed" ) do + @stmt.bind_params( 1, ":named" => 2 ) + assert_equal 2, @stmt.execute!.length + end + + define_method( "test_bind_param_by_index" ) do + @stmt.bind_params( 1, 2 ) + assert_equal 2, @stmt.execute!.length + end + + define_method( "test_bind_param_by_name_bad" ) do + assert_raise( SQLite3::Exception ) { @stmt.bind_param( "named", 2 ) } + end + + define_method( "test_bind_param_by_name_good" ) do + @stmt.bind_param( ":named", 2 ) + assert_equal 1, @stmt.execute!.length + end + + define_method( "test_execute_no_bind_no_block" ) do + assert_instance_of SQLite3::ResultSet, @stmt.execute + end + + define_method( "test_execute_with_bind_no_block" ) do + assert_instance_of SQLite3::ResultSet, @stmt.execute( 1, 2 ) + end + + define_method( "test_execute_no_bind_with_block" ) do + called = false + @stmt.execute { |row| called = true } + assert called + end + + define_method( "test_execute_with_bind_with_block" ) do + called = 0 + @stmt.execute( 1, 2 ) { |row| called += 1 } + assert_equal 1, called + end + + define_method( "test_reexecute" ) do + r = @stmt.execute( 1, 2 ) + assert_equal 2, r.to_a.length + assert_nothing_raised { r = @stmt.execute( 1, 2 ) } + assert_equal 2, r.to_a.length + end + + define_method( "test_execute_bang_no_bind_no_block" ) do + assert @stmt.execute!.empty? + end + + define_method( "test_execute_bang_with_bind_no_block" ) do + assert_equal 2, @stmt.execute!( 1, 2 ).length + end + + define_method( "test_execute_bang_no_bind_with_block" ) do + called = 0 + @stmt.execute! { |row| called += 1 } + assert_equal 0, called + end + + define_method( "test_execute_bang_with_bind_with_block" ) do + called = 0 + @stmt.execute!( 1, 2 ) { |row| called += 1 } + assert_equal 2, called + end + + define_method( "test_columns" ) do + c1 = @stmt.columns + c2 = @stmt.columns + assert_same c1, c2 + assert_equal 2, c1.length + end + + define_method( "test_columns_computed" ) do + called = false + @db.prepare( "select count(*) from foo" ) do |stmt| + called = true + assert_equal [ "count(*)" ], stmt.columns + end + assert called + end + + define_method( "test_types" ) do + t1 = @stmt.types + t2 = @stmt.types + assert_same t1, t2 + assert_equal 2, t1.length + end + + define_method( "test_types_computed" ) do + called = false + @db.prepare( "select count(*) from foo" ) do |stmt| + called = true + assert_equal [ nil ], stmt.types + end + assert called + end + end + const_set( "TC_Statement_#{driver}", test_case ) + + # == TC_ResultSet ========================================================= + + test_case = Class.new( Test::Unit::TestCase ) do + define_method( "setup" ) do + @db = SQLite3::Database.new( "test.db", :driver=>driver ) + @db.transaction do + @db.execute "create table foo ( a integer primary key, b text )" + @db.execute "insert into foo ( b ) values ( 'foo' )" + @db.execute "insert into foo ( b ) values ( 'bar' )" + @db.execute "insert into foo ( b ) values ( 'baz' )" + end + @stmt = @db.prepare( "select * from foo where a in ( ?, ? )" ) + @result = @stmt.execute + end + + define_method( "teardown" ) do + @stmt.close + @db.close + File.delete( "test.db" ) + end + + define_method( "test_reset_unused" ) do + assert_nothing_raised { @result.reset } + assert @result.to_a.empty? + end + + define_method( "test_reset_used" ) do + @result.to_a + assert_nothing_raised { @result.reset } + assert @result.to_a.empty? + end + + define_method( "test_reset_with_bind" ) do + @result.to_a + assert_nothing_raised { @result.reset( 1, 2 ) } + assert_equal 2, @result.to_a.length + end + + define_method( "test_eof_inner" ) do + @result.reset( 1 ) + assert !@result.eof? + end + + define_method( "test_eof_edge" ) do + @result.reset( 1 ) + @result.next # to first row + @result.next # to end of result set + assert @result.eof? + end + + define_method( "test_next_eof" ) do + @result.reset( 1 ) + assert_not_nil @result.next + assert_nil @result.next + end + + define_method( "test_next_no_type_translation_no_hash" ) do + @result.reset( 1 ) + assert_equal [ "1", "foo" ], @result.next + end + + define_method( "test_next_type_translation" ) do + @db.type_translation = true + @result.reset( 1 ) + assert_equal [ 1, "foo" ], @result.next + end + + define_method( "test_next_results_as_hash" ) do + @db.results_as_hash = true + @result.reset( 1 ) + assert_equal( { "a" => "1", "b" => "foo", 0 => "1", 1 => "foo" }, + @result.next ) + end + + define_method( "test_each" ) do + called = 0 + @result.reset( 1, 2 ) + @result.each { |row| called += 1 } + assert_equal 2, called + end + + define_method( "test_enumerable" ) do + @result.reset( 1, 2 ) + assert_equal 2, @result.to_a.length + end + + define_method( "test_types" ) do + assert_equal [ "integer", "text" ], @result.types + end + + define_method( "test_columns" ) do + assert_equal [ "a", "b" ], @result.columns + end + end + const_set( "TC_ResultSet_#{driver}", test_case ) + end + +end diff --git a/test/tests.rb b/test/tests.rb new file mode 100644 index 0000000..3bded2c --- /dev/null +++ b/test/tests.rb @@ -0,0 +1,36 @@ +#-- +# ============================================================================= +# Copyright (c) 2004, Jamis Buck (jgb3@email.byu.edu) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# ============================================================================= +#++ + + +Dir.chdir File.dirname( __FILE__ ) +Dir["**/tc_*.rb"].each { |file| load file } |