-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add modification_detection plugin, for automatic detection of in-plac…
…e column value modifications This is a simple if fairly slow way to reliably detect in-place column value modifications. Well, it's reliable if the objects themselves implement #hash correctly, but if they don't then those objects should be fixed. ActiveRecord is going to be doing something similar in the future by default. I don't think it's a good idea to force a slowdown on all users just because some users may modify values in place, so it will remain a plugin in Sequel. Users who care about performance should continue using the existing modified!(:column) method to manually mark column values as changed when changing them in-place.
- Loading branch information
1 parent
b137865
commit a9c4e06
Showing
3 changed files
with
172 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
module Sequel | ||
module Plugins | ||
# This plugin automatically detects in-place modifications to | ||
# columns as well as direct modifications of the values hash. | ||
# | ||
# class User < Sequel::Model | ||
# plugin :modification_detection | ||
# end | ||
# user = User[1] | ||
# user.a # => 'a' | ||
# user.a << 'b' | ||
# user.save_changes | ||
# # UPDATE users SET a = 'ab' WHERE (id = 1) | ||
# | ||
# Note that for this plugin to work correctly, the column values must | ||
# correctly implement the #hash method, returning the same value if | ||
# the object is equal, and a different value if the object is not equal. | ||
# | ||
# Note that this plugin causes a performance hit for all retrieved | ||
# objects, so it shouldn't be used in cases where performance is a | ||
# primary concern. | ||
# | ||
# Usage: | ||
# | ||
# # Make all model subclass automatically detect column modifications | ||
# Sequel::Model.plugin :modification_detection | ||
# | ||
# # Make the Album class automatically detect column modifications | ||
# Album.plugin :modification_detection | ||
module ModificationDetection | ||
module ClassMethods | ||
# Calculate the hashes for all of the column values, so that they | ||
# can be compared later to determine if the column value has changed. | ||
def call(_) | ||
v = super | ||
v.calculate_values_hashes | ||
v | ||
end | ||
end | ||
|
||
module InstanceMethods | ||
# Recalculate the column value hashes after updating. | ||
def after_update | ||
super | ||
recalculate_values_hashes | ||
end | ||
|
||
# Calculate the column hash values if they haven't been already calculated. | ||
def calculate_values_hashes | ||
@values_hashes || recalculate_values_hashes | ||
end | ||
|
||
# Detect which columns have been modified by comparing the cached hash | ||
# value to the hash of the current value. | ||
def changed_columns | ||
cc = super | ||
changed = [] | ||
v = @values | ||
if vh = @values_hashes | ||
(vh.keys - cc).each{|c| changed << c unless v.has_key?(c) && vh[c] == v[c].hash} | ||
end | ||
cc + changed | ||
end | ||
|
||
private | ||
|
||
# Recalculate the column value hashes after manually refreshing. | ||
def _refresh(dataset) | ||
super | ||
recalculate_values_hashes | ||
end | ||
|
||
# Recalculate the column value hashes after refreshing after saving a new object. | ||
def _save_refresh | ||
super | ||
recalculate_values_hashes | ||
end | ||
|
||
# Recalculate the column value hashes, caching them for later use. | ||
def recalculate_values_hashes | ||
vh = {} | ||
@values.each do |k,v| | ||
vh[k] = v.hash | ||
end | ||
@values_hashes = vh.freeze | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper") | ||
require 'yaml' | ||
|
||
describe "serialization_modification_detection plugin" do | ||
before do | ||
@ds = Sequel.mock(:fetch=>{:id=>1, :a=>'a', :b=>1, :c=>['a'], :d=>{'b'=>'c'}}, :numrows=>1, :autoid=>1)[:items] | ||
@c = Class.new(Sequel::Model(@ds)) | ||
@c.plugin :modification_detection | ||
@c.columns :a, :b, :c, :d | ||
@o = @c.first | ||
@ds.db.sqls | ||
end | ||
|
||
it "should only detect columns that have been changed" do | ||
@o.changed_columns.should == [] | ||
@o.a << 'b' | ||
@o.changed_columns.should == [:a] | ||
@o.a.replace('a') | ||
@o.changed_columns.should == [] | ||
|
||
@o.values[:b] = 2 | ||
@o.changed_columns.should == [:b] | ||
@o.values[:b] = 1 | ||
@o.changed_columns.should == [] | ||
|
||
@o.c[0] << 'b' | ||
@o.d['b'] << 'b' | ||
@o.changed_columns.sort_by{|c| c.to_s}.should == [:c, :d] | ||
@o.c[0] = 'a' | ||
@o.changed_columns.should == [:d] | ||
@o.d['b'] = 'c' | ||
@o.changed_columns.should == [] | ||
end | ||
|
||
it "should not list a column twice" do | ||
@o.a = 'b' | ||
@o.a << 'a' | ||
@o.changed_columns.should == [:a] | ||
end | ||
|
||
it "should report correct changed_columns after updating" do | ||
@o.a << 'a' | ||
@o.save_changes | ||
@o.changed_columns.should == [] | ||
|
||
@o.values[:b] = 2 | ||
@o.save_changes | ||
@o.changed_columns.should == [] | ||
|
||
@o.c[0] << 'b' | ||
@o.save_changes | ||
@o.changed_columns.should == [] | ||
|
||
@o.d['b'] << 'a' | ||
@o.save_changes | ||
@o.changed_columns.should == [] | ||
|
||
@ds.db.sqls.should == ["UPDATE items SET a = 'aa' WHERE (id = 1)", | ||
"UPDATE items SET b = 2 WHERE (id = 1)", | ||
"UPDATE items SET c = ('ab') WHERE (id = 1)", | ||
"UPDATE items SET d = ('b' = 'ca') WHERE (id = 1)"] | ||
end | ||
|
||
it "should report correct changed_columns after creating new object" do | ||
o = @c.create | ||
o.changed_columns.should == [] | ||
o.a << 'a' | ||
o.changed_columns.should == [:a] | ||
@ds.db.sqls.should == ["INSERT INTO items DEFAULT VALUES", "SELECT * FROM items WHERE (id = 1) LIMIT 1"] | ||
end | ||
|
||
it "should report correct changed_columns after refreshing existing object" do | ||
@o.a << 'a' | ||
@o.changed_columns.should == [:a] | ||
@o.refresh | ||
@o.changed_columns.should == [] | ||
@o.a << 'a' | ||
@o.changed_columns.should == [:a] | ||
end | ||
end |