Search code examples
ruby-on-railsrubyaliassequel

Aliasing Column with Hashtag in Name in Rails


I have a legacy database with a column named similar to My#Column which I am trying to alias. In my Sequel model I have:

alias_attribute :MyColumn, :"My#Column"

But I get a syntax error:

...Ruby24-x64/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/core_ext/module/aliasing.rb:26: syntax error, unexpected end-of-input, expecting keyword_end

The problem seems to be the #. I have tried escaping it like \# but I get the same error. I don't understand why I get a syntax error as this kind of symbol has worked for me in other places.

What do I do to get this alias to work?


Solution

  • This is what alias_attribute is actually doing Source

      module_eval <<-STR, __FILE__, __LINE__ + 1
        def #{new_name}; self.#{old_name}; end          # def subject; self.title; end
        def #{new_name}?; self.#{old_name}?; end        # def subject?; self.title?; end
        def #{new_name}=(v); self.#{old_name} = v; end  # def subject=(v); self.title = v; end
      STR
    

    So essentially this is becoming

      def MyColumn; self.My#Column; end
      def MyColumn?; self.My#Column?; end
      def MyColumn=(val); self.My#Column= val; end
    

    Notice the fact that this is in a single line which means that everything after self.My becomes a comment (including the end) and thus the error you are receiving. Even if this was not a single line in rails ruby would simply raise a NoMethodError for the fact that My is not a method because the #Column portion would be treated as a comment.

    This also seems strange since ActiveModel#alias_attribute has a functional implementation of the exact same via define_proxy_call which looks like

    def define_proxy_call(include_private, mod, name, send, *extra)
      defn = if NAME_COMPILABLE_REGEXP.match?(name)
         "def #{name}(*args)"
      else
         "define_method(:'#{name}') do |*args|"
      end
    
      extra = (extra.map!(&:inspect) << "*args").join(", ".freeze)
    
      target = if CALL_COMPILABLE_REGEXP.match?(send)
        "#{"self." unless include_private}#{send}(#{extra})"
      else
        "send(:'#{send}', #{extra})"
      end
    
      mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
        #{defn}
          #{target}
        end
        RUBY
    end
    

    Here you can see it actually checks to see if the new name(name) and original name(send) are "compilable" and if not it handles them appropriately.

    Rather than alias_attribute which is going to have issues with what is essentially a comment character. I would suggest manually implementing the same using public_send e.g.

      def MyColumn
        self.public_send("My#Column") 
      end 
      def MyColumn=(val)
        self.public_send("My#Column=",val)
      end
      def MyColumn?
        self.public_send("My#Column?")
      end
    

    This should result in the same but without the syntax issues.