diff options
Diffstat (limited to 'activerecord/lib')
14 files changed, 255 insertions, 149 deletions
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 9a01d793f9..04c12f86b6 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1621,11 +1621,11 @@ end # Allows you to set all the attributes at once by passing in a hash with keys # matching the attribute names (which again matches the column names). # - # If +guard_protected_attributes+ is true (the default), then sensitive - # attributes can be protected from this form of mass-assignment by using - # the +attr_protected+ macro. Or you can alternatively specify which - # attributes *can* be accessed with the +attr_accessible+ macro. Then all the - # attributes not included in that won't be allowed to be mass-assigned. + # If any attributes are protected by either +attr_protected+ or + # +attr_accessible+ then only settable attributes will be assigned. + # + # The +guard_protected_attributes+ argument is now deprecated, use + # the +assign_attributes+ method if you want to bypass mass-assignment security. # # class User < ActiveRecord::Base # attr_protected :is_admin @@ -1635,15 +1635,59 @@ end # user.attributes = { :username => 'Phusion', :is_admin => true } # user.username # => "Phusion" # user.is_admin? # => false + def attributes=(new_attributes, guard_protected_attributes = nil) + unless guard_protected_attributes.nil? + message = "the use of 'guard_protected_attributes' will be removed from the next major release of rails, " + + "if you want to bypass mass-assignment security then look into using assign_attributes" + ActiveSupport::Deprecation.warn(message) + end + + return unless new_attributes.is_a?(Hash) + + guard_protected_attributes ||= true + if guard_protected_attributes + assign_attributes(new_attributes) + else + assign_attributes(new_attributes, :without_protection => true) + end + end + + # Allows you to set all the attributes for a particular mass-assignment + # security scope by passing in a hash of attributes with keys matching + # the attribute names (which again matches the column names) and the scope + # name using the :as option. # - # user.send(:attributes=, { :username => 'Phusion', :is_admin => true }, false) + # To bypass mass-assignment security you can use the :without_protection => true + # option. + # + # class User < ActiveRecord::Base + # attr_accessible :name + # attr_accessible :name, :is_admin, :as => :admin + # end + # + # user = User.new + # user.assign_attributes({ :name => 'Josh', :is_admin => true }) + # user.name # => "Josh" + # user.is_admin? # => false + # + # user = User.new + # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin) + # user.name # => "Josh" # user.is_admin? # => true - def attributes=(new_attributes, guard_protected_attributes = true) - return unless new_attributes.is_a?(Hash) + # + # user = User.new + # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true) + # user.name # => "Josh" + # user.is_admin? # => true + def assign_attributes(new_attributes, options = {}) attributes = new_attributes.stringify_keys + scope = options[:as] || :default multi_parameter_attributes = [] - attributes = sanitize_for_mass_assignment(attributes) if guard_protected_attributes + + unless options[:without_protection] + attributes = sanitize_for_mass_assignment(attributes, scope) + end attributes.each do |k, v| if k.include?("(") 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 5ff81aa023..6d52cc344d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -62,6 +62,13 @@ module ActiveRecord exec_query(sql, name, binds) end + # Executes delete +sql+ statement in the context of this connection using + # +binds+ as the bind substitutes. +name+ is the logged along with + # the executed +sql+ statement. + def exec_delete(sql, name, binds) + exec_query(sql, name, binds) + end + # Returns the last auto-generated ID from the affected table. # # +id_value+ will be returned unless the value is nil, in @@ -82,8 +89,8 @@ module ActiveRecord end # Executes the delete statement and returns the number of rows affected. - def delete(sql, name = nil) - delete_sql(sql, name) + def delete(sql, name = nil, binds = []) + exec_delete(sql, name, binds) end # Checks whether there is currently no transaction active. This is done @@ -287,10 +294,6 @@ module ActiveRecord 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 "VALUES(DEFAULT)" end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index fc91814275..dee5c93b24 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -1,5 +1,6 @@ # encoding: utf-8 +gem 'mysql2', '~> 0.3.0' require 'mysql2' module ActiveRecord @@ -291,6 +292,14 @@ module ActiveRecord execute sql.gsub('?') { quote(*binds.shift.reverse) }, name end + def exec_delete(sql, name, binds) + binds = binds.dup + + # Pretend to support bind parameters + execute sql.gsub('?') { quote(*binds.shift.reverse) }, name + @connection.affected_rows + end + def last_inserted_id(result) @connection.last_id end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index eaadbc179b..6bc3b1548f 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -3,15 +3,8 @@ require 'active_support/core_ext/kernel/requires' require 'active_support/core_ext/object/blank' require 'set' -begin - require 'mysql' -rescue LoadError - raise "!!! Missing the mysql gem. Add it to your Gemfile: gem 'mysql'" -end - -unless defined?(Mysql::Result) && Mysql::Result.method_defined?(:each_hash) - raise "!!! Outdated mysql gem. Upgrade to 2.8.1 or later. In your Gemfile: gem 'mysql', '2.8.1'. Or use gem 'mysql2'" -end +gem 'mysql', '~> 2.8.1' +require 'mysql' class Mysql class Time @@ -402,34 +395,9 @@ module ActiveRecord def exec_query(sql, name = 'SQL', binds = []) log(sql, name, binds) do - result = nil - - cache = {} - if binds.empty? - stmt = @connection.prepare(sql) - else - cache = @statements[sql] ||= { - :stmt => @connection.prepare(sql) - } - stmt = cache[:stmt] - end - - stmt.execute(*binds.map { |col, val| - type_cast(val, col) - }) - if metadata = stmt.result_metadata - cols = cache[:cols] ||= metadata.fetch_fields.map { |field| - field.name - } - - metadata.free - result = ActiveRecord::Result.new(cols, stmt.to_a) + exec_stmt(sql, name, binds) do |cols, stmt| + ActiveRecord::Result.new(cols, stmt.to_a) if cols end - - stmt.free_result - stmt.close if binds.empty? - - result end end @@ -481,6 +449,14 @@ module ActiveRecord @connection.affected_rows end + def exec_delete(sql, name, binds) + log(sql, name, binds) do + exec_stmt(sql, name, binds) do |cols, stmt| + stmt.affected_rows + end + end + end + def begin_db_transaction #:nodoc: exec_without_stmt "BEGIN" rescue Mysql::Error @@ -840,6 +816,46 @@ module ActiveRecord end private + def exec_stmt(sql, name, binds) + cache = {} + if binds.empty? + stmt = @connection.prepare(sql) + else + cache = @statements[sql] ||= { + :stmt => @connection.prepare(sql) + } + stmt = cache[:stmt] + end + + + begin + stmt.execute(*binds.map { |col, val| type_cast(val, col) }) + rescue Mysql::Error => e + # Older versions of MySQL leave the prepared statement in a bad + # place when an error occurs. To support older mysql versions, we + # need to close the statement and delete the statement from the + # cache. + stmt.close + @statements.delete sql + raise e + end + + cols = nil + if metadata = stmt.result_metadata + cols = cache[:cols] ||= metadata.fetch_fields.map { |field| + field.name + } + end + + result = yield [cols, stmt] + + stmt.result_metadata.free if cols + stmt.free_result + stmt.close if binds.empty? + + result + end + def connect encoding = @config[:encoding] if encoding diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index aa9c9f5889..70d3d58c8f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -98,6 +98,9 @@ module ActiveRecord # XML type when 'xml' :xml + # tsvector type + when 'tsvector' + :tsvector # Arrays when /^\D+\[\]$/ :string @@ -119,6 +122,14 @@ module ActiveRecord # Extracts the value from a PostgreSQL column default definition. def self.extract_value_from_default(default) case default + # This is a performance optimization for Ruby 1.9.2 in development. + # If the value is nil, we return nil straight away without checking + # the regular expressions. If we check each regular expression, + # Regexp#=== will call NilClass#to_str, which will trigger + # method_missing (defined by whiny nil in ActiveSupport) which + # makes this method very very slow. + when NilClass + nil # Numeric types when /\A\(?(-?\d+(\.\d*)?\)?)\z/ $1 @@ -189,6 +200,11 @@ module ActiveRecord options = args.extract_options! column(args[0], 'xml', options) end + + def tsvector(*args) + options = args.extract_options! + column(args[0], 'tsvector', options) + end end ADAPTER_NAME = 'PostgreSQL' @@ -206,7 +222,8 @@ module ActiveRecord :date => { :name => "date" }, :binary => { :name => "bytea" }, :boolean => { :name => "boolean" }, - :xml => { :name => "xml" } + :xml => { :name => "xml" }, + :tsvector => { :name => "tsvector" } } # Returns 'PostgreSQL' as adapter name for identification purposes. @@ -228,7 +245,6 @@ module ActiveRecord # @local_tz is initialized as nil to avoid warnings when connect tries to use it @local_tz = nil @table_alias_length = nil - @postgresql_version = nil @statements = {} connect @@ -250,28 +266,16 @@ module ActiveRecord # Is this connection alive and ready for queries? def active? - if @connection.respond_to?(:status) - @connection.status == PGconn::CONNECTION_OK - else - # We're asking the driver, not Active Record, so use @connection.query instead of #query - @connection.query 'SELECT 1' - true - end - # postgres-pr raises a NoMethodError when querying if no connection is available. - rescue PGError, NoMethodError + @connection.status == PGconn::CONNECTION_OK + rescue PGError false end # Close then reopen the connection. def reconnect! - if @connection.respond_to?(:reset) - clear_cache! - @connection.reset - configure_connection - else - disconnect! - connect - end + clear_cache! + @connection.reset + configure_connection end def reset! @@ -425,17 +429,17 @@ module ActiveRecord # REFERENTIAL INTEGRITY ==================================== - def supports_disable_referential_integrity?() #:nodoc: + def supports_disable_referential_integrity? #:nodoc: true end def disable_referential_integrity #:nodoc: - if supports_disable_referential_integrity?() then + if supports_disable_referential_integrity? then execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) end yield ensure - if supports_disable_referential_integrity?() then + if supports_disable_referential_integrity? then execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) end end @@ -509,12 +513,7 @@ module ActiveRecord # Queries the database and returns the results in an Array-like object def query(sql, name = nil) #:nodoc: log(sql, name) do - if @async - res = @connection.async_exec(sql) - else - res = @connection.exec(sql) - end - return result_as_array(res) + result_as_array @connection.async_exec(sql) end end @@ -522,11 +521,7 @@ module ActiveRecord # or raising a PGError exception otherwise. def execute(sql, name = nil) log(sql, name) do - if @async - @connection.async_exec(sql) - else - @connection.exec(sql) - end + @connection.async_exec(sql) end end @@ -535,30 +530,26 @@ module ActiveRecord end def exec_query(sql, name = 'SQL', binds = []) - return exec_no_cache(sql, name) if binds.empty? - log(sql, name, binds) do - unless @statements.key? sql - nextkey = "a#{@statements.length + 1}" - @connection.prepare nextkey, sql - @statements[sql] = nextkey - end - - key = @statements[sql] + result = binds.empty? ? exec_no_cache(sql, binds) : + exec_cache(sql, binds) - # Clear the queue - @connection.get_last_result - @connection.send_query_prepared(key, binds.map { |col, val| - type_cast(val, col) - }) - @connection.block - result = @connection.get_last_result ret = ActiveRecord::Result.new(result.fields, result_as_array(result)) result.clear return ret end end + def exec_delete(sql, name = 'SQL', binds = []) + log(sql, name, binds) do + result = binds.empty? ? exec_no_cache(sql, binds) : + exec_cache(sql, binds) + affected = result.cmd_tuples + result.clear + affected + end + end + def sql_for_insert(sql, pk, id_value, sequence_name, binds) unless pk _, table = extract_schema_and_table(sql.split(" ", 4)[2]) @@ -945,22 +936,7 @@ module ActiveRecord protected # Returns the version of the connected PostgreSQL server. def postgresql_version - @postgresql_version ||= - if @connection.respond_to?(:server_version) - @connection.server_version - else - # Mimic PGconn.server_version behavior - begin - if query('SELECT version()')[0][0] =~ /PostgreSQL ([0-9.]+)/ - major, minor, tiny = $1.split(".") - (major.to_i * 10000) + (minor.to_i * 100) + tiny.to_i - else - 0 - end - rescue - 0 - end - end + @connection.server_version end def translate_exception(exception, message) @@ -975,13 +951,26 @@ module ActiveRecord end private - def exec_no_cache(sql, name) - log(sql, name) do - result = @connection.async_exec(sql) - ret = ActiveRecord::Result.new(result.fields, result_as_array(result)) - result.clear - ret + def exec_no_cache(sql, binds) + @connection.async_exec(sql) + end + + def exec_cache(sql, binds) + unless @statements.key? sql + nextkey = "a#{@statements.length + 1}" + @connection.prepare nextkey, sql + @statements[sql] = nextkey end + + key = @statements[sql] + + # Clear the queue + @connection.get_last_result + @connection.send_query_prepared(key, binds.map { |col, val| + type_cast(val, col) + }) + @connection.block + @connection.get_last_result end # The internal PostgreSQL identifier of the money data type. @@ -993,10 +982,6 @@ module ActiveRecord # connected server's characteristics. def connect @connection = PGconn.connect(*@connection_parameters) - PGconn.translate_results = false if PGconn.respond_to?(:translate_results=) - - # Ignore async_exec and async_query when using postgres-pr. - @async = @connection.respond_to?(:async_exec) # Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of # PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision @@ -1010,11 +995,7 @@ module ActiveRecord # This is called by #connect and should not be called manually. def configure_connection if @config[:encoding] - if @connection.respond_to?(:set_client_encoding) - @connection.set_client_encoding(@config[:encoding]) - else - execute("SET client_encoding TO '#{@config[:encoding]}'") - end + @connection.set_client_encoding(@config[:encoding]) end self.client_min_messages = @config[:min_messages] if @config[:min_messages] self.schema_search_path = @config[:schema_search_path] || @config[:schema_order] diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 4540cf015b..fe2868f367 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -183,6 +183,11 @@ module ActiveRecord end end + def exec_delete(sql, name = 'SQL', binds = []) + exec_query(sql, name, binds) + @connection.changes + end + def last_inserted_id(result) @connection.last_insert_row_id end @@ -357,10 +362,6 @@ module ActiveRecord alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) end - def null_insert_value - Arel.sql 'NULL' - end - def empty_insert_statement_value "VALUES(NULL)" end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index a916c88348..b4531ed35f 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -77,7 +77,15 @@ module ActiveRecord def destroy if persisted? IdentityMap.remove(self) if IdentityMap.enabled? - self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).delete_all + pk = self.class.primary_key + column = self.class.columns_hash[pk] + substitute = connection.substitute_at(column, 0) + + relation = self.class.unscoped.where( + self.class.arel_table[pk].eq(substitute)) + + relation.bind_values = [[column, id]] + relation.delete_all end @destroyed = true @@ -136,22 +144,27 @@ module ActiveRecord # Updates the attributes of the model from the passed-in hash and saves the # record, all wrapped in a transaction. If the object is invalid, the saving # will fail and false will be returned. - def update_attributes(attributes) + # + # When updating model attributes, mass-assignment security protection is respected. + # If no +:as+ option is supplied then the +:default+ scope will be used. + # If you want to bypass the protection given by +attr_protected+ and + # +attr_accessible+ then you can do so using the +:without_protection+ option. + def update_attributes(attributes, options = {}) # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. with_transaction_returning_status do - self.attributes = attributes + self.assign_attributes(attributes, options) save end end # Updates its receiver just like +update_attributes+ but calls <tt>save!</tt> instead # of +save+, so an exception is raised if the record is invalid. - def update_attributes!(attributes) + def update_attributes!(attributes, options = {}) # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. with_transaction_returning_status do - self.attributes = attributes + self.assign_attributes(attributes, options) save! end end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index cace6f0cc0..d38588519b 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -50,6 +50,9 @@ module ActiveRecord initializer "active_record.set_configs" do |app| ActiveSupport.on_load(:active_record) do + if app.config.active_record.delete(:whitelist_attributes) + attr_accessible(nil) + end app.config.active_record.each do |k,v| send "#{k}=", v end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 6b3c38cb58..7d76d7a19f 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -70,7 +70,13 @@ db_namespace = namespace :db do @charset = ENV['CHARSET'] || 'utf8' @collation = ENV['COLLATION'] || 'utf8_unicode_ci' creation_options = {:charset => (config['charset'] || @charset), :collation => (config['collation'] || @collation)} - error_class = config['adapter'] =~ /mysql2/ ? Mysql2::Error : Mysql::Error + if config['adapter'] =~ /jdbc/ + #FIXME After Jdbcmysql gives this class + require 'active_record/railties/jdbcmysql_error' + error_class = ArJdbcMySQL::Error + else + error_class = config['adapter'] =~ /mysql2/ ? Mysql2::Error : Mysql::Error + end access_denied_error = 1045 begin ActiveRecord::Base.establish_connection(config.merge('database' => nil)) @@ -94,7 +100,7 @@ db_namespace = namespace :db do $stderr.puts "(if you set the charset manually, make sure you have a matching collation)" if config['charset'] end end - when 'postgresql' + when /postgresql/ @encoding = config['encoding'] || ENV['CHARSET'] || 'utf8' begin ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public')) diff --git a/activerecord/lib/active_record/railties/jdbcmysql_error.rb b/activerecord/lib/active_record/railties/jdbcmysql_error.rb new file mode 100644 index 0000000000..6b9af2a0cb --- /dev/null +++ b/activerecord/lib/active_record/railties/jdbcmysql_error.rb @@ -0,0 +1,16 @@ +#FIXME Remove if ArJdbcMysql will give. +module ArJdbcMySQL + class Error < StandardError + attr_accessor :error_number, :sql_state + + def initialize msg + super + @error_number = nil + @sql_state = nil + end + + # Mysql gem compatibility + alias_method :errno, :error_number + alias_method :error, :message + end +end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 45a7000cce..658a949331 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -60,7 +60,7 @@ module ActiveRecord end if values.empty? # empty insert - im.values = im.create_values [connection.null_insert_value], [] + im.values = Arel.sql(connection.empty_insert_statement_value) else im.insert substitutes end @@ -338,7 +338,9 @@ module ActiveRecord where(conditions).delete_all else statement = arel.compile_delete - affected = @klass.connection.delete statement.to_sql + affected = @klass.connection.delete( + statement.to_sql, 'SQL', bind_values) + reset affected end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index a3d4b7f45a..57c9921ea8 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -279,8 +279,8 @@ module ActiveRecord unless record record = @klass.new do |r| - r.send(:attributes=, protected_attributes_for_create, true) unless protected_attributes_for_create.empty? - r.send(:attributes=, unprotected_attributes_for_create, false) unless unprotected_attributes_for_create.empty? + r.assign_attributes(protected_attributes_for_create) + r.assign_attributes(unprotected_attributes_for_create, :without_protection => true) end yield(record) if block_given? record.save if match.instantiator == :create diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 982b3d7e9f..2814771002 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -25,7 +25,18 @@ module ActiveRecord values = value.to_a.map { |x| x.is_a?(ActiveRecord::Base) ? x.id : x } - attribute.in(values) + + if values.include?(nil) + values = values.compact + if values.empty? + attribute.eq nil + else + attribute.in(values.compact).or attribute.eq(nil) + end + else + attribute.in(values) + end + when Range, Arel::Relation attribute.in(value) when ActiveRecord::Base diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index d1225a9ed9..4db4105389 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -56,8 +56,9 @@ module ActiveRecord column = klass.columns_hash[attribute.to_s] value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s if column.text? - if !options[:case_sensitive] && column.text? - relation = table[attribute].matches(value) + if !options[:case_sensitive] && value && column.text? + # will use SQL LOWER function before comparison + relation = table[attribute].lower.eq(table.lower(value)) else value = klass.connection.case_sensitive_modifier(value) relation = table[attribute].eq(value) |