6

I'm trying to access a column's default value in my Postgres 9.2 database. By using raw SQL, I can verify that the column default is "users_next_id()":

> db = ActiveRecord::Base.connection
> db.execute("SELECT table_name,column_name,column_default 
FROM information_schema.columns 
WHERE table_name = 'users' and column_name ='id'").first

=> {"table_name"=>"users",
 "column_name"=>"id",
 "column_default"=>"users_next_id()"}

But when I use AR's 'columns' method, the default value appears to be nil:

[26] pry(main)> db.columns('users')[0]=> #<ActiveRecord::ConnectionAdapters::PostgreSQLColumn:0x007feb397ba6e8
 @coder=nil,
 @default=nil,
 @limit=8,
 @name="id",
 @null=false,
 @precision=nil,
 @primary=nil,
 @scale=nil,
 @sql_type="bigint",
 @type=:integer>

This isn't causing any problems (other than confusing me), but is this expected behavior? Am I making an incorrect assumption about the 'columns' method?

MothOnMars
  • 2,229
  • 3
  • 20
  • 23
  • 1
    **Short answer**: AR doesn't know what `users_next_id()` means so it pretends it doesn't exist; there is special handling for `id` columns so the incorrect default value from `columns` doesn't cause trouble (but only in the specific case of `id`). **Long answer**: this comes later, gotta' run right now. – mu is too short Mar 06 '13 at 00:33
  • Thanks for that info. I must have been doing something wrong before, because now I see the defaults for other columns. Your answer makes perfect sense. I thought the IDs might be special; thanks for confirming that, which made me double-check my data. (And thanks for the upvote -- we noobs need all the encouragement we can get. :)) – MothOnMars Mar 06 '13 at 00:50
  • 2
    This is a good question as ActiveRecord's behavior here is hardly intuitive. As usual with Rails, the only way to figure out WTF is going is to take a tour through the Rails source and that's a pretty advanced topic. And hey, everyone starts as a noob. – mu is too short Mar 06 '13 at 04:34

1 Answers1

6

When ActiveRecord needs to know about a table it does a query similar to your information_schema query but AR will go through the PostgreSQL-specific system tables instead:

  SELECT a.attname, format_type(a.atttypid, a.atttypmod),
         pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod
    FROM pg_attribute a LEFT JOIN pg_attrdef d
      ON a.attrelid = d.adrelid AND a.attnum = d.adnum
   WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
     AND a.attnum > 0 AND NOT a.attisdropped
ORDER BY a.attnum

Search the PostgreSQL adapter source for "regclass" and you'll see some other queries that AR will use to figure out the table's structure.

The pg_get_expr call in the above query is where the column's default value comes from.

The results of that query go, more or less, straight into PostgreSQLColumn.new:

def columns(table_name, name = nil)
  # Limit, precision, and scale are all handled by the superclass.
  column_definitions(table_name).collect do |column_name, type, default, notnull|
    PostgreSQLColumn.new(column_name, default, type, notnull == 'f')
  end
end

The PostgreSQLColumn constructor will use extract_value_from_default to Ruby-ify the default; the end of the switch in extract_value_from_default is interesting here:

else
  # Anything else is blank, some user type, or some function
  # and we can't know the value of that, so return nil.
  nil

So if the default value is bound to a sequence (which an id column in PostgreSQL will be), then the default will come out of the database as a function call similar to this:

nextval('models_id_seq'::regclass)

That will end up in the above else branch and column.default.nil? will be true.

For an id column this isn't a problem, AR expects the database to supply the values for id columns so it doesn't care what the default value is.

This is a big problem if the column's default is something that AR doesn't understand, say a function call such as md5(random()::text). The problem is that AR will initialize all the attributes to their default values – as Model.columns sees them, not as the database sees them – when you say Model.new. For example, in the console you'll see things like this:

 > Model.new
=> #<Model id: nil, def_is_function: nil, def_is_zero: 0>

So if def_is_function actually uses a function call as its default value, AR will ignore that and try to insert a NULL as that column's value. That NULL will prevent the default value from being used and you'll end up with a confusing mess. Defaults that AR can understand (such as strings and numbers) work just fine though.

The result is that you can't really use non-trivial default column values with ActiveRecord, if you want a non-trivial value then you have to do in Ruby through one of the ActiveRecord callbacks (such as before_create).

IMO it would be much better if AR left the default values up to the database if it didn't understand them: leaving them out of the INSERT or using DEFAULT in the VALUES would produce much better results; AR would, of course, have to reload newly created objects from the database in order to get all the proper defaults but you'd only need the reload if there were defaults that AR didn't understand. If the else in extract_value_from_default used a special "I don't know what this means" flag instead of nil then the "I need to reload this object after the first save" condition would be trivial to detect and you'd only reload when necessary.


The above is PostgreSQL-specific but the process should be similar for other databases; however, I make no guarantees.

Community
  • 1
  • 1
mu is too short
  • 426,620
  • 70
  • 833
  • 800
  • That's incredibly helpful. I really appreciate the detailed answer. I am still finding my way around the pg_catalog data, so it's helpful to see what PG is doing on the backend, as well as to see the AR connection adapter code. Your answer also reminded me to tail my PG dev log to see what internal PG queries are being generated by my ActiveRecord calls. Many thanks! – MothOnMars Mar 06 '13 at 18:13