/Support/vendor/rails/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
Ruby | 462 lines | 366 code | 75 blank | 21 comment | 38 complexity | 650d9997d05471e912bc41432b9523e9 MD5 | raw file
- require 'active_record/connection_adapters/abstract_adapter'
- require 'active_support/core_ext/kernel/requires'
- module ActiveRecord
- class Base
- class << self
- # Establishes a connection to the database that's used by all Active Record objects
- def sqlite_connection(config) # :nodoc:
- parse_sqlite_config!(config)
- unless self.class.const_defined?(:SQLite)
- require_library_or_gem(config[:adapter])
- db = SQLite::Database.new(config[:database], 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::SQLite2Adapter.new(db, logger, config)
- else
- ConnectionAdapters::DeprecatedSQLiteAdapter.new(db, logger, config)
- end
- end
- end
- private
- def parse_sqlite_config!(config)
- # Require database.
- unless config[:database]
- raise ArgumentError, "No database file specified. Missing argument: database"
- end
- # Allow database path relative to RAILS_ROOT, but only if
- # the database path is not the special path that tells
- # Sqlite to build a database only in memory.
- if Object.const_defined?(:RAILS_ROOT) && ':memory:' != config[:database]
- config[:database] = File.expand_path(config[:database], RAILS_ROOT)
- end
- end
- end
- end
- module ConnectionAdapters #:nodoc:
- class SQLiteColumn < Column #:nodoc:
- class << self
- def string_to_binary(value)
- value.gsub(/\0|\%/n) do |b|
- case b
- when "\0" then "%00"
- when "%" then "%25"
- end
- end
- end
- def binary_to_string(value)
- value.gsub(/%00|%25/n) do |b|
- case b
- when "%00" then "\0"
- when "%25" then "%"
- end
- end
- end
- end
- end
- # The SQLite adapter works with both the 2.x and 3.x series of SQLite with the sqlite-ruby drivers (available both as gems and
- # from http://rubyforge.org/projects/sqlite-ruby/).
- #
- # Options:
- #
- # * <tt>:database</tt> - Path to the database file.
- class SQLiteAdapter < AbstractAdapter
- class Version
- include Comparable
- def initialize(version_string)
- @version = version_string.split('.').map(&:to_i)
- end
- def <=>(version_string)
- @version <=> version_string.split('.').map(&:to_i)
- end
- end
- def initialize(connection, logger, config)
- super(connection, logger)
- @config = config
- end
- def adapter_name #:nodoc:
- 'SQLite'
- end
- def supports_ddl_transactions?
- sqlite_version >= '2.0.0'
- end
- def supports_migrations? #:nodoc:
- true
- end
- def supports_primary_key? #:nodoc:
- true
- end
- def requires_reloading?
- true
- end
- def supports_add_column?
- sqlite_version >= '3.1.6'
- end
-
- def disconnect!
- super
- @connection.close rescue nil
- end
- def supports_count_distinct? #:nodoc:
- sqlite_version >= '3.2.6'
- end
- def supports_autoincrement? #:nodoc:
- sqlite_version >= '3.1.0'
- end
- def native_database_types #:nodoc:
- {
- :primary_key => default_primary_key_type,
- :string => { :name => "varchar", :limit => 255 },
- :text => { :name => "text" },
- :integer => { :name => "integer" },
- :float => { :name => "float" },
- :decimal => { :name => "decimal" },
- :datetime => { :name => "datetime" },
- :timestamp => { :name => "datetime" },
- :time => { :name => "time" },
- :date => { :name => "date" },
- :binary => { :name => "blob" },
- :boolean => { :name => "boolean" }
- }
- end
- # QUOTING ==================================================
- def quote_string(s) #:nodoc:
- @connection.class.quote(s)
- end
- def quote_column_name(name) #:nodoc:
- %Q("#{name}")
- end
- # Quote date/time values for use in SQL input. Includes microseconds
- # if the value is a Time responding to usec.
- def quoted_date(value) #:nodoc:
- if value.acts_like?(:time) && value.respond_to?(:usec)
- "#{super}.#{sprintf("%06d", value.usec)}"
- else
- super
- end
- end
- # DATABASE STATEMENTS ======================================
- def execute(sql, name = nil) #:nodoc:
- catch_schema_changes { log(sql, name) { @connection.execute(sql) } }
- end
- def update_sql(sql, name = nil) #:nodoc:
- super
- @connection.changes
- end
- def delete_sql(sql, name = nil) #:nodoc:
- sql += " WHERE 1=1" unless sql =~ /WHERE/i
- super sql, name
- end
- def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
- super || @connection.last_insert_row_id
- end
- def select_rows(sql, name = nil)
- execute(sql, name).map do |row|
- (0...(row.size / 2)).map { |i| row[i] }
- end
- end
- def begin_db_transaction #:nodoc:
- catch_schema_changes { @connection.transaction }
- end
- def commit_db_transaction #:nodoc:
- catch_schema_changes { @connection.commit }
- end
- def rollback_db_transaction #:nodoc:
- catch_schema_changes { @connection.rollback }
- end
- # SELECT ... FOR UPDATE is redundant since the table is locked.
- def add_lock!(sql, options) #:nodoc:
- sql
- end
- # SCHEMA STATEMENTS ========================================
- def tables(name = nil) #:nodoc:
- sql = <<-SQL
- SELECT name
- FROM sqlite_master
- WHERE type = 'table' AND NOT name = 'sqlite_sequence'
- SQL
- execute(sql, name).map do |row|
- row[0]
- end
- end
- def columns(table_name, name = nil) #:nodoc:
- table_structure(table_name).map do |field|
- SQLiteColumn.new(field['name'], field['dflt_value'], field['type'], field['notnull'] == "0")
- end
- end
- def indexes(table_name, name = nil) #:nodoc:
- execute("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row|
- index = IndexDefinition.new(table_name, row['name'])
- index.unique = row['unique'] != '0'
- index.columns = execute("PRAGMA index_info('#{index.name}')").map { |col| col['name'] }
- index
- end
- end
- def primary_key(table_name) #:nodoc:
- column = table_structure(table_name).find {|field| field['pk'].to_i == 1}
- column ? column['name'] : nil
- end
- def remove_index(table_name, options={}) #:nodoc:
- execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}"
- end
- def rename_table(name, new_name)
- execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
- end
- # See: http://www.sqlite.org/lang_altertable.html
- # SQLite has an additional restriction on the ALTER TABLE statement
- def valid_alter_table_options( type, options)
- type.to_sym != :primary_key
- end
- def add_column(table_name, column_name, type, options = {}) #:nodoc:
- if supports_add_column? && valid_alter_table_options( type, options )
- super(table_name, column_name, type, options)
- else
- alter_table(table_name) do |definition|
- definition.column(column_name, type, options)
- end
- end
- end
- def remove_column(table_name, *column_names) #:nodoc:
- column_names.flatten.each do |column_name|
- alter_table(table_name) do |definition|
- definition.columns.delete(definition[column_name])
- end
- end
- end
- alias :remove_columns :remove_column
- def change_column_default(table_name, column_name, default) #:nodoc:
- alter_table(table_name) do |definition|
- definition[column_name].default = default
- end
- end
- def change_column_null(table_name, column_name, null, default = nil)
- unless null || default.nil?
- execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
- end
- alter_table(table_name) do |definition|
- definition[column_name].null = null
- end
- end
- def change_column(table_name, column_name, type, options = {}) #:nodoc:
- alter_table(table_name) do |definition|
- include_default = options_include_default?(options)
- definition[column_name].instance_eval do
- self.type = type
- self.limit = options[:limit] if options.include?(:limit)
- self.default = options[:default] if include_default
- self.null = options[:null] if options.include?(:null)
- end
- end
- end
- def rename_column(table_name, column_name, new_column_name) #:nodoc:
- unless columns(table_name).detect{|c| c.name == column_name.to_s }
- raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}"
- end
- alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s})
- end
- def empty_insert_statement(table_name)
- "INSERT INTO #{table_name} VALUES(NULL)"
- end
- protected
- def select(sql, name = nil) #:nodoc:
- execute(sql, name).map do |row|
- record = {}
- row.each_key do |key|
- if key.is_a?(String)
- record[key.sub(/^"?\w+"?\./, '')] = row[key]
- end
- end
- record
- end
- end
- def table_structure(table_name)
- returning structure = execute("PRAGMA table_info(#{quote_table_name(table_name)})") do
- raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
- end
- end
- def alter_table(table_name, options = {}) #:nodoc:
- altered_table_name = "altered_#{table_name}"
- caller = lambda {|definition| yield definition if block_given?}
- transaction do
- move_table(table_name, altered_table_name,
- options.merge(:temporary => true))
- move_table(altered_table_name, table_name, &caller)
- end
- end
- def move_table(from, to, options = {}, &block) #:nodoc:
- copy_table(from, to, options, &block)
- drop_table(from)
- end
- def copy_table(from, to, options = {}) #:nodoc:
- options = options.merge(:id => (!columns(from).detect{|c| c.name == 'id'}.nil? && 'id' == primary_key(from).to_s))
- create_table(to, options) do |definition|
- @definition = definition
- columns(from).each do |column|
- column_name = options[:rename] ?
- (options[:rename][column.name] ||
- options[:rename][column.name.to_sym] ||
- column.name) : column.name
-
- @definition.column(column_name, column.type,
- :limit => column.limit, :default => column.default,
- :null => column.null)
- end
- @definition.primary_key(primary_key(from)) if primary_key(from)
- yield @definition if block_given?
- end
- copy_table_indexes(from, to, options[:rename] || {})
- copy_table_contents(from, to,
- @definition.columns.map {|column| column.name},
- options[:rename] || {})
- end
- def copy_table_indexes(from, to, rename = {}) #:nodoc:
- indexes(from).each do |index|
- name = index.name
- if to == "altered_#{from}"
- name = "temp_#{name}"
- elsif from == "altered_#{to}"
- name = name[5..-1]
- end
- to_column_names = columns(to).map(&:name)
- columns = index.columns.map {|c| rename[c] || c }.select do |column|
- to_column_names.include?(column)
- end
- unless columns.empty?
- # index name can't be the same
- opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") }
- opts[:unique] = true if index.unique
- add_index(to, columns, opts)
- end
- end
- end
- def copy_table_contents(from, to, columns, rename = {}) #:nodoc:
- column_mappings = Hash[*columns.map {|name| [name, name]}.flatten]
- rename.inject(column_mappings) {|map, a| map[a.last] = a.first; map}
- from_columns = columns(from).collect {|col| col.name}
- columns = columns.find_all{|col| from_columns.include?(column_mappings[col])}
- quoted_columns = columns.map { |col| quote_column_name(col) } * ','
- quoted_to = quote_table_name(to)
- @connection.execute "SELECT * FROM #{quote_table_name(from)}" do |row|
- sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES ("
- sql << columns.map {|col| quote row[column_mappings[col]]} * ', '
- sql << ')'
- @connection.execute sql
- end
- end
- def catch_schema_changes
- return yield
- rescue ActiveRecord::StatementInvalid => exception
- if exception.message =~ /database schema has changed/
- reconnect!
- retry
- else
- raise
- end
- end
- def sqlite_version
- @sqlite_version ||= SQLiteAdapter::Version.new(select_value('select sqlite_version(*)'))
- end
- def default_primary_key_type
- if supports_autoincrement?
- 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL'.freeze
- else
- 'INTEGER PRIMARY KEY NOT NULL'.freeze
- end
- end
- def translate_exception(exception, message)
- case exception.message
- when /column(s)? .* (is|are) not unique/
- RecordNotUnique.new(message, exception)
- else
- super
- end
- end
- end
- class SQLite2Adapter < SQLiteAdapter # :nodoc:
- def rename_table(name, new_name)
- move_table(name, new_name)
- end
- end
- class DeprecatedSQLiteAdapter < SQLite2Adapter # :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