Skip to content

Commit

Permalink
Add Amalgalite adapter
Browse files Browse the repository at this point in the history
Amalgalite is a ruby library that embeds SQLite in a ruby extension
without requiring a separate SQLite installation.

This adapter is full featured and passes all integration tests, and
all but one of the SQLite adapter tests (I could fix that failing
test, but I don't think it's worth it).

The only major issue with the amalgalite adapter is that it is
currently pretty slow (10 times slower in the integration tests,
40 times slower in the adapter specs).  This could have something
to do with the fact that the adapter reloads the entire schema
whenever there is a possibility that cached information could be
stale.  I didn't want to do that, but otherwise amalgalite can
give you weird NoMethodErrors.

As I don't use this in production, I'm not planning on working on
the performance issues.
  • Loading branch information
jeremyevans committed Apr 15, 2009
1 parent 26ead69 commit 8398b5b
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 7 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
=== HEAD

* Add Amalgalite adapter (jeremyevans)

* Remove Sequel::Metaprogramming#metaattr_accessor and metaattr_reader (jeremyevans)

* Remove Dataset#irregular_function_sql (jeremyevans)
Expand Down
6 changes: 3 additions & 3 deletions README.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ Sequel is a lightweight database access toolkit for Ruby.
configurations, and database sharding.
* Sequel makes it easy to deal with multiple records without having
to break your teeth on SQL.
* Sequel currently has adapters for ADO, DataObjects, DB2, DBI,
Firebird, Informix, JDBC, MySQL, ODBC, OpenBase, Oracle, PostgreSQL
and SQLite3.
* Sequel currently has adapters for ADO, Amalgalite, DataObjects,
DB2, DBI, Firebird, Informix, JDBC, MySQL, ODBC, OpenBase, Oracle,
PostgreSQL and SQLite3.

== Resources

Expand Down
203 changes: 203 additions & 0 deletions lib/sequel/adapters/amalgalite.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
require 'amalgalite'
Sequel.require 'adapters/shared/sqlite'

module Sequel
# Top level module for holding all Amalgalite-related modules and classes
# for Sequel.
module Amalgalite
# Type conversion map class for Sequel's use of Amalgamite
class SequelTypeMap < ::Amalgalite::TypeMaps::DefaultMap
methods_handling_sql_types.delete('string')
methods_handling_sql_types.merge!(
'datetime' => %w'datetime timestamp',
'time' => %w'time',
'float' => ['float', 'double', 'real', 'double precision'],
'decimal' => %w'numeric decimal money'
)

# Return blobs as instances of Sequel::SQL::Blob instead of
# Amalgamite::Blob
def blob(s)
SQL::Blob.new(s)
end

# Return numeric/decimal types as instances of BigDecimal
# instead of Float
def decimal(s)
BigDecimal.new(s)
end

# Return datetime types as instances of Sequel.datetime_class
def datetime(s)
Sequel.string_to_datetime(s)
end

# Don't raise an error if the value is a string and the declared
# type doesn't match a known type, just return the value.
def result_value_of(declared_type, value)
if value.is_a?(::Amalgalite::Blob)
SQL::Blob.new(value.source)
elsif value.is_a?(String) && declared_type
(meth = self.class.sql_to_method(declared_type.downcase)) ? send(meth, value) : value
else
super
end
end
end

# Database class for SQLite databases used with Sequel and the
# amalgalite driver.
class Database < Sequel::Database
include ::Sequel::SQLite::DatabaseMethods

set_adapter_scheme :amalgalite

# Mimic the file:// uri, by having 2 preceding slashes specify a relative
# path, and 3 preceding slashes specify an absolute path.
def self.uri_to_options(uri) # :nodoc:
{ :database => (uri.host.nil? && uri.path == '/') ? nil : "#{uri.host}#{uri.path}" }
end
private_class_method :uri_to_options

# Connect to the database. Since SQLite is a file based database,
# the only options available are :database (to specify the database
# name), and :timeout, to specify how long to wait for the database to
# be available if it is locked, given in milliseconds (default is 5000).
def connect(server)
opts = server_opts(server)
opts[:database] = ':memory:' if blank_object?(opts[:database])
db = ::Amalgalite::Database.new(opts[:database])
db.busy_handler(::Amalgalite::BusyTimeout.new(opts.fetch(:timeout, 5000)/50, 50))
db.type_map = SequelTypeMap.new
db
end

# Return instance of Sequel::Amalgalite::Dataset with the given options.
def dataset(opts = nil)
Amalgalite::Dataset.new(self, opts)
end

# Run the given SQL with the given arguments and reload the schema. Returns nil.
def execute_ddl(sql, opts={})
_execute(sql, opts){|conn| conn.execute_batch(sql); conn.reload_schema!}
nil
end

# Run the given SQL with the given arguments and return the number of changed rows.
def execute_dui(sql, opts={})
_execute(sql, opts){|conn| conn.execute_batch(sql); conn.row_changes}
end

# Run the given SQL with the given arguments and return the last inserted row id.
def execute_insert(sql, opts={})
_execute(sql, opts){|conn| conn.execute_batch(sql); conn.last_insert_rowid}
end

# Run the given SQL with the given arguments and yield each row.
def execute(sql, opts={})
retried = false
_execute(sql, opts) do |conn|
conn.prepare(sql) do |stmt|
begin
stmt.result_meta
rescue NoMethodError
conn.reload_schema!
stmt.result_meta
end
yield stmt
end
end
end

# Run the given SQL with the given arguments and return the first value of the first row.
def single_value(sql, opts={})
_execute(sql, opts){|conn| conn.first_value_from(sql)}
end

# Use the native driver transaction method if there isn't already a transaction
# in progress on the connection, always yielding a connection inside a transaction
# transaction.
def transaction(opts={})
synchronize(opts[:server]) do |conn|
return yield(conn) if conn.in_transaction?
begin
result = nil
log_info('Transaction.begin')
conn.transaction{result = yield(conn)}
result
rescue ::Exception => e
log_info('Transaction.rollback')
transaction_error(e, ::Amalgalite::Error, ::Amalgalite::SQLite3::Error)
ensure
log_info('Transaction.commit') unless e
end
end
end

private

# Log the SQL and yield an available connection. Rescue
# any Amalgalite::Errors and turn them into DatabaseErrors.
def _execute(sql, opts)
begin
log_info(sql)
synchronize(opts[:server]){|conn| yield conn}
rescue ::Amalgalite::Error, ::Amalgalite::SQLite3::Error => e
raise_error(e)
end
end

# The Amagalite adapter does not need the pool to convert exceptions.
# Also, force the max connections to 1 if a memory database is being
# used, as otherwise each connection gets a separate database.
def connection_pool_default_options
o = super.merge(:pool_convert_exceptions=>false)
# Default to only a single connection if a memory database is used,
# because otherwise each connection will get a separate database
o[:max_connections] = 1 if @opts[:database] == ':memory:' || blank_object?(@opts[:database])
o
end

# Disconnect given connections from the database.
def disconnect_connection(c)
c.close
end
end

# Dataset class for SQLite datasets that use the amalgalite driver.
class Dataset < Sequel::Dataset
include ::Sequel::SQLite::DatasetMethods

EXPLAIN = 'EXPLAIN %s'.freeze

# Return an array of strings specifying a query explanation for the
# current dataset.
def explain
res = []
@db.result_set(EXPLAIN % select_sql(opts), nil) {|r| res << r}
res
end

# Yield a hash for each row in the dataset.
def fetch_rows(sql)
execute(sql) do |stmt|
stmt.result_meta
@columns = cols = stmt.result_fields.map{|c| output_identifier(c)}
col_count = cols.size
stmt.each do |result|
row = {}
col_count.times{|i| row[cols[i]] = result[i]}
yield row
end
end
end

private

# Quote the string using the adapter instance method.
def literal_string(v)
"#{db.synchronize{|c| c.quote(v)}}"
end
end
end
end
7 changes: 4 additions & 3 deletions lib/sequel/adapters/sqlite.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module Sequel
# Top level module for holding all SQLite-related modules and classes
# for Sequel.
module SQLite
# Database class for PostgreSQL databases used with Sequel and the
# Database class for SQLite databases used with Sequel and the
# ruby-sqlite3 driver.
class Database < Sequel::Database
UNIX_EPOCH_TIME_FORMAT = /\A\d+\z/.freeze
Expand Down Expand Up @@ -106,7 +106,7 @@ def transaction(opts={})
private

# Log the SQL and the arguments, and yield an available connection. Rescue
# any SQLite3::Exceptions and turn the into DatabaseErrors.
# any SQLite3::Exceptions and turn them into DatabaseErrors.
def _execute(sql, opts)
begin
log_info(sql, opts[:arguments])
Expand All @@ -116,7 +116,7 @@ def _execute(sql, opts)
end
end

# SQLite does not need the pool to convert exceptions.
# The SQLite adapter does not need the pool to convert exceptions.
# Also, force the max connections to 1 if a memory database is being
# used, as otherwise each connection gets a separate database.
def connection_pool_default_options
Expand Down Expand Up @@ -226,6 +226,7 @@ def prepare(type, name=nil, values=nil)

private

# Quote the string using the adapter class method.
def literal_string(v)
"'#{::SQLite3::Database.quote(v)}'"
end
Expand Down
2 changes: 1 addition & 1 deletion spec/integration/schema_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@
INTEGRATION_DB.alter_table(:items){add_column :name, :text}
INTEGRATION_DB.schema(:items, :reload=>true).map{|x| x.first}.should == [:number, :name]
@ds.columns!.should == [:number, :name]
unless INTEGRATION_DB.url =~ /sqlite/
unless INTEGRATION_DB.url =~ /sqlite|amalgalite/
INTEGRATION_DB.alter_table(:items){add_primary_key :id}
INTEGRATION_DB.schema(:items, :reload=>true).map{|x| x.first}.should == [:number, :name, :id]
@ds.columns!.should == [:number, :name, :id]
Expand Down

0 comments on commit 8398b5b

Please sign in to comment.