find_or_create_xxx behaving differently than create

It appears that find_or_create_by_xxx is attempting type conversion
where a simple create(...) does not. Bug?

Before going further, here's the schema and the model:

  create_table "weather_stations", :force => true do |t|
    t.string "callsign"
    t.decimal "lat", :precision => 9, :scale => 6
    t.decimal "lng", :precision => 9, :scale => 6
    t.decimal "elevation_m", :precision => 5, :scale => 1
  end

class WeatherStation < ActiveRecord::Base
  validates_presence_of :callsign, :lat, :lng
  validates_uniqueness_of :callsign, :case_sensitive => false
  acts_as_mappable
  ...
end

[Note the "validates_uniqueness_of :callsign, :case_sensitive => false"
-- that comes into play here.]

** Assume 'specs' is a hash whose :callsign is a string:

specs

=> {:callsign=>"0N6", :lat=>39.0128889, :lng=>-75.5339722,
:elevation_m=>15.0}

specs[:callsign].class

=> String

** The call to find_or_create_by_callsign fails -- note that id comes
back nil and the other fields are not filled in:

w = WeatherStation.find_or_create_by_callsign(specs[:callsign], :lat => specs[:lat], :lng => specs[:lng])

=> #<WeatherStation id: nil, callsign: "0N6", domain: nil, created_at:
nil, updated_at: nil, lat: nil, lng: nil, elevation_m: nil>

** The generated SQL shows a rollback:

  WeatherStation Load (22.2ms) SELECT * FROM `weather_stations` WHERE
(`weather_stations`.`callsign` = '0N6') LIMIT 1
  SQL (0.4ms) BEGIN
  WeatherStation Load (230.6ms) SELECT `weather_stations`.id FROM
`weather_stations` WHERE (LOWER(`weather_stations`.`callsign`) = BINARY
'0n6') LIMIT 1
  SQL (1.0ms) ROLLBACK

** Using a simple .create() instead works:

x = WeatherStation.create(:callsign => specs[:callsign], :lat => specs[:lat], :lng => specs[:lng])

=> #<WeatherStation id: 30774, callsign: "0N6", domain: nil, created_at:
"2010-05-16 06:38:22", updated_at: "2010-05-16 06:38:22", lat:
#<BigDecimal:8db79b0,'0.390128889E2',12(16)>, lng:
#<BigDecimal:8db7828,'-0.755339722E2',12(16)>, elevation_m: nil>

** ...but looking at the SQL shows that it does the same check for
uniqueness before inserting the row. Since the record isn't in the
table, it's okay that it fails:

SQL (70.0ms) BEGIN
  WeatherStation Load (365.4ms) SELECT `weather_stations`.id FROM
`weather_stations` WHERE (LOWER(`weather_stations`.`callsign`) = BINARY
'0n6') LIMIT 1
  WeatherStation Create (2.0ms) INSERT INTO `weather_stations`
(`callsign`, `domain`, `created_at`, `updated_at`, `lat`, `lng`,
`elevation_m`) VALUES('0N6', NULL, '2010-05-16 06:38:22', '2010-05-16
06:38:22', 39.0128889, -75.5339722, NULL)
  SQL (3.3ms) COMMIT

I don't understand enough about SQL's "BINARY '0n6'" -- is that the
source of the trouble? Or is my validation bogus? Why is
find_or_create_by_callsign() failing?

TIA.

- ff

I don't understand enough about SQL's "BINARY '0n6'" -- is that the
source of the trouble? Or is my validation bogus? Why is
find_or_create_by_callsign() failing?

I think it's far more likely that it's the nil attributes you noted
that are the problem - they would certainly prevent the record from
being saved.
It appears that find_or_create_by doesn't quite work the way you
expected it to - the code and unit tests suggests that you should be
doing

WeatherStation.find_or_create_by_callsign(:callsign =>
specs[:callsign], :lat => specs[:lat], :lng => specs[:lng])

There is an another example in the docs of the usage you were
attempting though, looks like it is just wrong/misleading.

Fred

Frederick Cheung wrote:

...
It appears that find_or_create_by doesn't quite work the way you
expected it to - the code and unit tests suggests that you should be
doing

WeatherStation.find_or_create_by_callsign(:callsign =>
specs[:callsign], :lat => specs[:lat], :lng => specs[:lng])

You are correct -- that works as expected.

There is an another example in the docs of the usage you were
attempting though, looks like it is just wrong/misleading.

Correct again! The line in the doc is:

  # No 'Summer' tag exists
  Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name =>
"Summer")

which passes "Summer" rather than :name => "Summer".

Sigh. A guy I used to work with told me that there is only one
"authoritative documentation", and that's the source code itself.
Evidently, Rails subscribes to this philosophy as well.

Frederick Cheung wrote:
> ...

> There is an another example in the docs of the usage you were
> attempting though, looks like it is just wrong/misleading.

Correct again! The line in the doc is:

# No 'Summer' tag exists
Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name =>
"Summer")

Actually that usage is correct too. The misleading example is actuall
demonstrating a different point.

which passes "Summer" rather than :name => "Summer".

Sigh. A guy I used to work with told me that there is only one
"authoritative documentation", and that's the source code itself.
Evidently, Rails subscribes to this philosophy as well.

A fair amount of effort has gone into the rails documentation of late.
Like the rest of rails it only gets better if people contribute

Frederick Cheung wrote:

A fair amount of effort has gone into the rails documentation of late.
Like the rest of rails it only gets better if people contribute

Agreed. But, as evidenced by my sometimes boneheaded questions in this
forum, Rails would be in serious trouble if I were a contributor! :slight_smile:
(At least for now...)