Skip to content

Instantly share code, notes, and snippets.

@zerowidth
Created October 4, 2011 16:11
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zerowidth/1262051 to your computer and use it in GitHub Desktop.
Save zerowidth/1262051 to your computer and use it in GitHub Desktop.
Let postgres insert default values for columns with default values that AR 3.1 doesn't understand
Gem::Specification.new do |s|
s.name = "ar_pg_defaults"
s.version = "0.0.1"
s.platform = Gem::Platform::RUBY
s.author = "Nathan Witmer"
s.email = "nwitmer@gmail.com"
s.homepage = "https://gist.github.com/1262051"
s.summary = "Help AR let postgres do its thing"
s.description = "Let postgres insert default values for columns with default values that AR 3.1 doesn't understand"
s.files = ["ar_pg_defaults.rb"]
s.test_file = "ar_pg_defaults_spec.rb"
s.require_path = "."
s.add_dependency "active_record", "~> 3.1.0"
s.add_dependency "pg", "~> 0.11"
s.add_development_dependency "rspec", "~> 2.0"
end
# ActiveRecord 3.1 uses prepared statements for all DB activity. This
# includes inserts. Unfortunately, AR is unable to handle default values
# that it doesn't understand. Instead, it will automatically insert a
# NULL for those column. This doesn't work when the column is
# non-nullable!
#
# This monkeypatch forces AR models to *not* insert attributes into the
# database that have been left as the (unknown) database default value.
#
# The original approach in AR 2.x was to insert DEFAULT literals as a
# placeholder (see https://gist.github.com/449251) but the pg bindings don't
# appear to allow literal/keyword values in prepared statements. Instead, hack
# the attributes hash directly when creating new records.
require "active_record/connection_adapters/postgresql_adapter"
ActiveRecord::ConnectionAdapters::PostgreSQLColumn.module_eval do
class << self
# Override the default value extraction so that it sets the default
# value of a column with an unknown default to the symbol :default.
# This allows the database to automatically insert the default value
# when creating a new record.
def extract_value_from_default_with_unknowns(default)
value = extract_value_from_default_without_unknowns(default)
if !default.nil?
value = :default
end
value
end
alias_method_chain :extract_value_from_default, :unknowns
end
# A primary key column can't have a default value of :default, as it
# confuses the record instantiation process. Catch that here and return
# nil, otherwise return the usual (normally nil) default value.
#
# Based on how the :default value is typecast when setting the column
# defaults, it actually ends up being an integer (see: :default.to_i)!
# In any case, kill that here.
def default
primary ? nil : @default
end
end
# Furthermore, prevent typecasting of :default to other types
ActiveRecord::ConnectionAdapters::Column.module_eval do
def type_cast_with_default(value)
value == :default ? value : type_cast_without_default(value)
end
alias_method_chain :type_cast, :default
end
ActiveRecord::Base.module_eval do
def self.unknown_defaults
@unknown_defaults ||= columns.select { |c| c.default == :default}.map(&:name)
end
before_save(:on => :create) do
self.class.unknown_defaults.each do |column|
# Directly remove attributes that have been set to the "unknown" default
# value. This prevents them from being included in the INSERT
# statement altogether.
if @attributes[column] == :default
@attributes.delete(column)
end
end
end
after_save(:on => :create) do
unknown = self.class.unknown_defaults
return if unknown.empty?
# Automatically reload the defaulted values after creating a new
# record.
values = self.class.find(id, :select => unknown.join(", ")).attributes
self.attributes = values
end
end
require "active_record"
require "ar_pg_defaults"
describe ActiveRecord::Base, "with a default that AR doesn't understand" do
before :all do
ActiveRecord::Base.establish_connection :host => "localhost",
:database => "postgres",
:username => "postgres",
:password => "",
:adapter => "postgresql"
ActiveRecord::Base.connection.execute <<-sql
create temporary table things(
-- this default is ok still, replace it below
id integer not null primary key,
-- AR is confused by this
number integer default '12345'::integer,
-- and also by this
created_time timestamp(0) with time zone DEFAULT ('now'::text)::timestamp
);
-- now, create a default value for the primary key that will confuse
-- activerecord when it creates new records.
create temporary sequence things_id_seq start with 1 increment by 1;
alter sequence things_id_seq owned by things.id;
alter table things alter column id set default nextval('things_id_seq'::regclass);
sql
end
after :all do
ActiveRecord::Base.connection.disconnect!
end
let(:connection) { ActiveRecord::Base.connection }
let(:model) do
Class.new(ActiveRecord::Base) do
set_table_name "things"
end
end
it "has an nil column default for the primary key" do
model.column_defaults["id"].should == nil
end
it "has :default as the default value as an integer column" do
model.column_defaults["number"].should == :default
end
it "lets the database insert the default value if it doesn't understand it" do
record = model.create
record.reload.created_time.should_not be_nil
end
it "does not require a manual reload to see the database's inserted value" do
record = model.create
record.created_time.should_not be_nil
end
it "does not override the default value for the primary key" do
record = model.create
record.id.should > 1
record.id.should < 10
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment