Created
October 4, 2011 16:11
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = "[email protected]" | |
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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