From b3626bf468b36984701bd013e2fa1f249c2c453b Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Wed, 7 Jan 2015 13:08:19 -0800 Subject: [PATCH] Add column_conflicts plugin to automatically handle columns that conflict 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. --- CHANGELOG | 2 + lib/sequel/plugins/column_conflicts.rb | 93 ++++++++++++++++++++++++ spec/extensions/column_conflicts_spec.rb | 55 ++++++++++++++ www/pages/plugins.html.erb | 1 + 4 files changed, 151 insertions(+) create mode 100644 lib/sequel/plugins/column_conflicts.rb create mode 100644 spec/extensions/column_conflicts_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 50fb36715a..bb28ce799c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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) diff --git a/lib/sequel/plugins/column_conflicts.rb b/lib/sequel/plugins/column_conflicts.rb new file mode 100644 index 0000000000..3606819f4b --- /dev/null +++ b/lib/sequel/plugins/column_conflicts.rb @@ -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 diff --git a/spec/extensions/column_conflicts_spec.rb b/spec/extensions/column_conflicts_spec.rb new file mode 100644 index 0000000000..bc6c95772e --- /dev/null +++ b/spec/extensions/column_conflicts_spec.rb @@ -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 diff --git a/www/pages/plugins.html.erb b/www/pages/plugins.html.erb index c713539d31..127ee73878 100644 --- a/www/pages/plugins.html.erb +++ b/www/pages/plugins.html.erb @@ -21,6 +21,7 @@
  • Attributes: