Array of strings not converted to arrays of integers - ruby-on-rails

I'm trying to understand the following behaviour:
irb(main):016:0* pg = ProductGroup.find(1)
irb(main):017:0> pg.good_type_ids
=> [1]
irb(main):018:0> pg.update({"good_type_ids"=>["", "1"]})
irb(main):019:0> pg.changed?
=> false
irb(main):020:0> pg.good_type_ids
=> ["1"]
The code above mimics a form update. The good_type_ids are selected ids's of a multi select. Although the value of the array changes from [1] to ["1"], the changed? method returns false. The update method seems also be smart enough to strip the empty string.
irb(main):021:0> pg = ProductGroup.find(1)
irb(main):022:0> pg.good_type_ids
=> [1]
irb(main):023:0> pg.attributes = {"good_type_ids"=>["", "1"]}
=> {"good_type_ids"=>["", "1"]}
irb(main):024:0> pg.changed?
=> true
irb(main):025:0> pg.good_type_ids
=> ["", "1"]
In this second example I'm trying to apply the changed params from the form to the object. I don't want to save it to the dbs!
Somehow the attributes method behaves different than the update method and good_type_ids will be ["","1"] after the method was invoked.
It seems that the type of the column in the dbs is taken into account by the "update" and "attributes" method, only for arrays (postgress) this doesn't seem to work.
irb(main):030:0* pg.attributes = {"recourse_days"=>5}
=> {"recourse_days"=>5}
irb(main):031:0> pg.changed?
=> true
irb(main):039:0* pg.recourse_days
=> 5
recourse_days are defined as integer, during post the controller receives a string and converts it correctly to an integer.
Schema.rb: t.integer "good_type_ids", default: [], array: true
As temporary work arround I have put this in the update method of the controller
params["good_type_ids"].reject!(&:blank?)
params["good_type_ids"].map!(&:to_i)

UPD
Strings conversion issues and nil elements saving to pgsql seems to be fixed in rails 4.2.0, i've heard whole type casting system is pretty much revised there:
> doc.test_digit = ["1"] #=> ["1"]
> doc.test_digit #=> [1]
> doc.test_string #=> ["2"]
> doc.test_string = [2] #=> [2]
> doc.test_string #=> ["2"]
> doc.attributes = {'test_digit'=>["", "2"]}
=> {"test_digit"=>["", "2"]}
> doc.test_digit #=> [nil, 2]
> doc.save
(0.9ms) BEGIN
SQL (1.8ms) UPDATE "documents" SET "test_digit" = $1, "updated_at" = $2 WHERE "documents"."id" = $3 [["test_digit", "{NULL,2}"], ["updated_at", "2015-02-26 00:32:25.326735"], ["id", 1]]
(3.7ms) COMMIT
=> true
> doc.reload
Document Load (1.7ms) SELECT "documents".* FROM "documents" WHERE "documents"."id" = $1 LIMIT 1 [["id", 1]]
=> #<Document id: 1, name: "test1", test_digit: [nil, 2], test_string: ["2"], created_at: "2015-02-25 23:51:32", updated_at: "2015-02-26 00:32:25">
> doc.test_digit #=> [nil, 2]
I used following migrations:
t.integer :test_digit, array: true, default: []
t.string :test_string, array: true, default: []
Update method updates the db, thus your model attributes are no longer dirty.
Good guide on this
The update method seems also be smart enough to strip the empty string.
This is not normal behavior, according to the source of pgsql adapter/array_parser, empty array elements should be saved and loaded as any other values. (this functionality was added on Aug 20, 2012 to the adapter just in case you are using an older version)

Related

Update to array in ActiveRecord column not saving

I have a Postgres database. One of the columns in one of my tables consists of arrays. It's called aliases. I'm trying to merge two instances but when I try to merge the arrays they are not saving.
irb(main):001:0> original
=> #<Thing id: 1, name: "Foo", aliases: ["Foo"]>
irb(main):002:0> duplicate
=> #<Thing id: 2, name: "Bar", aliases: ["Bar"]>
irb(main):003:0> original.aliases | duplicate.aliases
=> ["Foo", "Bar"]
irb(main):004:0> original.save!
(0.3ms) BEGIN
Thing Exists (0.8ms) SELECT 1 AS one FROM "thing" WHERE "things"."name" = $1 AND ("thing"."id" != $2) LIMIT $3 [["name", "Foo"], ["id", 1], ["LIMIT", 1]]
(0.3ms) COMMIT
Thing Store (6.1ms) {"id":1}
=> true
But then when I check original the aliases have not merged.
irb(main):005:0> original
=> #<Thing id: 1, name: "Foo", aliases: ["Foo"]>
I tried several different ways to insert duplicate.aliases into original.aliases but nothing seems to be saving. Any ideas why? Am I missing something?
Edit:
I'm on Rails 5.1.4 and Ruby 2.5.0p0
Are you on Rails 3? I've noticed that Rails3 activerecord array handling does not like inline updates. You need to use a temporary variable, augment that variable and then assign it back.
aliases = original.aliases # make a temp variable
aliases = aliases | duplicate.aliases # modify temp variable
original.aliases = aliases # assign back
original.save! # now you can save
I encountered this myself when I was trying to push onto an array:
original.aliases << "something" # aliases would never get changed

Allow Only Specific Words for Attribute

How do you restrict strings allowed in a string field to specific words?
Example: I have a model attribute animal:string that I would like to exclusively accept ["dog", "cat", "bird", "fish"]. Every thing else would make the object invalid.
Add an inclusion validation to your model:
validates :animal, inclusion: { in: %w(dog cat bird fish) }
As I said, I'd go with Rails Enum feature.
class ModelName < ActiveRecord::Base
enum animals: %w(dog cat)
# ...
end
There is one gotcha which you might notice since this is called enum: you will need to update your database column to be an integer value. Rails will do implicit mapping between the integer value and the index of the value in the array automatically
If you go with Enum feature, having the single line of code, Rails will generate several helper methods for you:
# Query method
model_name.dog?
model_name.cat?
#..
# Action method
model_name.cat!
model_name.dog!
#..
# List of statuses and their corresponding values in the database.
model_name.animals
Tried and Tested :
[arup#app]$ rails c
Loading development environment (Rails 4.1.1)
[1] pry(main)> Pet.create!(animals: 'cat')
(0.3ms) BEGIN
SQL (0.9ms) INSERT INTO "pets" ("animals", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["animals", 1], ["created_at", "2015-02-19 18:27:28.074640"], ["updated_at", "2015-02-19 18:27:28.074640"]]
(42.2ms) COMMIT
=> #<Pet id: 5, animals: 1, created_at: "2015-02-19 18:27:28", updated_at: "2015-02-19 18:27:28">
[2] pry(main)> Pet.create!(animals: 'cow')
ArgumentError: 'cow' is not a valid animals
from /home/arup/.rvm/gems/ruby-2.1.2#app/gems/activerecord-4.1.1/lib/active_record/enum.rb:103:in 'block (3 levels) in enum'
[3] pry(main)> Pet.animals
=> {"dog"=>0, "cat"=>1}
[5] pry(main)> Pet.first.dog?
Pet Load (0.8ms) SELECT "pets".* FROM "pets" ORDER BY "pets"."id" ASC LIMIT 1
=> false
[6] pry(main)> Pet.first.cat?
Pet Load (0.7ms) SELECT "pets".* FROM "pets" ORDER BY "pets"."id" ASC LIMIT 1
=> true
[7] pry(main)> Pet.first.cow?
Pet Load (0.7ms) SELECT "pets".* FROM "pets" ORDER BY "pets"."id" ASC LIMIT 1
NoMethodError: undefined method 'cow?' for #<Pet:0xbf8455c>
from /home/arup/.rvm/gems/ruby-2.1.2#app/gems/activemodel-4.1.1/lib/active_model/attribute_methods.rb:435:in `method_missing'
[8] pry(main)>
Pet.create!(animals: 'cow') throws error, which confirmed that, Pet model wouldn't accept anything other than the enum values.
you can use select field in your form, and write this in your model:
module Animal
dog = 1
cat = 2
bird = 3
fish = 4
end
and in tour form:
<%= f.select :animal, { "dog" => 1, "cat" => 2, "bird" => 3, "fish" => 4} %>

rspec serialized Hash - query empty for records

I have a model called Content, with a column called dependencies, serialized as Hash:
class Content < ActiveRecord::Base
attr_accessible :dependencies
serialize :dependencies, Hash
end
This really killed my nerves for the last few hours. I'll appreciate any help/hint.
Questions:
What should be the default (empty) value in migration?
What should be the default (empty) value in FactoryGirl?
Most important - how to query in order to find empty values?
Thanks in advance!
What should be the default (empty) value in migration?
What should be the default (empty) value in FactoryGirl?
In both cases, the empty hash {}
Most important - how to query in order to find empty values?
Since serialized values are stored using YAML, you need to search as follows:
Content.where('dependencies = ?', {}.to_yaml)
Here's an irb transcription for my test of the above:
MacbookAir1:so1 palfvin$ rails c
Loading development environment (Rails 4.0.0)
2.0.0-p247 :001 > u = User.new(role: {})
=> #<User id: nil, role: {}, role2: nil>
2.0.0-p247 :002 > u.save
(0.3ms) begin transaction
SQL (3.3ms) INSERT INTO "users" ("role", "role2") VALUES (?, ?) [["role", "--- {}\n"], ["role2", nil]]
(1.1ms) commit transaction
=> true
2.0.0-p247 :003 > u.role
=> {}
2.0.0-p247 :004 > {}.to_yaml
=> "--- {}\n"
2.0.0-p247 :005 > u
=> #<User id: 4, role: {}, role2: nil>
2.0.0-p247 :006 > User.where(role: {}.to_yaml)
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."role" = '--- {}
'
=> #<ActiveRecord::Relation [#<User id: 3, role: {}, role2: nil>, #<User id: 4, role: {}, role2: nil>]>
2.0.0-p247 :007 >
(Note: I had created a User instance (#3) prior to posting the first version of this answer, which is why that shows up in my where as well).
And here's my user.rb file:
class User < ActiveRecord::Base
has_many :who_rated_comment_rels, foreign_key: "user_id", dependent: :destroy
serialize :role, Hash
serialize :role2
end
You can ignore the stuff not-relevant to your case (i.e. anything other than role). I hack on this project for various StackOverflow purposes.

Rails serialization converts 0 to String

I've got a model that uses the default Rails serialization to serialize an array. Behold:
class Account < ActiveRecord::Base
serialize :number_of_free_jobs
end
number_of_free_jobs is an array of FixNums, but for some reason Rails is converting all the entries that are 0 into strings, but leaving the rest as FixNums, check it:
account = Account.last
account.number_of_free_jobs = [10, 5, 0, 1]
account.save
account.number_of_free_jobs
=> [10, 5, "0", 1]
Converting the 0 to a String means some comparisons are failing. I can just map the array with to_i but I'm curious as to why this is happening. I'm using Rails 3.2.13 and Ruby 1.9.3
This doesn't occur on my machine(Sqlite3, Rails 3.2.13 & Ruby 1.9.3):
Loading development environment (Rails 3.2.13)
irb(main):001:0> Account
=> Account(id: integer, number_of_free_jobs: string, created_at: datetime, updated_at: datetime)
irb(main):002:0> Account.create
(0.1ms) begin transaction
SQL (9.5ms) INSERT INTO "accounts" ("created_at", "number_of_free_jobs", "updated_at") VALUES (?, ?, ?) [["created_at", Mon, 20 May 2013 18:19:26 UTC +00:00], ["number_of_free_jobs", nil], ["updated_at", Mon, 20 May 2013 18:19:26 UTC +00:00]]
(179.1ms) commit transaction
=> #<Account id: 2, number_of_free_jobs: nil, created_at: "2013-05-20 18:19:26", updated_at: "2013-05-20 18:19:26">
irb(main):003:0> account = Account.last
Account Load (0.3ms) SELECT "accounts".* FROM "accounts" ORDER BY "accounts"."id" DESC LIMIT 1
=> #<Account id: 2, number_of_free_jobs: nil, created_at: "2013-05-20 18:19:26", updated_at: "2013-05-20 18:19:26">
irb(main):004:0> account.number_of_free_jobs = [10, 5, 0, 1]
=> [10, 5, 0, 1]
irb(main):005:0> account.save
(0.1ms) begin transaction
(0.4ms) UPDATE "accounts" SET "number_of_free_jobs" = '---
- 10
- 5
- 0
- 1
', "updated_at" = '2013-05-20 18:19:46.430558' WHERE "accounts"."id" = 2
(155.9ms) commit transaction
=> true
irb(main):006:0> account.number_of_free_jobs
=> [10, 5, 0, 1]
Can you provide steps to reproduce this issue? perhaps a sample app on github which has this error/bug/behaviour?
The problem was with the Tolk gem installing safe_yaml as a dependency. Removing Tolk removed the dependency and zeroes are no longer being converted to strings. Bit of a strange one, I'll look into why safe_yaml does this...

Strange ActiveRecord model behavior

I have model
class Owner < ActiveRecord::Base
attr_accessible :telephone
validates_uniqueness_of :telephone
validates_telephone_number_of :telephone
before_validation :telephone_normalize
end
in rails console
a = Owner.new(:telephone => '949123456')
=> #<Owner id: nil, telephone: "949123456", created_at: nil, updated_at: nil>
1.9.3-p362 :002 > a.valid?
Owner Exists (0.1ms) SELECT 1 AS one FROM "owners" WHERE "owners"."telephone" = '+421949123456' LIMIT 1
=> false
1.9.3-p362 :003 > a
=> #<Owner id: nil, telephone: "421949123456", created_at: nil, updated_at: nil>
The same, when I save unique number:
1.9.3-p362 :006 > a.telephone = '949123457'
=> "949123457"
1.9.3-p362 :007 > a.save
(0.1ms) begin transaction
Owner Exists (0.2ms) SELECT 1 AS one FROM "owners" WHERE "owners"."telephone" = '+421949123457' LIMIT 1
SQL (2.3ms) INSERT INTO "owners" ("created_at", "telephone", "updated_at") VALUES (?, ?, ?) [["created_at", Wed, 16 Jan 2013 11:55:44 UTC +00:00], ["telephone", "421949123457"], ["updated_at", Wed, 16 Jan 2013 11:55:44 UTC +00:00]]
(88.3ms) commit transaction
=> true
Rails (3.2.11) omits '+' in the beginning of number. Type of number is string. It also saves it without plus sign (if it is unique), but when validating, it calls with plus sign.
What am I doing wrong?
It think telephone column in database is integer type. So the string you have passed is out of its range. That's why you have face this problem.
Unfortunetaly there was bug in my validates_telephone_number_of validator. It had modified attribute :-/
Say
> a = 'aaa' # => 'aaa'
> b = a.to_s # => 'aaa'
> b << 'c' # => 'aaac'
> b # => 'aaac'
> a # => 'aaac'
It is needed to use b = a.to_s.dup.

Resources