Rails 7 has_secure_password doesn't throw validation error on update when password is blank - rails-activerecord

I'm running Rails 7.0.4 right now.
Here's my User Model
class User < ApplicationRecord
has_secure_password
validates :fullname, presence: true, length: { maximum: 100 }
validates :email, presence: true,
format: {with: URI::MailTo::EMAIL_REGEXP, message: "Please use a valid email address."},
uniqueness: true
end
Given a standard user,
When I update the password with blank information,
I expect a validation error "can't be blank" to get thrown.
What I get instead is no error thrown and what looks like a successful database transaction with no rollback.
Here's an example where I use the update method on an instance of User and see the validation doesn't get thrown and prevent the transaction.
pry(main)> u = User.first
User Load (0.3ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1 [["LIMIT", 1]]
=> #<User:0x0000000108e89280
id: 1,
fullname: "foo",
email: "foo#bar.com",
password_digest: "[FILTERED]",
created_at: Fri, 06 Jan 2023 04:13:52.650976000 UTC +00:00,
updated_at: Fri, 06 Jan 2023 04:13:52.672566000 UTC +00:00,
company_id: nil,
auth_token: "[FILTERED]">
[4] pry(main)> u.update(password: '', password_confirmation: '')
TRANSACTION (0.2ms) BEGIN
User Exists? (0.5ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = $1 AND "users"."id" != $2 LIMIT $3 [["email", "foo#bar.com"], ["id", 1], ["LIMIT", 1]]
TRANSACTION (0.2ms) COMMIT
=> true
And here is the continuation of the above session where I attempt to just set the attributes and call save directly. This is what I understand the update and update_attributes methods do under the hood.
[5] pry(main)> u.password
=> nil
[6] pry(main)> attributes = { password: '', password_confirmation: '' }
=> {:password=>"", :password_confirmation=>""}
[7] pry(main)> u.attributes
=> {"id"=>1,
"fullname"=>"foo",
"email"=>"foo#bar.com",
"password_digest"=>"$2a$12$W5CwTg.DjhLZdSiXDvD2fuWinwbMtmavK46IZCR27EzopLgfQzQxy",
"created_at"=>Fri, 06 Jan 2023 04:13:52.650976000 UTC +00:00,
"updated_at"=>Fri, 06 Jan 2023 04:13:52.672566000 UTC +00:00,
"company_id"=>nil,
"auth_token"=>
"eyJfcmFpbHMiOnsibWVzc2FnZSI6Ik1RPT0iLCJleHAiOm51bGwsInB1ciI6InVzZXIvc2Vzc2lvbnMifX0=--239334c38fd5527b383c22d7aad6277f07743b3f6ecd2de846d42731c22e39d5"}
[8] pry(main)> u.attributes = attributes
=> {:password=>"", :password_confirmation=>""}
[9] pry(main)> u
=> #<User:0x0000000108e89280
id: 1,
fullname: "foo",
email: "foo#bar.com",
password_digest: "[FILTERED]",
created_at: Fri, 06 Jan 2023 04:13:52.650976000 UTC +00:00,
updated_at: Fri, 06 Jan 2023 04:13:52.672566000 UTC +00:00,
company_id: nil,
auth_token: "[FILTERED]">
[10] pry(main)> u.save
TRANSACTION (0.2ms) BEGIN
User Exists? (0.3ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = $1 AND "users"."id" != $2 LIMIT $3 [["email", "foo#bar.com"], ["id", 1], ["LIMIT", 1]]
TRANSACTION (0.2ms) COMMIT
=> true
Shouldn't save trigger the validation errors, which are in has_secure_password?

Here's the current sourcecode for the update method.
def update(id, attributes)
if id.is_a?(Array)
idx = -1
id.collect { |one_id| idx += 1; update(one_id, attributes[idx]) }
else
object = find(id)
object.update_attributes(attributes)
object
end
end
And that refers to the update_attributes method here:
def update_attributes(attributes)
self.attributes = attributes
save
end
And here's the has_secure_password sourcecode:
def has_secure_password(options = {})
# Load bcrypt gem only when has_secure_password is used.
# This is to avoid ActiveModel (and by extension the entire framework)
# being dependent on a binary library.
begin
require "bcrypt"
rescue LoadError
$stderr.puts "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install"
raise
end
include InstanceMethodsOnActivation
if options.fetch(:validations, true)
include ActiveModel::Validations
# This ensures the model has a password by checking whether the password_digest
# is present, so that this works with both new and existing records. However,
# when there is an error, the message is added to the password attribute instead
# so that the error message will make sense to the end-user.
validate do |record|
record.errors.add(:password, :blank) unless record.password_digest.present?
end
validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
validates_confirmation_of :password, allow_blank: true
end
end
After studying the has_secure_password sourcecode I saw that it only throws the blank validation error when password_digest is blank.
So the solution here that I've found is to create a validation of my own in the model class to check for presence upon updates, and to pass it my own message. I also see that this method doesn't check for password strength, so I needed to add in password strength regex.
Here is the new model class and it works as expected.
The downside is that this implementation doesn't pull apart the different parts of the regex that it doesn't fail. So if the password is missing a symbol it will just render the entire format validation error instead of specifically saying "Password must include a symbol."
class User < ApplicationRecord
has_secure_password
validates :fullname, presence: true, length: { maximum: 100 }
validates :email, presence: true,
format: {with: URI::MailTo::EMAIL_REGEXP, message: "be a valid email format"},
uniqueness: true
PASSWORD_REQUIREMENTS = /\A
(?=.{6,}) # At least 6 characters long
(?=.*\d)
(?=.*[a-z])
(?=.*[A-Z])
(?=.*[[:^alnum:]])
/x
validates :password, presence: true,
format: { with: PASSWORD_REQUIREMENTS,
message: 'must be at least 6 characters long, contain a number, a lower and upper letter, and have one special character.' }
end

Related

Phone is defined as string in db schema, but integers are saving as valid in Ruby

I feel like I'm missing something very simple here...
My db schema:
create_table "plans", force: true do |t|
t.string "phone1"
...
end
Here's a snippet from my console:
#plan = Plan.create(a bunch of params)
#plan.phone1 = "123"
#plan.valid?
# => true
# above is great, here's where the problem comes in:
#plan.update_attribute("phone1", 123)
#plan.phone1
# => 123
#plan.valid?
# => true
This is not making my model tests very happy. Nor me for that matter. From my model, here are all the relevant validations:
validates :phone1, presence: true
validates :phone1, length: { is: 3 }
ActiveRecord looks at your schema.rb and creates setters which typecast based on the column value.
class Plan < ActiveRecord::Base
# "Automagically" creating by Active Record.
# def phone1= val
# #phone1 = val.to_s
# end
end
So when you call .valid on #plan the 'phone1' attribute is a string. I'm not sure what your test looks like but if your are doing:
plan = Plan.new(123)
expect(plan.valid?).to be_falsy
Expecting plan to be invalid solely because it's passed a number than your have simply misunderstood how rails works.
Given:
$ rails g model plan phone1:string ends:datetime
$ rails g migrate
irb(main):004:0>#plan = Plan.create(ends: Date.tomorrow, phone1: 123)
(0.3ms) begin transaction
SQL (1.2ms) INSERT INTO "plans" ("ends", "phone1", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["ends", "2015-06-24 00:00:00.000000"], ["phone1", "123"], ["created_at", "2015-06-23 02:21:39.236332"], ["updated_at", "2015-06-23 02:21:39.236332"]]
(1.2ms) commit transaction
=> #<Plan id: 2, phone1: "123", ends: "2015-06-24 00:00:00", created_at: "2015-06-23 02:21:39", updated_at: "2015-06-23 02:21:39">
irb(main):005:0> #plan.phone1 = 123456
=> 123456
irb(main):006:0> #plan.phone1.class
=> String
irb(main):007:0> #plan.update_attribute("phone1", 123)
(0.8ms) begin transaction
(0.3ms) commit transaction
=> true
irb(main):008:0> #plan.phone1.class
=> String
irb(main):013:0> #plan.ends = "2015-06-23"
=> "2015-06-23"
irb(main):014:0> #plan.ends
=> Tue, 23 Jun 2015 00:00:00 UTC +00:00
irb(main):015:0>
You could write a custom validation method to check that phone1 is a String*:
class Plan
validates :phone1, presence: true
validates :phone1, length: { is: 3 }
validates :special_validations
def special_validations
errors.add(:phone1, "Must be String") unless phone1.is_a? String
# add whatever you feel like
true
def
end
On the other hand, if you get a numerical field when loading the data from the database, than your database's field type isn't a string. Maybe an older setting persists?
* I'm not too savvy as far as the Rails specialty features go, so there might be a shortcut to this...

ActiveSupport::Concern, has_secure password not update some columns

I have a Recoverable module for my Customer model. Customer model using has_secure method for authentication. Here is the Customer model:
class Customer < ActiveRecord::Base
include Recoverable
##
# Validations
validates :email, format: { with: REGEX_EMAIL }, allow_nil: false, allow_blank: false
validates_uniqueness_of :email
validates_presence_of :email
has_secure_password
validates :password, length: { minimum: 6 }, if: :password_digest_changed?
validates :password_confirmation, presence: true, if: :password_digest_changed?
end
And here is the Recoverable module:
# encoding: utf-8
module Recoverable
extend ActiveSupport::Concern
def reset_password!(new_password, new_password_confirmation)
self.password = new_password
self.password_confirmation = new_password_confirmation
if valid?
self.reset_password_token = nil
self.reset_password_sent_at = nil
end
save
end
end
My problem is after reset_password called reset_password_token, reset_password_sent_at are not null. It's not set to null. Update query is not set below columns. Why? Am I miss something? If you need more info let me know.
My environments: I'm using Rails 4 app.
UPDATE 1
When I puts self.inspect I get following outputs:
#<Customer id: 79, email: "milk#yahoo.com", password_digest: "$2a$10$U2knjpm5LF1V/sgXag0DcOpgZWHSpLw8nfCy4U8D57s6...", created_at: "2013-05-11 11:55:34", updated_at: "2013-05-16 10:04:45", reset_password_sent_at: nil, reset_password_token: nil>
UPDATE 2:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"PbUhgSPvQZWXflT5fA1WhqhHJX3c7NMapg6eeDQvpBI=", "token"=>"fiMXi2_4cYCHsFMop9TJBL2Qeqc41xWhHA", "q"=>{"password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}}
Unpermitted parameters: utf8, _method, authenticity_token, q
Customer Load (0.4ms) SELECT "customers".* FROM "customers" WHERE "customers"."reset_password_token" IS NULL LIMIT 1
Unpermitted parameters: password_confirmation
Unpermitted parameters: password
Customer Exists (0.3ms) SELECT 1 AS one FROM "customers" WHERE ("customers"."email" = 'milk#yahoo.com' AND "customers"."id" != 79) LIMIT 1
----------------------------BEFORE:
#<ActiveModel::Errors:0xb593c280 #base=#<Customer id: 79, email: "milk#yahoo.com", password_digest: "$2a$10$/xYeks8yyaCMOFORFLMb1.xR7fxfskW6kHR4S2df/LTK...", store_id: 124, created_at: "2013-05-11 11:55:34", updated_at: "2013-05-16 11:56:52", reset_password_sent_at: nil, reset_password_token: nil>, #messages={}>
(0.1ms) BEGIN
CACHE (0.0ms) SELECT 1 AS one FROM "customers" WHERE ("customers"."email" = 'milk#yahoo.com' AND "customers"."id" != 79) LIMIT 1
SQL (0.3ms) UPDATE "customers" SET "password_digest" = $1, "updated_at" = $2 WHERE "customers"."id" = 79 [["password_digest", "$2a$10$/xYeks8yyaCMOFORFLMb1.xR7fxfskW6kHR4S2df/LTKUI001xu0O"], ["updated_at", Thu, 16 May 2013 19:58:25 ULAT +08:00]]
(16.5ms) COMMIT
---------------------------- SAVE:
true
----------------------------AFTER:
#<ActiveModel::Errors:0xb593c280 #base=#<Customer id: 79, email: "milk#yahoo.com", password_digest: "$2a$10$/xYeks8yyaCMOFORFLMb1.xR7fxfskW6kHR4S2df/LTK...", store_id: 124, created_at: "2013-05-11 11:55:34", updated_at: "2013-05-16 11:58:25", reset_password_sent_at: nil, reset_password_token: nil>, #messages={}>
Ok so finally if your model is not valid after clearing variables you can do that:
save(validate: false)
It will skip validation and will allow you to save invalid model
Could you check if your model is really valid ?
I mean something like
if valid?
puts "valid"
self.reset_password_token = nil
self.reset_password_sent_at = nil
else
puts self.errors.inspect
end
Maybe you have some forgotten validation and you are not going to that block ?

Association id = nil when using find_or_create_by

When I try adding a Client through the find_or_create_by method I don`t get a trainer_id passed in as an value (trainer_id for client = nil)
Even thou in my Client_Controller I have the the build method, which I assume would pull the id from current_trainer
I'm assuming there's a disconnect between find_or_create_by_name & build method.
Would appreciate any help with this.
Client_Controller
def create
#client = current_trainer.clients.build(params[:client])
respond_to do |format|
if #client.save
ClientMailer.registration_confirmation(#client).deliver
format.html { redirect_to #client, :notice => 'Client was successfully added.' }
format.json { render :json => #client, :status => :created, :location => #client }
else
format.html { render :action => "new" }
format.json { render :json => #client.errors, :status => :unprocessable_entity }
end
end
end
Workout Model
class Workout < ActiveRecord::Base
belongs_to :client
belongs_to :trainer
validates_presence_of :day,
:client_id,
:trainer_id,
:message => "You have to indicated for when you want to schedule this workout."
def client_name
client.try(:name)
end
def client_name=(name)
self.client = Client.find_or_create_by_name(name) if name.present?
end
end
Client Model
class Client < ActiveRecord::Base
belongs_to :trainer
before_save :generate_password
has_many :workouts
has_many :weigh_ins
validates_presence_of :name, :message => "You have to provide a client name in order to add new client."
has_attached_file :profile_pic, :styles => { :micro => "60x60>", :small => "150x150>" },
:default_url => 'profile_default_small.jpg',
:storage => :s3,
:s3_credentials => S3_CREDENTIALS,
:bucket => '#',
:path => ":attachment/:id/:style/:basename.:extension"
validates_attachment_size :profile_pic,
:less_than => 3.megabytes,
:message => "Your profile picture can`t be bigger than 3MB, sorry."
def generate_password
self.password = name[0..2].to_s + rand(99).to_s
end
def self.authenticate(email, password)
client = find_by_email(email)
if client && client.password == client.password
client
else
nil
end
end
Server Log
Started POST "/workouts" for 127.0.0.1 at Wed Apr 25 17:39:37 -0400 2012
Processing by WorkoutsController#create as HTML
Parameters: {"commit"=>"Schedule Session", "authenticity_token"=>"CxJSVfn0fwerdyrpA9/JEe8fX8Ep2/ZhnOqQkjZ3iwE=", "utf8"=>"✓", "workout"=>{"client_name"=>"John Doe", "day(1i)"=>"2012", "day(2i)"=>"4", "day(3i)"=>"29", "day(4i)"=>"21", "day(5i)"=>"00", "note"=>"", "title"=>"Current_Client"}}
Trainer Load (0.1ms) SELECT "trainers".* FROM "trainers" WHERE "trainers"."id" = ? LIMIT 1 [["id", 37]]
Client Load (0.2ms) SELECT "clients".* FROM "clients" WHERE "clients"."name" = 'John Doe' LIMIT 1
(0.0ms) begin transaction
SQL (0.4ms) INSERT INTO "clients" ("created_at", "email", "goal_weight", "name", "notes", "password", "profile_pic_content_type", "profile_pic_file_name", "profile_pic_file_size", "profile_pic_updated_at", "start_weight", "trainer_id", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) [["created_at", Wed, 25 Apr 2012 21:39:37 UTC +00:00], ["email", nil], ["goal_weight", nil], ["name", "John Doe"], ["notes", nil], ["password", "Joh55"], ["profile_pic_content_type", nil], ["profile_pic_file_name", nil], ["profile_pic_file_size", nil], ["profile_pic_updated_at", nil], ["start_weight", nil], ["trainer_id", nil], ["updated_at", Wed, 25 Apr 2012 21:39:37 UTC +00:00]]
[paperclip] Saving attachments.
(2.3ms) commit transaction
(0.1ms) begin transaction
SQL (0.5ms) INSERT INTO "workouts" ("client_id", "created_at", "day", "note", "title", "trainer_id", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?) [["client_id", 53], ["created_at", Wed, 25 Apr 2012 21:39:37 UTC +00:00], ["day", Sun, 29 Apr 2012 21:00:00 UTC +00:00], ["note", ""], ["title", "Current_Client"], ["trainer_id", 37], ["updated_at", Wed, 25 Apr 2012 21:39:37 UTC +00:00]]
(1.1ms) commit transaction
Redirected to http://localhost:3000/workouts/64
Completed 302 Found in 13ms (ActiveRecord: 4.8ms)
As I understand it, find_or_create_by is going to use the create method, not your create controller action. That's why none of the other information is making it into the new Client record.
You can pass additional parameters to find_or_create_by_name. These parameters will either be 1) ignored if a record is found or 2) used in the create method if no record is found. You need to pass the create the additional parameters for your relationships because it won't automagically handle them the way build does.
You want something like this: find_or_create_by_name(:name => name, :trainer_id => self.trainer_id). Note that when using additional params, you need to pass them all as a hash.
More info: Passing extra data to find_or_create

Associations in test fixtures inside rails mountable engine

I have encountered a problem during unit testing of my fresh new mountable engine.
I have two basic models:
module Ads
class Category < ActiveRecord::Base
set_table_name :categories
has_many :ads
end
end
module Ads
class Ad < ActiveRecord::Base
set_table_name :ads
belongs_to :category
end
end
It works perfectly fine in rails console.
ruby-1.9.2-p290 :007 > c = Ads::Category.create(title: 'foo')
(0.4ms) BEGIN
SQL (1.0ms) INSERT INTO "categories" ("created_at", "title", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["created_at", Tue, 22 Nov 2011 15:34:41 UTC +00:00], ["title", "foo"], ["updated_at", Tue, 22 Nov 2011 15:34:41 UTC +00:00]]
(18.0ms) COMMIT
=> #<Ads::Category id: 2, title: "foo", created_at: "2011-11-22 15:34:41", updated_at: "2011-11-22 15:34:41">
ruby-1.9.2-p290 :008 > c.ads.create(title: 'bar')
(0.6ms) BEGIN
SQL (1.1ms) INSERT INTO "ads" ("category_id", "created_at", "title", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["category_id", 2], ["created_at", Tue, 22 Nov 2011 15:34:43 UTC +00:00], ["title", "bar"], ["updated_at", Tue, 22 Nov 2011 15:34:43 UTC +00:00]]
(16.8ms) COMMIT
=> #<Ads::Ad id: 2, title: "bar", category_id: 2, created_at: "2011-11-22 15:34:43", updated_at: "2011-11-22 15:34:43">
I moved on to a unit test:
# test/unit/ads/category_test.rb
require 'test_helper'
module Ads
class CategoryTest < ActiveSupport::TestCase
# by default it searches in dummy app, is there any cleaner way to change this?
self.fixture_path = Rails.root.parent + "./fixtures/ads"
fixtures :categories, :ads
test "should find cars" do
assert_equal 1, Category.where(title: 'Cars').count
end
end
end
# test/fixtures/ads/categories.yml
cars:
title: Cars
# test/fixtures/ads/ads.yml
foo:
title: Foo
category: cars
When i run unit tests:
rake app:test:units
It ends up with error during populating test db:
ActiveRecord::StatementInvalid: PGError: ERROR: column "category" of relation "ads" does not exist
LINE 1: INSERT INTO "ads" ("title", "category") VALUES ('Foo', 'cars...
It seems like ads<->categories association' is ignored.
Same approach works perfectly fine in a standalone rails app.
My question is: what am I doing wrong?
The side question is if there is a cleaner solution to change fixtures path?
Rails: 3.1.3
I had the exact same issue but with a Rails 3.0 engine. I couldn't fix it properly, but as a work around I did this.
Instead of:
category: cars
Do:
category_id: <%= Fixtures.identify(:cars) %>
This works, because the ID is actually a hash of the label. And Fixtures.identify is what does that hashing.

Rails 3.0.9 : ActiveRecord Uniqueness Constraint failing on every updated, doesn't matter if the unique column isn't touched

I have a Profile model
class Profile < ActiveRecord::Base
attr_accessible :user_id, :race_id, :nickname, :first_name, :last_name, :gender, :birth_date, :eighteen,
:time_zone, :metric_scale, :referral_code, :referrer_id, :tag_line
# Relationships
belongs_to :user
belongs_to :race
belongs_to :referred_by, :class_name => "Profile", :foreign_key => "referral_code"
has_many :referrals, :class_name => "Profile", :foreign_key => "referrer_id"
# Validations
validates :user_id, :race_id, :nickname, :first_name, :last_name, :time_zone, :gender, :presence => true
validates :referral_code, :nickname, :uniqueness => { :case_sensitive => false }
# Instance Methods
def full_name
first_name + " " + last_name
end
# Class Methods
def self.search(search)
search_condition = "%" + search + "%"
find(:all, :conditions => ['nickname LIKE ?', search_condition])
end
def self.find_by_referral_code(referrer_code)
find(:one, :conditions => ['referral_code LIKE ?', referrer_code])
end
end
No matter which column I am updated the Uniqueness Constraint on 'referral_code' false and I cannot update the model and I can't figure out why. From what I read online as of Rails 3 ActiveRecord was supposed to be tracking dirty objects and only generating update queries containing the altered columns leaving all others alone. Because it should only be performing update queries on columns other than the Unique ones the validation should not be failing. Unfortunately it is. Here is Rails Console session displaying this:
Loading development environment (Rails 3.0.9)
ruby-1.9.2-p180 :001 > profile = Profile.find(3)
=> #<Profile id: 3, user_id: 3, race_id: 2, nickname: "Premium-User", first_name: "Premium", last_name: "User", gender: "M", birth_date: "1977-01-01", eighteen: true, complete: true, time_zone: "Kuala Lumpur", metric_scale: false, referral_code: "bo", referrer_id: nil, tag_line: "This is what its like.", created_at: "2011-09-21 04:08:00", updated_at: "2011-09-21 04:08:00">
ruby-1.9.2-p180 :002 > update = {"tag_line"=>"Changed to this"}
=> {"tag_line"=>"Changed to this"}
ruby-1.9.2-p180 :003 > profile.update_attributes(update)
=> false
ruby-1.9.2-p180 :004 > profile.errors
=> {:referral_code=>["has already been taken"]}
Even performing an update directly on a single column which is not unique causes the uniqueness constraint to fail and the record will not be updated, here is a console session:
Loading development environment (Rails 3.0.9)
ruby-1.9.2-p180 :001 > profile = Profile.find(3)
=> #<Profile id: 3, user_id: 3, race_id: 2, nickname: "Premium-User", first_name: "Premium", last_name: "User", gender: "M", birth_date: "1977-01-01", eighteen: true, complete: true, time_zone: "Kuala Lumpur", metric_scale: false, referral_code: "bo", referrer_id: nil, tag_line: "This is what its like.", created_at: "2011-09-21 04:08:00", updated_at: "2011-09-21 04:08:00">
ruby-1.9.2-p180 :002 > profile.tag_line = "changed to this"
=> "changed to this"
ruby-1.9.2-p180 :003 > profile.save
=> false
ruby-1.9.2-p180 :004 > profile.errors
=> {:referral_code=>["has already been taken"]}
I also ran a check to see if ActiveRecord was actually tracking the dirty object and it appears to be, here is the console session:
Loading development environment (Rails 3.0.9)
ruby-1.9.2-p180 :001 > profile = Profile.find(3)
=> #<Profile id: 3, user_id: 3, race_id: 2, nickname: "Premium-User", first_name: "Premium", last_name: "User", gender: "M", birth_date: "1977-01-01", eighteen: true, complete: true, time_zone: "Kuala Lumpur", metric_scale: false, referral_code: "bo", referrer_id: nil, tag_line: "This is what its like.", created_at: "2011-09-21 04:08:00", updated_at: "2011-09-21 04:08:00">
ruby-1.9.2-p180 :002 > profile.tag_line = "change to this"
=> "change to this"
ruby-1.9.2-p180 :003 > profile.changes
=> {"tag_line"=>["This is what its like.", "change to this"]}
ruby-1.9.2-p180 :004 > profile.save
=> false
ruby-1.9.2-p180 :005 > profile.errors
=> {:referral_code=>["has already been taken"]}
I honestly am at a loss, I have spent quite a bit of time digging into it as well as searching Google and I cannot find an answer as to why this is happening.
You are right, Rails does only "track" the dirty columns and generates the minimum update statement necessary. If you look in your log/development.log file you will see the actual SQL that is being generated, and you'll see that the update statement is only touching the columns you have edited. At least you would if your code was getting that far.
Before saving your model, Rails will run all the validations on it, and that includes seeing if the referral code is unique. To do this it will run a select SQL statement against the database to check; if you look in the development.log file you will definitely see this query.
So Rails is working correctly here.
If your referral codes are supposed to be unique, why aren't they? My guess would be that you are trying to save models with a nil or blank code. If that is the case, try adding :allow_nil => true or :allow_blank => true to the :uniqueness hash.

Resources