Skip to content

Commit

Permalink
Add column_conflicts plugin to automatically handle columns that conf…
Browse files Browse the repository at this point in the history
…lict with method names

This makes it easy to have Sequel automatically handle column
names that conflict with method names.  Just load the plugin
into the model, and it will override get_column_value and
set_column_value appropariately to set the conflict values
directly in the values hash instead of calling the method.
  • Loading branch information
jeremyevans committed Jan 7, 2015
1 parent aac7220 commit b3626bf
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
=== HEAD

* Add column_conflicts plugin to automatically handle columns that conflict with method names (#929)

* Add Model#get_column_value and #set_column_value to get/set column values (jeremyevans) (#929)

=== 4.18.0 (2015-01-02)
Expand Down
93 changes: 93 additions & 0 deletions lib/sequel/plugins/column_conflicts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
module Sequel
module Plugins
# The column_conflicts plugin overrides Model#get_column_value and #set_column_value
# to automatically handle column names that conflict with Ruby/Sequel method names.
#
# By default, Model#get_column_value and #set_column_value just call send, this
# plugin overrides the methods and gets/sets the value directly in the values
# hash if the column name conflicts with an existing Sequel::Model instance
# method name.
#
# Checking for column conflicts causes a performance hit, which is why Sequel
# does not enable such checks by default.
#
# When using this plugin, you can manually update the columns used. This may be useful if
# the columns conflict with one of your custom methods, instead of a method defined in
# Sequel::Model:
#
# Album.plugin :column_conflicts
# Album.get_column_conflict!(:column)
# Album.set_column_conflict!(:other_column)
#
# Usage:
#
# # Make all model's handle column conflicts automatically (called before loading subclasses)
# Sequel::Model.plugin :column_conflicts
#
# # Make the Album class handle column conflicts automatically
# Album.plugin :column_conflicts
module ColumnConflicts
# Check for column conflicts on the current model if the model has a dataset.
def self.configure(model)
model.instance_eval do
@get_column_conflicts = {}
@set_column_conflicts = {}
check_column_conflicts if @dataset
end
end

module ClassMethods
Plugins.after_set_dataset(self, :check_column_conflicts)
Plugins.inherited_instance_variables(self, :@get_column_conflicts=>:dup, :@set_column_conflicts=>:dup)

# Hash for columns where the getter method already exists. keys are column symbols/strings that
# conflict with method names and should be looked up directly instead of calling a method,
# values are the column symbol to lookup in the values hash.
attr_reader :get_column_conflicts

# Hash for columns where the setter method already exists. keys are column symbols/strings suffixed
# with = that conflict with method names and should be set directly in the values hash,
# values are the column symbol to set in the values hash.
attr_reader :set_column_conflicts

# Compare the column names for the model with the methods defined on Sequel::Model, and automatically
# setup the column conflicts.
def check_column_conflicts
mod = Sequel::Model
columns.find_all{|c| mod.method_defined?(c)}.each{|c| get_column_conflict!(c)}
columns.find_all{|c| mod.method_defined?("#{c}=")}.each{|c| set_column_conflict!(c)}
end

# Set the given column as one with a getter method conflict.
def get_column_conflict!(column)
@get_column_conflicts[column.to_sym] = @get_column_conflicts[column.to_s] = column.to_sym
end

# Set the given column as one with a setter method conflict.
def set_column_conflict!(column)
@set_column_conflicts[:"#{column}="] = @set_column_conflicts["#{column}="] = column.to_sym
end
end

module InstanceMethods
# If the given column has a getter method conflict, lookup the value directly in the values hash.
def get_column_value(c)
if col = model.get_column_conflicts[c]
self[col]
else
super
end
end

# If the given column has a setter method conflict, set the value directly in the values hash.
def set_column_value(c, v)
if col = model.set_column_conflicts[c]
self[col] = v
else
super
end
end
end
end
end
end
55 changes: 55 additions & 0 deletions spec/extensions/column_conflicts_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")

describe "column_conflicts plugin" do
before do
@db = Sequel.mock
@c = Class.new(Sequel::Model(@db[:test]))
@c.columns :model, :use_transactions, :foo
@c.plugin :column_conflicts
@o = @c.load(:model=>1, :use_transactions=>2, :foo=>4)
end

it "should have mass assignment work correctly" do
@o.set_fields({:use_transactions=>3}, [:use_transactions])
@o.get_column_value(:use_transactions).should == 3
end

it "should handle both symbols and strings" do
@o.get_column_value(:model).should == 1
@o.get_column_value("model").should == 1
@o.set_column_value(:use_transactions=, 3)
@o.get_column_value(:use_transactions).should == 3
@o.set_column_value(:use_transactions=, 4)
@o.get_column_value(:use_transactions).should == 4
end

it "should allow manual setting of conflicted columns" do
@c.send(:define_method, :foo){raise}
@c.get_column_conflict!(:foo)
@o.get_column_value(:foo).should == 4

@c.send(:define_method, :model=){raise}
@c.set_column_conflict!(:model)
@o.set_column_value(:model=, 2).should == 2
@o.get_column_value(:model).should == 2
end

it "should work correctly in subclasses" do
@o = Class.new(@c).load(:model=>1, :use_transactions=>2)
@o.get_column_value(:model).should == 1
@o.get_column_value("model").should == 1
@o.set_column_value(:use_transactions=, 3)
@o.get_column_value(:use_transactions).should == 3
@o.set_column_value(:use_transactions=, 4)
@o.get_column_value(:use_transactions).should == 4
end

it "should work correctly for dataset changes" do
ds = @db[:test]
def ds.columns; [:object_id] end
@c.dataset = ds
o = @c.load(:object_id=>3)
o.get_column_value(:object_id).should == 3
o.object_id.should_not == 3
end
end
1 change: 1 addition & 0 deletions www/pages/plugins.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<li>Attributes:<ul>
<li><a href="rdoc-plugins/classes/Sequel/Plugins/BlacklistSecurity.html">blacklist_security</a>: Adds blacklist-based model mass-assignment protection.</li>
<li><a href="rdoc-plugins/classes/Sequel/Plugins/BooleanReaders.html">boolean_readers</a>: Adds attribute? methods for all boolean columns.</li>
<li><a href="rdoc-plugins/classes/Sequel/Plugins/ColumnConflicts.html">column_conflicts</a>: Automatically handles column names that conflict with Ruby/Sequel method names.</li>
<li><a href="rdoc-plugins/classes/Sequel/Plugins/Dirty.html">dirty</a>: Allows you get get initial values of columns after changing the values.</li>
<li><a href="rdoc-plugins/classes/Sequel/Plugins/DefaultsSetter.html">defaults_setter</a>: Get default values for new models before saving.</li>
<li><a href="rdoc-plugins/classes/Sequel/Plugins/ForceEncoding.html">force_encoding</a>: Forces the all model column string values to a given encoding.</li>
Expand Down

0 comments on commit b3626bf

Please sign in to comment.