0

I'm working with a legacy MySQL database that I'm trying to interact with using Rails, and I get an error when creating records:

2.7.2 :005 > HashRef.create!(hash: '00080e597c26a05197aaa4462e53cdf9847b56a4d5d47e550fdc01554434df8gg2fffsss')
  TRANSACTION (0.2ms)  BEGIN
  HashRef Create (0.3ms)  INSERT INTO `hash_ref` (`hash`) VALUES ('00080e597c26a05197aaa4462e53cdf9847b56a4d5d47e550fdc01554434df8gg2fffsss')
  TRANSACTION (4.7ms)  COMMIT
Traceback (most recent call last):
        2: from (irb):4
        1: from (irb):5:in `rescue in irb_binding'
TypeError (no implicit conversion of String into Integer)

It would appear that this is because the DB table has a string column named hash (the primary key) which is conflicting with some Rails stuff.

CREATE TABLE `hash_ref` (
  `hash` varchar(512) NOT NULL,
  `cipher` varchar(512) DEFAULT NULL,
  `clear` varchar(512) DEFAULT NULL,
  `record_insert_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`hash`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

I can alias the attribute, but this raises the same error when writing.

  alias_attribute :my_hash, :hash

Unfortunately renaming the column is not an option at this point.

I would have thought there was some way to proxy a column name, so I can refer to it as my_hash in the code but when it queries the DB it uses hash.

I've seen this monkey patch proposed in another answer, which seems to work, but I'm wondering if it's safe to do:

class << self
    def instance_method_already_implemented?(method_name)
      return true if method_name == 'hash'
      super
    end
  end

There's also a gem suggested, but it hasn't been updated for many years:

https://github.com/bjones/safe_attributes

The only thing I can think of is to use raw SQL for writing to this DB, but it's very awkward when writing RSpec tests to not be able to use the model or factories.

Thanks for any help

Dave McGinn
  • 149
  • 4
  • 9
  • Honestly I don't know what will work best, this is quite an unfortunate position you're in. I'd need to play around with different ideas to try what works, but I guess you're in a much better position to do that than I am right now. – Tom Lord Aug 06 '21 at 09:58
  • However, one additional possibility I found while looking was [here](https://stackoverflow.com/a/1510938/1954610): Maybe you can use something like `default_scope :select=> 'hash as hash_value'` to rename the attribute? – Tom Lord Aug 06 '21 at 10:00
  • 1
    But as answered below, renaming the column could really just be the easiest solution?... I know you said "it's not an option", but perhaps whatever your rationale for that statement would be overruled by the complexity/danger of a different workaround. – Tom Lord Aug 06 '21 at 10:01

2 Answers2

2

The method you're clashing with is Object#hash from the Ruby core and not Rails. This is how the Hash class looks up if two hash keys are identical. Thats why you're getting TypeError (no implicit conversion of String into Integer) as the method is expected to return an integer.

This is pretty hacky but circumvents the issue:

# IMPORTANT!
# the 'hash' column is available as hash_value as the name collides a method from the 
# ruby core
class Hashref < ApplicationRecord
  self.primary_key = :hash
  
  # avoids clobbering the hash method from Object
  def self.define_attribute_method(attr_name, **opts)
    super unless attr_name == "hash"
  end

  # feel free to rename this to whatever you want
  def hash_value
    read_attribute(:hash)
  end

  def hash_value=(value)
    write_attribute(:hash, value)
  end
end
Loading development environment (Rails 6.1.3.2)
irb(main):001:0> hashref = Hashref.create(hash_value: 'abcd1234')
   (0.5ms)  SELECT sqlite_version(*)
  TRANSACTION (0.1ms)  begin transaction
  Hashref Create (8.2ms)  INSERT INTO "hashrefs" ("hash", "created_at", "updated_at") VALUES (?, ?, ?)  [["hash", "abcd1234"], ["created_at", "2021-08-06 12:54:56.406910"], ["updated_at", "2021-08-06 12:54:56.406910"]]
  TRANSACTION (5.7ms)  commit transaction
irb(main):002:0> hashref.hash
=> 2355933382791539607
irb(main):003:0> hashref.hash_value
=> "abcd1234"

The key here will probally be lots of tests and documentation so that you don't accidentially use hash instead of hash_value.

max
  • 96,212
  • 14
  • 104
  • 165
  • I suspect that `self.primary_key = :hash` may cause issues if Object#hash is for example used for comparisons instead of the actual id. YMMV. – max Aug 06 '21 at 13:46
-1

yeah, hash is defined by Active Record. The solution you provided is dangerous. I do not recommend using it. Instead, I advise you to rename a field name

  • This is both technically incorrect and more of a comment then an actual answer. – max Aug 06 '21 at 12:36