Skip to content

Commit

Permalink
Enable full support for SQLite-JDBC using the JDBC adapter
Browse files Browse the repository at this point in the history
SQLite-JDBC is now fully supported using the JDBC adapter.  It passes
the SQLite specs (which did have to be modified, but now support both
the native and JDBC adapters.  It passes the integration test suite.

This was done similar to the PostgreSQL-JDBC adapter, by spliting the
native adapter into shared and unshared parts, using the shared parts
in the JDBC adapter, and reimplementing the unshared parts.

This doesn't mean that the native and JDBC drivers operate exactly
the same.  For example, the native adapter will return strings for
values such as computed columns, where the JDBC adapter will return
numbers.  The JDBC adapter also does not allow submitting multiple
statements at once.  Also, the JDBC adapter will raise a generic
Error instead of Error::InvalidStatement if there is a problem with
the query.

One significant change is that Schema operations can now return
arrays in addition to strings, and the Database object will handle it
correctly.  This was done to allow drop_column to work with
SQLite-JDBC, since you can't submit multiple statements at once,
and that is necessary to handle a column drop in SQLite.

A type_test integration test has been added for testing that certain
database types are supported.

A few minor changes:

* JDBC::Database::url alias to uri was added
* Database::<< uses execute_ddl instead of execute
* The SQLite specs no longer require the use of a memory database
* You can use the SQLITE_URL constant instead of SQLITE_DB
* You can use the SEQUEL_SQLITE_SPEC_DB environment variable as well
* The dataset integration tests check delete and update return values
  • Loading branch information
jeremyevans committed Jul 25, 2008
1 parent 332883a commit f8aa8a3
Show file tree
Hide file tree
Showing 11 changed files with 400 additions and 279 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG
@@ -1,5 +1,7 @@
=== HEAD

* Enable full support for SQLite-JDBC using the JDBC adapter (jeremyevans)

* Minor changes to allow for full Ruby 1.9 compatibility (jeremyevans)

* Make Database#disconnect work for the ADO adapter (spicyj)
Expand Down
34 changes: 21 additions & 13 deletions lib/sequel_core/adapters/jdbc.rb
Expand Up @@ -7,22 +7,29 @@ module JavaSQL; include_package 'java.sql'; end
DATABASE_SETUP = {:postgresql=>proc do |db|
require 'sequel_core/adapters/jdbc/postgresql'
db.extend(Sequel::JDBC::Postgres::DatabaseMethods)
begin
require 'jdbc/postgres'
rescue LoadError
# jdbc-postgres gem not used, hopefully the user has the
# PostgreSQL-JDBC .jar in their CLASSPATH
end
JDBC.load_gem('postgres')
org.postgresql.Driver
end,
:mysql=>proc{com.mysql.jdbc.Driver},
:sqlite=>proc{org.sqlite.JDBC},
:mysql=>proc do |db|
JDBC.load_gem('mysql')
com.mysql.jdbc.Driver
end,
:sqlite=>proc do |db|
require 'sequel_core/adapters/jdbc/sqlite'
db.extend(Sequel::JDBC::SQLite::DatabaseMethods)
JDBC.load_gem('sqlite3')
org.sqlite.JDBC
end,
:oracle=>proc{oracle.jdbc.driver.OracleDriver},
:sqlserver=>proc{com.microsoft.sqlserver.jdbc.SQLServerDriver}
}

def self.load_driver(driver)
JavaLang::Class.forName(driver)
def self.load_gem(name)
begin
require "jdbc/#{name}"
rescue LoadError
# jdbc gem not used, hopefully the user has the .jar in their CLASSPATH
end
end

class Database < Sequel::Database
Expand Down Expand Up @@ -57,7 +64,7 @@ def execute(sql)
stmt = conn.createStatement
begin
yield stmt.executeQuery(sql)
rescue NativeException => e
rescue NativeException, JavaSQL::SQLException => e
raise Error, e.message
ensure
stmt.close
Expand All @@ -71,7 +78,7 @@ def execute_ddl(sql)
stmt = conn.createStatement
begin
stmt.execute(sql)
rescue NativeException => e
rescue NativeException, JavaSQL::SQLException => e
raise Error, e.message
ensure
stmt.close
Expand All @@ -85,7 +92,7 @@ def execute_dui(sql)
stmt = conn.createStatement
begin
stmt.executeUpdate(sql)
rescue NativeException => e
rescue NativeException, JavaSQL::SQLException => e
raise Error, e.message
ensure
stmt.close
Expand All @@ -101,6 +108,7 @@ def uri
ur = @opts[:uri] || @opts[:url] || @opts[:database]
ur =~ /^\Ajdbc:/ ? ur : "jdbc:#{ur}"
end
alias url uri

private

Expand Down
72 changes: 72 additions & 0 deletions lib/sequel_core/adapters/jdbc/sqlite.rb
@@ -0,0 +1,72 @@
require 'sequel_core/adapters/shared/sqlite'

module Sequel
module JDBC
module SQLite
module DatabaseMethods
include Sequel::SQLite::DatabaseMethods

def dataset(opts=nil)
Sequel::JDBC::SQLite::Dataset.new(self, opts)
end

def execute_insert(sql)
begin
log_info(sql)
@pool.hold do |conn|
stmt = conn.createStatement
begin
stmt.executeUpdate(sql)
rs = stmt.executeQuery('SELECT last_insert_rowid()')
rs.next
rs.getInt(1)
rescue NativeException, JavaSQL::SQLException => e
raise Error, e.message
ensure
stmt.close
end
end
rescue NativeException, JavaSQL::SQLException => e
raise Error, "#{sql}\r\n#{e.message}"
end
end

def transaction
@pool.hold do |conn|
@transactions ||= []
return yield(conn) if @transactions.include?(Thread.current)
stmt = conn.createStatement
begin
log_info(Sequel::Database::SQL_BEGIN)
stmt.execute(Sequel::Database::SQL_BEGIN)
@transactions << Thread.current
yield(conn)
rescue Exception => e
log_info(Sequel::Database::SQL_ROLLBACK)
stmt.execute(Sequel::Database::SQL_ROLLBACK)
raise e unless Error::Rollback === e
ensure
unless e
log_info(Sequel::Database::SQL_COMMIT)
stmt.execute(Sequel::Database::SQL_COMMIT)
end
stmt.close
@transactions.delete(Thread.current)
end
end
end

private

def connection_pool_default_options
o = super
uri == 'jdbc:sqlite::memory:' ? o.merge(:max_connections=>1) : o
end
end

class Dataset < JDBC::Dataset
include Sequel::SQLite::DatasetMethods
end
end
end
end
146 changes: 146 additions & 0 deletions lib/sequel_core/adapters/shared/sqlite.rb
@@ -0,0 +1,146 @@
module Sequel
module SQLite
module DatabaseMethods
AUTO_VACUUM = {'0' => :none, '1' => :full, '2' => :incremental}.freeze
SCHEMA_TYPE_RE = /\A(\w+)\((\d+)\)\z/
SYNCHRONOUS = {'0' => :off, '1' => :normal, '2' => :full}.freeze
TABLES_FILTER = "type = 'table' AND NOT name = 'sqlite_sequence'"
TEMP_STORE = {'0' => :default, '1' => :file, '2' => :memory}.freeze

def alter_table_sql(table, op)
case op[:op]
when :add_column
super
when :add_index
index_definition_sql(table, op)
when :drop_column
columns_str = (schema_parse_table(table, {}).map{|c| c[0]} - Array(op[:name])).join(",")
["BEGIN TRANSACTION",
"CREATE TEMPORARY TABLE #{table}_backup(#{columns_str})",
"INSERT INTO #{table}_backup SELECT #{columns_str} FROM #{table}",
"DROP TABLE #{table}",
"CREATE TABLE #{table}(#{columns_str})",
"INSERT INTO #{table} SELECT #{columns_str} FROM #{table}_backup",
"DROP TABLE #{table}_backup",
"COMMIT"]
else
raise Error, "Unsupported ALTER TABLE operation"
end
end

def auto_vacuum
AUTO_VACUUM[pragma_get(:auto_vacuum).to_s]
end

def auto_vacuum=(value)
value = AUTO_VACUUM.key(value) || (raise Error, "Invalid value for auto_vacuum option. Please specify one of :none, :full, :incremental.")
pragma_set(:auto_vacuum, value)
end

def pragma_get(name)
self["PRAGMA #{name}"].single_value
end

def pragma_set(name, value)
execute_ddl("PRAGMA #{name} = #{value}")
end

def serial_primary_key_options
{:primary_key => true, :type => :integer, :auto_increment => true}
end

def synchronous
SYNCHRONOUS[pragma_get(:synchronous).to_s]
end

def synchronous=(value)
value = SYNCHRONOUS.key(value) || (raise Error, "Invalid value for synchronous option. Please specify one of :off, :normal, :full.")
pragma_set(:synchronous, value)
end

def tables
self[:sqlite_master].filter(TABLES_FILTER).map {|r| r[:name].to_sym}
end

def temp_store
TEMP_STORE[pragma_get(:temp_store).to_s]
end

def temp_store=(value)
value = TEMP_STORE.key(value) || (raise Error, "Invalid value for temp_store option. Please specify one of :default, :file, :memory.")
pragma_set(:temp_store, value)
end

private

def schema_parse_table(table_name, opts)
rows = self["PRAGMA table_info(?)", table_name].collect do |row|
row.delete(:cid)
row[:column] = row.delete(:name)
row[:allow_null] = row.delete(:notnull).to_i == 0 ? 'YES' : 'NO'
row[:default] = row.delete(:dflt_value)
row[:primary_key] = row.delete(:pk).to_i == 1 ? true : false
row[:db_type] = row.delete(:type)
if m = SCHEMA_TYPE_RE.match(row[:db_type])
row[:db_type] = m[1]
row[:max_chars] = m[2].to_i
else
row[:max_chars] = nil
end
row[:numeric_precision] = nil
row
end
schema_parse_rows(rows)
end

def schema_parse_tables(opts)
schemas = {}
tables.each{|table| schemas[table] = schema_parse_table(table, opts)}
schemas
end
end

module DatasetMethods
def complex_expression_sql(op, args)
case op
when :~, :'!~', :'~*', :'!~*'
raise Error, "SQLite does not support pattern matching via regular expressions"
when :LIKE, :'NOT LIKE', :ILIKE, :'NOT ILIKE'
# SQLite is case insensitive for ASCII, and non case sensitive for other character sets
"#{'NOT ' if [:'NOT LIKE', :'NOT ILIKE'].include?(op)}(#{literal(args.at(0))} LIKE #{literal(args.at(1))})"
else
super(op, args)
end
end

def delete(opts = nil)
# check if no filter is specified
unless (opts && opts[:where]) || @opts[:where]
@db.transaction do
unfiltered_count = count
@db.execute_dui delete_sql(opts)
unfiltered_count
end
else
@db.execute_dui delete_sql(opts)
end
end

def insert(*values)
@db.execute_insert insert_sql(*values)
end

def insert_sql(*values)
if (values.size == 1) && values.first.is_a?(Sequel::Dataset)
"INSERT INTO #{source_list(@opts[:from])} #{values.first.sql};"
else
super(*values)
end
end

def quoted_identifier(c)
"`#{c}`"
end
end
end
end

0 comments on commit f8aa8a3

Please sign in to comment.