aboutsummaryrefslogblamecommitdiffstats
path: root/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
blob: c0cc072bfe606a3f488faf4bfb12969a88a986a7 (plain) (tree)
1
2
3
4
5
6
7
8



                                                            
                 

                                                                                         
                                    



                                                  
                                                         





                                                                 
                                                              



                                                                       
         
 
             
                                        

                                               
                                  
                                                                                         
             
 

                                                                   
                                                      
                                                                                  
                                                                               

             


       

                                        

                                   
                                     



                                  
             
           
 
                                   
                                       



                                  
             
           
         
       
 




                                                                                                                                  
                                                        
                                         






                                       
 
                             

            




                                    
 
                                           
                                 
         
 



                                          
                                        
         
                                                   



                                                                
                                                 




                                                  
                                                


         







                                                                  
                     

         



                                                                  
                                                                            

         

                                              
                           

         
                                              
                                                  
                       

         

                                                                                             
         
 




                                                 
 
                                       
                                                        
         
 
                                        
                                                   


                                          
                                                     
         
 
 





                                                                     


                                                                  






                                                               

                
         
 
                                                  
                                                  
                                                                                                      
           
         
 
                                                  
                                                                                        

                                                              
                                                                                                 



               
                                          



                                                                                 
                                                       
                                                                                  
         
 
                                      
                                                           
         
 
                                                                          
                                                     

                                                                          
         
 
                                                         



                                                            
 
                                                                          




                                                   
                                                                             
                                               
                                                             

                                                  

                                                                      
                                                                    



             
                                                                          
                                                                                      
         
 


                                                
 
               











                                                        
                                       
                                                                                                
                                                                                                             

             
 




                                                                         
                                                      
                                                


                                                               
 



                                                               
 
                                                       
                                                                                        

                                                   
                                          



                                                        
              
                                                          

                                                                   
               
                                                                           

                                             
 
                                                              

                                                           

                                   
 
                                                              
                                       





                                         
 










                                                                                

             
 


                                                                              

                                                                                       

                                                                             


                                                                                



                                                                             
           
 









                                                               



                                                                      

                                    
                                    




                                                               
       
 
                                                  

                                           
         
 


                                      
 




                                                                          
       
 
                                                            




                                                           
     
   
require 'active_record/connection_adapters/abstract_adapter'

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)
          else
            ConnectionAdapters::DeprecatedSQLiteAdapter.new(db, logger)
          end
        end
      end

      private
        def parse_sqlite_config!(config)
          config[:database] ||= config[:dbfile]
          # 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
      def adapter_name #:nodoc:
        'SQLite'
      end

      def supports_migrations? #:nodoc:
        true
      end

      def requires_reloading?
        true
      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 => "datetime" },
          :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


      # 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 #{name} RENAME TO #{new_name}"
      end

      def add_column(table_name, column_name, type, options = {}) #:nodoc:
        super(table_name, column_name, type, options)
        # See last paragraph on http://www.sqlite.org/lang_altertable.html
        execute "VACUUM"
      end

      def remove_column(table_name, column_name) #:nodoc:
        alter_table(table_name) do |definition|
          definition.columns.delete(definition[column_name])
        end
      end

      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(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:
        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?)
          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 ||= 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
    end

    class SQLite2Adapter < SQLiteAdapter # :nodoc:
      def supports_count_distinct? #:nodoc:
        false
      end

      def rename_table(name, new_name)
        move_table(name, new_name)
      end

      def add_column(table_name, column_name, type, options = {}) #:nodoc:
        alter_table(table_name) do |definition|
          definition.column(column_name, type, options)
        end
      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