summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJamis Buck <jamis@37signals.com>2005-01-09 15:04:16 +0000
committerJamis Buck <jamis@37signals.com>2005-01-09 15:04:16 +0000
commitc51fa9fc3d7c0ec6f59bcaf3d352fae9325857e2 (patch)
treefa6c5fdee64da937145d594348100b03225a4207
downloadthird_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
-rw-r--r--LICENSE27
-rw-r--r--README36
-rw-r--r--Rakefile178
-rw-r--r--doc/faq/faq.rb177
-rw-r--r--doc/faq/faq.yml426
-rw-r--r--ext/sqlite3_api/MANIFEST4
-rw-r--r--ext/sqlite3_api/extconf.rb8
-rw-r--r--ext/sqlite3_api/post-clean.rb3
-rw-r--r--ext/sqlite3_api/post-distclean.rb4
-rw-r--r--ext/sqlite3_api/sqlite3_api.i339
-rw-r--r--lib/sqlite3.rb33
-rw-r--r--lib/sqlite3/constants.rb81
-rw-r--r--lib/sqlite3/database.rb720
-rw-r--r--lib/sqlite3/driver/dl/api.rb184
-rw-r--r--lib/sqlite3/driver/dl/driver.rb334
-rw-r--r--lib/sqlite3/driver/native/driver.rb218
-rw-r--r--lib/sqlite3/errors.rb84
-rw-r--r--lib/sqlite3/pragmas.rb254
-rw-r--r--lib/sqlite3/resultset.rb172
-rw-r--r--lib/sqlite3/statement.rb218
-rw-r--r--lib/sqlite3/translator.rb135
-rw-r--r--lib/sqlite3/value.rb89
-rw-r--r--lib/sqlite3/version.rb45
-rw-r--r--setup.rb1333
-rw-r--r--sqlite3-ruby-win32.gemspec28
-rw-r--r--sqlite3-ruby.gemspec30
-rw-r--r--test/bm.rb140
-rw-r--r--test/driver/dl/tc_driver.rb322
-rw-r--r--test/mocks.rb99
-rw-r--r--test/native-vs-dl.rb126
-rw-r--r--test/tc_database.rb216
-rw-r--r--test/tc_integration.rb838
-rw-r--r--test/tests.rb36
33 files changed, 6937 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f55ce29
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README b/README
new file mode 100644
index 0000000..47a395d
--- /dev/null
+++ b/README
@@ -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 }