diff options
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters/abstract')
8 files changed, 274 insertions, 409 deletions
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 02a8f4e214..4297c26413 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -1,3 +1,4 @@ +require 'thread' require 'monitor' require 'set' require 'active_support/core_ext/module/synchronization' @@ -56,7 +57,9 @@ module ActiveRecord # * +wait_timeout+: number of seconds to block and wait for a connection # before giving up and raising a timeout error (default 5 seconds). class ConnectionPool + attr_accessor :automatic_reconnect attr_reader :spec, :connections + attr_reader :columns, :columns_hash, :primary_keys, :tables # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification # object which describes database connection information (e.g. adapter, @@ -73,18 +76,70 @@ module ActiveRecord # The mutex used to synchronize pool access @connection_mutex = Monitor.new @queue = @connection_mutex.new_cond - - # default 5 second timeout unless on ruby 1.9 - @timeout = - if RUBY_VERSION < '1.9' - spec.config[:wait_timeout] || 5 - end + @timeout = spec.config[:wait_timeout] || 5 # default max pool size to 5 @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5 @connections = [] @checked_out = [] + @automatic_reconnect = true + @tables = {} + + @columns = Hash.new do |h, table_name| + h[table_name] = with_connection do |conn| + + # Fetch a list of columns + conn.columns(table_name, "#{table_name} Columns").tap do |columns| + + # set primary key information + columns.each do |column| + column.primary = column.name == primary_keys[table_name] + end + end + end + end + + @columns_hash = Hash.new do |h, table_name| + h[table_name] = Hash[columns[table_name].map { |col| + [col.name, col] + }] + end + + @primary_keys = Hash.new do |h, table_name| + h[table_name] = with_connection do |conn| + table_exists?(table_name) ? conn.primary_key(table_name) : 'id' + end + end + end + + # A cached lookup for table existence + def table_exists?(name) + return true if @tables.key? name + + with_connection do |conn| + conn.tables.each { |table| @tables[table] = true } + end + + @tables.key? name + end + + # Clears out internal caches: + # + # * columns + # * columns_hash + # * tables + def clear_cache! + @columns.clear + @columns_hash.clear + @tables.clear + end + + # Clear out internal caches for table with +table_name+ + def clear_table_cache!(table_name) + @columns.delete table_name + @columns_hash.delete table_name + @primary_keys.delete table_name end # Retrieve the connection associated with the current thread, or call @@ -93,11 +148,7 @@ module ActiveRecord # #connection can be called any number of times; the connection is # held in a hash keyed by the thread id. def connection - if conn = @reserved_connections[current_connection_id] - conn - else - @reserved_connections[current_connection_id] = checkout - end + @reserved_connections[current_connection_id] ||= checkout end # Signal that the thread is finished with the current connection. @@ -109,7 +160,7 @@ module ActiveRecord end # If a connection already exists yield it to the block. If no connection - # exists checkout a connection, yield it to the block, and checkin the + # exists checkout a connection, yield it to the block, and checkin the # connection when finished. def with_connection connection_id = current_connection_id @@ -165,7 +216,6 @@ module ActiveRecord keys = @reserved_connections.keys - Thread.list.find_all { |t| t.alive? }.map { |thread| thread.object_id } - keys.each do |key| checkin @reserved_connections[key] @reserved_connections.delete(key) @@ -198,16 +248,18 @@ module ActiveRecord checkout_new_connection end return conn if conn - # No connections available; wait for one - if @queue.wait(@timeout) + + @queue.wait(@timeout) + + if(@checked_out.size < @connections.size) next else - # try looting dead threads clear_stale_cached_connections! if @size == @checked_out.size raise ConnectionTimeoutError, "could not obtain a database connection#{" within #{@timeout} seconds" if @timeout}. The max pool size is currently #{@size}; consider increasing it." end end + end end end @@ -219,7 +271,7 @@ module ActiveRecord # calling +checkout+ on this pool. def checkin(conn) @connection_mutex.synchronize do - conn.send(:_run_checkin_callbacks) do + conn.run_callbacks :checkin do @checked_out.delete conn @queue.signal end @@ -239,6 +291,8 @@ module ActiveRecord end def checkout_new_connection + raise ConnectionNotEstablished unless @automatic_reconnect + c = new_connection @connections << c checkout_and_verify(c) @@ -326,7 +380,7 @@ module ActiveRecord # already been opened. def connected?(klass) conn = retrieve_connection_pool(klass) - conn ? conn.connected? : false + conn && conn.connected? end # Remove the connection for this class. This will close the active @@ -337,7 +391,7 @@ module ActiveRecord pool = @connection_pools[klass.name] return nil unless pool - @connection_pools.delete_if { |key, value| value == pool } + pool.automatic_reconnect = false pool.disconnect! pool.spec.config end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb index 8e74eff0ab..d88720c8bf 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb @@ -67,12 +67,12 @@ module ActiveRecord begin require "active_record/connection_adapters/#{spec[:adapter]}_adapter" - rescue LoadError - raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{$!})" + rescue LoadError => e + raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e})" end adapter_method = "#{spec[:adapter]}_connection" - if !respond_to?(adapter_method) + unless respond_to?(adapter_method) raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter" end @@ -89,6 +89,16 @@ module ActiveRecord retrieve_connection end + # Returns the configuration of the associated connection as a hash: + # + # ActiveRecord::Base.connection_config + # # => {:pool=>5, :timeout=>5000, :database=>"db/development.sqlite3", :adapter=>"sqlite3"} + # + # Please use only for reading. + def connection_config + connection_pool.spec.config + end + def connection_pool connection_handler.retrieve_connection_pool(self) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb index a130c330dd..29ac9341ec 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb @@ -37,9 +37,9 @@ module ActiveRecord 16 end - # the maximum number of elements in an IN (x,y,z) clause + # the maximum number of elements in an IN (x,y,z) clause. nil means no limit def in_clause_length - 65535 + nil end # the maximum length of an SQL query diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 25432e9985..a3082b8f01 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -1,10 +1,20 @@ +require 'active_support/core_ext/module/deprecation' + module ActiveRecord module ConnectionAdapters # :nodoc: module DatabaseStatements # Returns an array of record hashes with the column names as keys and # column values as values. - def select_all(sql, name = nil) - select(sql, name) + def select_all(sql, name = nil, binds = []) + if supports_statement_cache? + select(sql, name, binds) + else + return select(sql, name) if binds.empty? + binds = binds.dup + select sql.gsub('?') { + quote(*binds.shift.reverse) + }, name + end end # Returns a record hash with the column names as keys and column values @@ -35,10 +45,16 @@ module ActiveRecord undef_method :select_rows # Executes the SQL statement in the context of this connection. - def execute(sql, name = nil, skip_logging = false) + def execute(sql, name = nil) end undef_method :execute + # Executes +sql+ statement in the context of this connection using + # +binds+ as the bind substitutes. +name+ is logged along with + # the executed +sql+ statement. + def exec_query(sql, name = 'SQL', binds = []) + end + # Returns the last auto-generated ID from the affected table. def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) insert_sql(sql, name, pk, id_value, sequence_name) @@ -68,6 +84,12 @@ module ActiveRecord nil end + # Returns +true+ when the connection adapter supports prepared statement + # caching, otherwise returns +false+ + def supports_statement_cache? + false + end + # Runs the given block in a database transaction, and returns the result # of the block. # @@ -209,11 +231,12 @@ module ActiveRecord # # This method *modifies* the +sql+ parameter. # + # This method is deprecated!! Stop using it! + # # ===== Examples # add_limit_offset!('SELECT * FROM suppliers', {:limit => 10, :offset => 50}) # generates # SELECT * FROM suppliers LIMIT 10 OFFSET 50 - def add_limit_offset!(sql, options) if limit = options[:limit] sql << " LIMIT #{sanitize_limit(limit)}" @@ -223,6 +246,7 @@ module ActiveRecord end sql end + deprecate :add_limit_offset! def default_sequence_name(table, column) nil @@ -236,7 +260,19 @@ module ActiveRecord # Inserts the given fixture into the table. Overridden in adapters that require # something beyond a simple insert (eg. Oracle). def insert_fixture(fixture, table_name) - execute "INSERT INTO #{quote_table_name(table_name)} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert' + columns = Hash[columns(table_name).map { |c| [c.name, c] }] + + key_list = [] + value_list = fixture.map do |name, value| + key_list << quote_column_name(name) + quote(value, columns[name]) + end + + execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", 'Fixture Insert' + end + + def null_insert_value + Arel.sql 'DEFAULT' end def empty_insert_statement_value @@ -251,10 +287,29 @@ module ActiveRecord "WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})" end + # Sanitizes the given LIMIT parameter in order to prevent SQL injection. + # + # The +limit+ may be anything that can evaluate to a string via #to_s. It + # should look like an integer, or a comma-delimited list of integers, or + # an Arel SQL literal. + # + # Returns Integer and Arel::Nodes::SqlLiteral limits as is. + # Returns the sanitized limit parameter, either as an integer, or as a + # string which contains a comma-delimited list of integers. + def sanitize_limit(limit) + if limit.is_a?(Integer) || limit.is_a?(Arel::Nodes::SqlLiteral) + limit + elsif limit.to_s =~ /,/ + Arel.sql limit.to_s.split(',').map{ |i| Integer(i) }.join(',') + else + Integer(limit) + end + end + protected # Returns an array of record hashes with the column names as keys and # column values as values. - def select(sql, name = nil) + def select(sql, name = nil, binds = []) end undef_method :select @@ -274,21 +329,6 @@ module ActiveRecord update_sql(sql, name) end - # Sanitizes the given LIMIT parameter in order to prevent SQL injection. - # - # +limit+ may be anything that can evaluate to a string via #to_s. It - # should look like an integer, or a comma-delimited list of integers. - # - # Returns the sanitized limit parameter, either as an integer, or as a - # string which contains a comma-delimited list of integers. - def sanitize_limit(limit) - if limit.to_s =~ /,/ - limit.to_s.split(',').map{ |i| i.to_i }.join(',') - else - limit.to_i - end - end - # Send a rollback message to all records after they have been rolled back. If rollback # is false, only rollback records since the last save point. def rollback_transaction_records(rollback) #:nodoc diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 78fffaff6e..1db397f584 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/object/duplicable' - module ActiveRecord module ConnectionAdapters # :nodoc: module QueryCache @@ -49,40 +47,26 @@ module ActiveRecord @query_cache.clear end - def select_all(*args) - if @query_cache_enabled - cache_sql(args.first) { super } - else - super - end - end - - def columns(*) + def select_all(sql, name = nil, binds = []) if @query_cache_enabled - @query_cache["SHOW FIELDS FROM #{args.first}"] ||= super + cache_sql(sql, binds) { super } else super end end private - def cache_sql(sql) + def cache_sql(sql, binds) result = - if @query_cache.has_key?(sql) + if @query_cache[sql].key?(binds) ActiveSupport::Notifications.instrument("sql.active_record", - :sql => sql, :name => "CACHE", :connection_id => self.object_id) - @query_cache[sql] + :sql => sql, :name => "CACHE", :connection_id => object_id) + @query_cache[sql][binds] else - @query_cache[sql] = yield + @query_cache[sql][binds] = yield end - if Array === result - result.collect { |row| row.dup } - else - result.duplicable? ? result.dup : result - end - rescue TypeError - result + result.collect { |row| row.dup } end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index e2b3773a99..7489e88eef 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -10,28 +10,32 @@ module ActiveRecord return value.quoted_id if value.respond_to?(:quoted_id) case value - when String, ActiveSupport::Multibyte::Chars - value = value.to_s - if column && column.type == :binary && column.class.respond_to?(:string_to_binary) - "'#{quote_string(column.class.string_to_binary(value))}'" # ' (for ruby-mode) - elsif column && [:integer, :float].include?(column.type) - value = column.type == :integer ? value.to_i : value.to_f - value.to_s - else - "'#{quote_string(value)}'" # ' (for ruby-mode) - end - when NilClass then "NULL" - when TrueClass then (column && column.type == :integer ? '1' : quoted_true) - when FalseClass then (column && column.type == :integer ? '0' : quoted_false) - when Float, Fixnum, Bignum then value.to_s - # BigDecimals need to be output in a non-normalized form and quoted. - when BigDecimal then value.to_s('F') + when String, ActiveSupport::Multibyte::Chars + value = value.to_s + return "'#{quote_string(value)}'" unless column + + case column.type + when :binary then "'#{quote_string(column.string_to_binary(value))}'" + when :integer then value.to_i.to_s + when :float then value.to_f.to_s + else + "'#{quote_string(value)}'" + end + + when true, false + if column && column.type == :integer + value ? '1' : '0' else - if value.acts_like?(:date) || value.acts_like?(:time) - "'#{quoted_date(value)}'" - else - "'#{quote_string(value.to_s)}'" - end + value ? quoted_true : quoted_false + end + # BigDecimals need to be put in a non-normalized form and quoted. + when nil then "NULL" + when BigDecimal then value.to_s('F') + when Numeric then value.to_s + when Date, Time then "'#{quoted_date(value)}'" + when Symbol then "'#{quote_string(value.to_s)}'" + else + "'#{quote_string(value.to_yaml)}'" end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 9118ceb33c..7ac48c6646 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -6,259 +6,6 @@ require 'bigdecimal/util' module ActiveRecord module ConnectionAdapters #:nodoc: - # An abstract definition of a column in a table. - class Column - TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set - FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set - - module Format - ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/ - ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ - end - - attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale - attr_accessor :primary - - # Instantiates a new column in the table. - # - # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>. - # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>. - # +sql_type+ is used to extract the column's length, if necessary. For example +60+ in - # <tt>company_name varchar(60)</tt>. - # It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute. - # +null+ determines if this column allows +NULL+ values. - def initialize(name, default, sql_type = nil, null = true) - @name, @sql_type, @null = name, sql_type, null - @limit, @precision, @scale = extract_limit(sql_type), extract_precision(sql_type), extract_scale(sql_type) - @type = simplified_type(sql_type) - @default = extract_default(default) - - @primary = nil - end - - # Returns +true+ if the column is either of type string or text. - def text? - type == :string || type == :text - end - - # Returns +true+ if the column is either of type integer, float or decimal. - def number? - type == :integer || type == :float || type == :decimal - end - - def has_default? - !default.nil? - end - - # Returns the Ruby class that corresponds to the abstract data type. - def klass - case type - when :integer then Fixnum - when :float then Float - when :decimal then BigDecimal - when :datetime then Time - when :date then Date - when :timestamp then Time - when :time then Time - when :text, :string then String - when :binary then String - when :boolean then Object - end - end - - # Casts value (which is a String) to an appropriate instance. - def type_cast(value) - return nil if value.nil? - case type - when :string then value - when :text then value - when :integer then value.to_i rescue value ? 1 : 0 - when :float then value.to_f - when :decimal then self.class.value_to_decimal(value) - when :datetime then self.class.string_to_time(value) - when :timestamp then self.class.string_to_time(value) - when :time then self.class.string_to_dummy_time(value) - when :date then self.class.string_to_date(value) - when :binary then self.class.binary_to_string(value) - when :boolean then self.class.value_to_boolean(value) - else value - end - end - - def type_cast_code(var_name) - case type - when :string then nil - when :text then nil - when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)" - when :float then "#{var_name}.to_f" - when :decimal then "#{self.class.name}.value_to_decimal(#{var_name})" - when :datetime then "#{self.class.name}.string_to_time(#{var_name})" - when :timestamp then "#{self.class.name}.string_to_time(#{var_name})" - when :time then "#{self.class.name}.string_to_dummy_time(#{var_name})" - when :date then "#{self.class.name}.string_to_date(#{var_name})" - when :binary then "#{self.class.name}.binary_to_string(#{var_name})" - when :boolean then "#{self.class.name}.value_to_boolean(#{var_name})" - else nil - end - end - - # Returns the human name of the column name. - # - # ===== Examples - # Column.new('sales_stage', ...).human_name # => 'Sales stage' - def human_name - Base.human_attribute_name(@name) - end - - def extract_default(default) - type_cast(default) - end - - class << self - # Used to convert from Strings to BLOBs - def string_to_binary(value) - value - end - - # Used to convert from BLOBs to Strings - def binary_to_string(value) - value - end - - def string_to_date(string) - return string unless string.is_a?(String) - return nil if string.empty? - - fast_string_to_date(string) || fallback_string_to_date(string) - end - - def string_to_time(string) - return string unless string.is_a?(String) - return nil if string.empty? - - fast_string_to_time(string) || fallback_string_to_time(string) - end - - def string_to_dummy_time(string) - return string unless string.is_a?(String) - return nil if string.empty? - - string_to_time "2000-01-01 #{string}" - end - - # convert something to a boolean - def value_to_boolean(value) - if value.is_a?(String) && value.blank? - nil - else - TRUE_VALUES.include?(value) - end - end - - # convert something to a BigDecimal - def value_to_decimal(value) - # Using .class is faster than .is_a? and - # subclasses of BigDecimal will be handled - # in the else clause - if value.class == BigDecimal - value - elsif value.respond_to?(:to_d) - value.to_d - else - value.to_s.to_d - end - end - - protected - # '0.123456' -> 123456 - # '1.123456' -> 123456 - def microseconds(time) - ((time[:sec_fraction].to_f % 1) * 1_000_000).to_i - end - - def new_date(year, mon, mday) - if year && year != 0 - Date.new(year, mon, mday) rescue nil - end - end - - def new_time(year, mon, mday, hour, min, sec, microsec) - # Treat 0000-00-00 00:00:00 as nil. - return nil if year.nil? || year == 0 - - Time.time_with_datetime_fallback(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil - end - - def fast_string_to_date(string) - if string =~ Format::ISO_DATE - new_date $1.to_i, $2.to_i, $3.to_i - end - end - - # Doesn't handle time zones. - def fast_string_to_time(string) - if string =~ Format::ISO_DATETIME - microsec = ($7.to_f * 1_000_000).to_i - new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec - end - end - - def fallback_string_to_date(string) - new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) - end - - def fallback_string_to_time(string) - time_hash = Date._parse(string) - time_hash[:sec_fraction] = microseconds(time_hash) - - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) - end - end - - private - def extract_limit(sql_type) - $1.to_i if sql_type =~ /\((.*)\)/ - end - - def extract_precision(sql_type) - $2.to_i if sql_type =~ /^(numeric|decimal|number)\((\d+)(,\d+)?\)/i - end - - def extract_scale(sql_type) - case sql_type - when /^(numeric|decimal|number)\((\d+)\)/i then 0 - when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i - end - end - - def simplified_type(field_type) - case field_type - when /int/i - :integer - when /float|double/i - :float - when /decimal|numeric|number/i - extract_scale(field_type) == 0 ? :integer : :decimal - when /datetime/i - :datetime - when /timestamp/i - :timestamp - when /time/i - :time - when /date/i - :date - when /clob/i, /text/i - :text - when /blob/i, /binary/i - :binary - when /char/i, /string/i - :string - when /boolean/i - :boolean - end - end - end - class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc: end @@ -268,6 +15,10 @@ module ActiveRecord # for generating a number of table creation or table changing SQL statements. class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :precision, :scale, :default, :null) #:nodoc: + def string_to_binary(value) + value + end + def sql_type base.type_to_sql(type.to_sym, limit, precision, scale) rescue type end @@ -318,21 +69,13 @@ module ActiveRecord @base = base end - #Handles non supported datatypes - e.g. XML - def method_missing(symbol, *args) - if symbol.to_s == 'xml' - xml_column_fallback(args) - else - super - end - end + def xml(*args) + raise NotImplementedError unless %w{ + sqlite mysql mysql2 + }.include? @base.adapter_name.downcase - def xml_column_fallback(*args) - case @base.adapter_name.downcase - when 'sqlite', 'mysql' - options = args.extract_options! - column(args[0], :text, options) - end + options = args.extract_options! + column(args[0], :text, options) end # Appends a primary key definition to the table definition. @@ -360,7 +103,7 @@ module ActiveRecord # # Available options are (none of these exists by default): # * <tt>:limit</tt> - - # Requests a maximum column length. This is number of characters for <tt>:string</tt> and + # Requests a maximum column length. This is number of characters for <tt>:string</tt> and # <tt>:text</tt> columns and number of bytes for :binary and :integer columns. # * <tt>:default</tt> - # The column's default value. Use nil for NULL. @@ -464,7 +207,7 @@ module ActiveRecord # TableDefinition#timestamps that'll add created_at and +updated_at+ as datetimes. # # TableDefinition#references will add an appropriately-named _id column, plus a corresponding _type - # column if the <tt>:polymorphic</tt> option is supplied. If <tt>:polymorphic</tt> is a hash of + # column if the <tt>:polymorphic</tt> option is supplied. If <tt>:polymorphic</tt> is a hash of # options, these will be used when creating the <tt>_type</tt> column. So what can be written like this: # # create_table :taggings do |t| @@ -528,7 +271,7 @@ module ActiveRecord # concatenated together. This string can then be prepended and appended to # to generate the final SQL to create the table. def to_sql - @columns.map(&:to_sql) * ', ' + @columns.map { |c| c.to_sql } * ', ' end private diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 7dee68502f..8bae50885f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -12,7 +12,7 @@ module ActiveRecord # Truncates a table alias according to the limits of the current adapter. def table_alias_for(table_name) - table_name[0..table_alias_length-1].gsub(/\./, '_') + table_name[0...table_alias_length].gsub(/\./, '_') end # def tables(name = nil) end @@ -110,8 +110,8 @@ module ActiveRecord # # Also note that this just sets the primary key in the table. You additionally # need to configure the primary key in the model via the +set_primary_key+ macro. - # Models do NOT auto-detect the primary key from their table definition. - # + # Models do NOT auto-detect the primary key from their table definition. + # # [<tt>:options</tt>] # Any extra options you want appended to the table definition. # [<tt>:temporary</tt>] @@ -151,10 +151,10 @@ module ActiveRecord # # See also TableDefinition#column for details on how to create columns. def create_table(table_name, options = {}) - table_definition = TableDefinition.new(self) - table_definition.primary_key(options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false + td = table_definition + td.primary_key(options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false - yield table_definition if block_given? + yield td if block_given? if options[:force] && table_exists?(table_name) drop_table(table_name, options) @@ -162,7 +162,7 @@ module ActiveRecord create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE " create_sql << "#{quote_table_name(table_name)} (" - create_sql << table_definition.to_sql + create_sql << td.to_sql create_sql << ") #{options[:options]}" execute create_sql end @@ -176,6 +176,13 @@ module ActiveRecord # # Other column alterations here # end # + # The +options+ hash can include the following keys: + # [<tt>:bulk</tt>] + # Set this to true to make this a bulk alter query, such as + # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ... + # + # Defaults to false. + # # ===== Examples # ====== Add a column # change_table(:suppliers) do |t| @@ -224,8 +231,14 @@ module ActiveRecord # # See also Table for details on # all of the various column transformation - def change_table(table_name) - yield Table.new(table_name, self) + def change_table(table_name, options = {}) + if supports_bulk_alter? && options[:bulk] + recorder = ActiveRecord::Migration::CommandRecorder.new(self) + yield Table.new(table_name, recorder) + bulk_change_table(table_name, recorder.commands) + else + yield Table.new(table_name, self) + end end # Renames a table. @@ -253,10 +266,7 @@ module ActiveRecord # remove_column(:suppliers, :qualification) # remove_columns(:suppliers, :qualification, :experience) def remove_column(table_name, *column_names) - raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty? - column_names.flatten.each do |column_name| - execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}" - end + columns_for_remove(table_name, *column_names).each {|column_name| execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{column_name}" } end alias :remove_columns :remove_column @@ -269,12 +279,11 @@ module ActiveRecord raise NotImplementedError, "change_column is not implemented" end - # Sets a new default value for a column. If you want to set the default - # value to +NULL+, you are out of luck. You need to - # DatabaseStatements#execute the appropriate SQL statement yourself. + # Sets a new default value for a column. # ===== Examples # change_column_default(:suppliers, :qualification, 'new') # change_column_default(:accounts, :authorized, 1) + # change_column_default(:users, :email, nil) def change_column_default(table_name, column_name, default) raise NotImplementedError, "change_column_default is not implemented" end @@ -327,29 +336,8 @@ module ActiveRecord # # Note: SQLite doesn't support index length def add_index(table_name, column_name, options = {}) - options[:name] = options[:name].to_s if options.key?(:name) - - column_names = Array.wrap(column_name) - index_name = index_name(table_name, :column => column_names) - - if Hash === options # legacy support, since this param was a string - index_type = options[:unique] ? "UNIQUE" : "" - index_name = options[:name] || index_name - else - index_type = options - end - - if index_name.length > index_name_length - @logger.warn("Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters. Skipping.") - return - end - if index_name_exists?(table_name, index_name, false) - @logger.warn("Index name '#{index_name}' on table '#{table_name}' already exists. Skipping.") - return - end - quoted_column_names = quoted_columns_for_index(column_names, options).join(", ") - - execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names})" + index_name, index_type, index_columns = add_index_options(table_name, column_name, options) + execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})" end # Remove the given index from the table. @@ -363,12 +351,7 @@ module ActiveRecord # Remove the index named by_branch_party in the accounts table. # remove_index :accounts, :name => :by_branch_party def remove_index(table_name, options = {}) - index_name = index_name(table_name, options) - unless index_name_exists?(table_name, index_name, true) - @logger.warn("Index name '#{index_name}' on table '#{table_name}' does not exist. Skipping.") - return - end - remove_index!(table_name, index_name) + remove_index!(table_name, index_name_for_remove(table_name, options)) end def remove_index!(table_name, index_name) #:nodoc: @@ -407,6 +390,7 @@ module ActiveRecord # as there's no way to determine the correct answer in that case. def index_name_exists?(table_name, index_name, default) return default unless respond_to?(:indexes) + index_name = index_name.to_s indexes(table_name).detect { |i| i.name == index_name } end @@ -446,12 +430,14 @@ module ActiveRecord end end - def assume_migrated_upto_version(version, migrations_path = ActiveRecord::Migrator.migrations_path) + def assume_migrated_upto_version(version, migrations_paths = ActiveRecord::Migrator.migrations_paths) + migrations_paths = Array.wrap(migrations_paths) version = version.to_i sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name) - migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i) - versions = Dir["#{migrations_path}/[0-9]*_*.rb"].map do |filename| + migrated = select_values("SELECT version FROM #{sm_table}").map { |v| v.to_i } + paths = migrations_paths.map {|p| "#{p}/[0-9]*_*.rb" } + versions = Dir[*paths].map do |filename| filename.split('/').last.split('_').first.to_i end @@ -471,7 +457,7 @@ module ActiveRecord end def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: - if native = native_database_types[type] + if native = native_database_types[type.to_sym] column_type_sql = (native.is_a?(Hash) ? native[:name] : native).dup if type == :decimal # ignore limit, use precision and scale @@ -538,6 +524,50 @@ module ActiveRecord def options_include_default?(options) options.include?(:default) && !(options[:null] == false && options[:default].nil?) end + + def add_index_options(table_name, column_name, options = {}) + column_names = Array.wrap(column_name) + index_name = index_name(table_name, :column => column_names) + + if Hash === options # legacy support, since this param was a string + index_type = options[:unique] ? "UNIQUE" : "" + index_name = options[:name].to_s if options.key?(:name) + else + index_type = options + end + + if index_name.length > index_name_length + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters" + end + if index_name_exists?(table_name, index_name, false) + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" + end + index_columns = quoted_columns_for_index(column_names, options).join(", ") + + [index_name, index_type, index_columns] + end + + def index_name_for_remove(table_name, options = {}) + index_name = index_name(table_name, options) + + unless index_name_exists?(table_name, index_name, true) + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" + end + + index_name + end + + def columns_for_remove(table_name, *column_names) + column_names = column_names.flatten + + raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.blank? + column_names.map {|column_name| quote_column_name(column_name) } + end + + private + def table_definition + TableDefinition.new(self) + end end end end |