diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 1e1c5ee1a9..8bc77eca5b 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,7 @@ *SVN* +* Added SQLite3 compatibility through the sqlite3-ruby adapter by Jamis Buck #381 [bitsweat] + * Added support for the new protocol spoken by MySQL 4.1.1+ servers for the Ruby/MySQL adapter that ships with Rails #440 [Matt Mower] * Added that Observers can use the observes class method instead of overwriting self.observed_class(). diff --git a/activerecord/Rakefile b/activerecord/Rakefile index d5a2fbfada..5edd39c0ad 100755 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -19,7 +19,7 @@ PKG_FILES = FileList[ desc "Default Task" -task :default => [ :test_ruby_mysql, :test_mysql_ruby, :test_sqlite, :test_postgresql ] +task :default => [ :test_ruby_mysql, :test_mysql_ruby, :test_sqlite, :test_sqlite3, :test_postgresql ] # Run the unit tests @@ -47,6 +47,12 @@ Rake::TestTask.new("test_sqlite") { |t| t.verbose = true } +Rake::TestTask.new("test_sqlite3") { |t| + t.libs << "test" << "test/connections/native_sqlite3" + t.pattern = 'test/*_test.rb' + t.verbose = true +} + Rake::TestTask.new("test_sqlserver") { |t| t.libs << "test" << "test/connections/native_sqlserver" t.pattern = 'test/*_test.rb' @@ -101,6 +107,9 @@ spec = Gem::Specification.new do |s| s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) } end s.files.delete "test/fixtures/fixture_database.sqlite" + s.files.delete "test/fixtures/fixture_database_2.sqlite" + s.files.delete "test/fixtures/fixture_database.sqlite3" + s.files.delete "test/fixtures/fixture_database_2.sqlite3" s.require_path = 'lib' s.autorequire = 'active_record' diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 7a7b39890a..0b7a6b4c76 100755 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -24,7 +24,7 @@ $:.unshift(File.dirname(__FILE__)) -require 'action_controller/support/core_ext' +require 'active_record/support/core_ext' require 'active_record/support/clean_logger' require 'active_record/support/misc' require 'active_record/support/dependencies' diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 547c98911a..9eaac85571 100755 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -89,7 +89,7 @@ module ActiveRecord raise AdapterNotSpecified, "#{spec} database is not configured" end else - spec = symbolize_strings_in_hash(spec) + spec = spec.symbolize_keys unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end adapter_method = "#{spec[:adapter]}_connection" unless respond_to?(adapter_method) then raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter" end @@ -152,10 +152,7 @@ module ActiveRecord # Converts all strings in a hash to symbols. def self.symbolize_strings_in_hash(hash) - hash.inject({}) do |hash_with_symbolized_strings, pair| - hash_with_symbolized_strings[pair.first.to_sym] = pair.last - hash_with_symbolized_strings - end + hash.symbolize_keys end end @@ -356,7 +353,7 @@ module ActiveRecord end def quote_column_name(name) - return name + name end # Returns a string of the CREATE TABLE SQL statements for recreating the entire structure of the database. @@ -367,16 +364,20 @@ module ActiveRecord end protected - def log(sql, name, connection, &action) + def log(sql, name, connection = nil) + connection ||= @connection begin if @logger.nil? || @logger.level > Logger::INFO - action.call(connection) - else + yield connection + elsif block_given? result = nil - bm = measure { result = action.call(connection) } + bm = measure { result = yield connection } @runtime += bm.real log_info(sql, name, bm.real) result + else + log_info(sql, name, 0) + nil end rescue => e log_info("#{e.message}: #{sql}", name, 0) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 4dc8d634b3..b8a91929c7 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -1,33 +1,67 @@ # sqlite_adapter.rb -# author: Luke Holden +# author: Luke Holden +# updated for SQLite3: Jamis Buck require 'active_record/connection_adapters/abstract_adapter' module ActiveRecord class Base - # Establishes a connection to the database that's used by all Active Record objects - def self.sqlite_connection(config) # :nodoc: - require_library_or_gem('sqlite') unless self.class.const_defined?(:SQLite) - symbolize_strings_in_hash(config) - unless config.has_key?(:dbfile) - raise ArgumentError, "No database file specified. Missing argument: dbfile" + class << self + # sqlite3 adapter reuses sqlite_connection. + def sqlite3_connection(config) # :nodoc: + parse_config!(config) + + unless self.class.const_defined?(:SQLite3) + require_library_or_gem(config[:adapter]) + end + + db = SQLite3::Database.new( + config[:dbfile], + :results_as_hash => true, + :type_translation => false + ) + ConnectionAdapters::SQLiteAdapter.new(db, logger) end - - config[:dbfile] = File.expand_path(config[:dbfile], RAILS_ROOT) if Object.const_defined?(:RAILS_ROOT) - db = SQLite::Database.new(config[:dbfile], 0) - db.show_datatypes = "ON" if !defined? SQLite::Version - db.results_as_hash = true if defined? SQLite::Version - db.type_translation = false + # Establishes a connection to the database that's used by all Active Record objects + def sqlite_connection(config) # :nodoc: + parse_config!(config) - ConnectionAdapters::SQLiteAdapter.new(db, logger) + unless self.class.const_defined?(:SQLite) + require_library_or_gem(config[:adapter]) + + db = SQLite::Database.new(config[:dbfile], 0) + db.show_datatypes = "ON" if !defined? SQLite::Version + db.results_as_hash = true if defined? SQLite::Version + db.type_translation = false + + # "Downgrade" deprecated sqlite API + if SQLite.const_defined?(:Version) + ConnectionAdapters::SQLiteAdapter.new(db, logger) + else + ConnectionAdapters::DeprecatedSQLiteAdapter.new(db, logger) + end + end + end + + private + def parse_config!(config) + # Require dbfile. + unless config.has_key?(:dbfile) + raise ArgumentError, "No database file specified. Missing argument: dbfile" + end + + # Allow database path relative to RAILS_ROOT. + if Object.const_defined?(:RAILS_ROOT) + config[:dbfile] = File.expand_path(config[:dbfile], RAILS_ROOT) + end + end end end module ConnectionAdapters class SQLiteColumn < Column - def string_to_binary(value) value.gsub(/(\0|\%)/) do case $1 @@ -45,92 +79,79 @@ module ActiveRecord end end end - end + class SQLiteAdapter < AbstractAdapter # :nodoc: - def select_all(sql, name = nil) - select(sql, name) - end - - def select_one(sql, name = nil) - result = select(sql, name) - result.nil? ? nil : result.first - end - - def columns(table_name, name = nil) - table_structure(table_name).inject([]) do |columns, field| - columns << SQLiteColumn.new(field['name'], field['dflt_value'], field['type']) - columns - end - end - - def insert(sql, name = nil, pk = nil, id_value = nil) - execute(sql, name = nil) - id_value || @connection.send( defined?( SQLite::Version ) ? :last_insert_row_id : :last_insert_rowid ) - end - def execute(sql, name = nil) - log(sql, name, @connection) do |connection| - if defined?( SQLite::Version ) - case sql - when "BEGIN" then connection.transaction - when "COMMIT" then connection.commit - when "ROLLBACK" then connection.rollback - else connection.execute(sql) - end - else - connection.execute( sql ) - end - end + log(sql, name) { @connection.execute(sql) } end def update(sql, name = nil) execute(sql, name) @connection.changes end - + def delete(sql, name = nil) sql += " WHERE 1=1" unless sql =~ /WHERE/i execute(sql, name) @connection.changes end - def begin_db_transaction() execute "BEGIN" end - def commit_db_transaction() execute "COMMIT" end - def rollback_db_transaction() execute "ROLLBACK" end + def insert(sql, name = nil, pk = nil, id_value = nil) + execute(sql, name = nil) + id_value || @connection.last_insert_row_id + end + + def select_all(sql, name = nil) + execute(sql, name).map do |row| + record = {} + row.each_key do |key| + record[key.sub(/\w+\./, '')] = row[key] unless key.is_a?(Fixnum) + end + record + end + end + + def select_one(sql, name = nil) + result = select_all(sql, name) + result.nil? ? nil : result.first + end + + + def begin_db_transaction() @connection.transaction end + def commit_db_transaction() @connection.commit end + def rollback_db_transaction() @connection.rollback end + + + def tables + execute('.table').map { |table| Table.new(table) } + end + + def columns(table_name, name = nil) + table_structure(table_name).map { |field| + SQLiteColumn.new(field['name'], field['dflt_value'], field['type']) + } + end def quote_string(s) - SQLite::Database.quote(s) + @connection.class.quote(s) end - + def quote_column_name(name) return "'#{name}'" end - private - def select(sql, name = nil) - results = nil - log(sql, name, @connection) { |connection| results = connection.execute(sql) } - - rows = [] - - results.each do |row| - hash_only_row = {} - row.each_key do |key| - hash_only_row[key.sub(/\w+\./, "")] = row[key] unless key.class == Fixnum - end - rows << hash_only_row - end - - return rows - end - + protected def table_structure(table_name) - sql = "PRAGMA table_info(#{table_name});" - results = nil - log(sql, nil, @connection) { |connection| results = connection.execute(sql) } - return results + execute "PRAGMA table_info(#{table_name})" end end + + class DeprecatedSQLiteAdapter < SQLiteAdapter # :nodoc: + def insert(sql, name = nil, pk = nil, id_value = nil) + execute(sql, name = nil) + id_value || @connection.last_insert_rowid + end + end end end diff --git a/activerecord/test/abstract_unit.rb b/activerecord/test/abstract_unit.rb index 881657fb4a..5433b306f9 100755 --- a/activerecord/test/abstract_unit.rb +++ b/activerecord/test/abstract_unit.rb @@ -17,4 +17,4 @@ class Test::Unit::TestCase #:nodoc: end end -Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/" \ No newline at end of file +Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/" diff --git a/activerecord/test/connections/native_sqlite3/connection.rb b/activerecord/test/connections/native_sqlite3/connection.rb new file mode 100644 index 0000000000..573d942716 --- /dev/null +++ b/activerecord/test/connections/native_sqlite3/connection.rb @@ -0,0 +1,34 @@ +print "Using native SQLite3\n" +require 'fixtures/course' +require 'logger' +ActiveRecord::Base.logger = Logger.new("debug.log") +ActiveRecord::Base.logger.level = Logger::DEBUG + +BASE_DIR = File.expand_path(File.dirname(__FILE__) + '/../../fixtures') +sqlite_test_db = "#{BASE_DIR}/fixture_database.sqlite3" +sqlite_test_db2 = "#{BASE_DIR}/fixture_database_2.sqlite3" + +def make_connection(clazz, db_file, db_definitions_file) + unless File.exist?(db_file) + puts "SQLite3 database not found at #{db_file}. Rebuilding it." + sqlite_command = "sqlite3 #{db_file} 'create table a (a integer); drop table a;'" + puts "Executing '#{sqlite_command}'" + `#{sqlite_command}` + clazz.establish_connection( + :adapter => "sqlite3", + :dbfile => db_file) + script = File.read("#{BASE_DIR}/db_definitions/#{db_definitions_file}") + # SQLite-Ruby has problems with semi-colon separated commands, so split and execute one at a time + script.split(';').each do + |command| + clazz.connection.execute(command) unless command.strip.empty? + end + else + clazz.establish_connection( + :adapter => "sqlite3", + :dbfile => db_file) + end +end + +make_connection(ActiveRecord::Base, sqlite_test_db, 'sqlite.sql') +make_connection(Course, sqlite_test_db2, 'sqlite2.sql')