-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
26ead69
commit 8398b5b
Showing
5 changed files
with
213 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters