ar_merge 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.markdown +43 -0
- data/Rakefile +20 -0
- data/VERSION +1 -0
- data/ar_merge.gemspec +52 -0
- data/init.rb +1 -0
- data/lib/ar_merge.rb +58 -0
- data/spec/ar_merge_spec.rb +102 -0
- data/spec/setup_test_model.rb +41 -0
- data/spec/spec_helper.rb +4 -0
- metadata +74 -0
data/README.markdown
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
- Merges associations/attributes you want
|
|
2
|
+
- Can merge duplicates
|
|
3
|
+
- Protects from self-merges
|
|
4
|
+
- Keeps counters valid
|
|
5
|
+
- Removes merged record
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
INSTALL
|
|
9
|
+
=======
|
|
10
|
+
|
|
11
|
+
Rails plugin
|
|
12
|
+
|
|
13
|
+
script/plugin install git://github.com/grosser/ar_merge.git
|
|
14
|
+
|
|
15
|
+
OR Gem
|
|
16
|
+
|
|
17
|
+
sudo gem install ar_merge
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
USAGE
|
|
21
|
+
=====
|
|
22
|
+
Merge from outside the model:
|
|
23
|
+
|
|
24
|
+
user.merge!(other,:attributes=>user.attributes.keys,:associations=>%w[movies friends])`
|
|
25
|
+
|
|
26
|
+
Merge from inside the model
|
|
27
|
+
|
|
28
|
+
User < ActiveRecord::Base
|
|
29
|
+
def merge!(other)
|
|
30
|
+
super(other,:attributes=>%w[email website])
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
Merge duplicates
|
|
35
|
+
|
|
36
|
+
#merge all new users, that have the same email
|
|
37
|
+
User.merge_duplicates!(User.find_all_by_status('new')) , :compare=>:email)
|
|
38
|
+
|
|
39
|
+
AUTHOR
|
|
40
|
+
======
|
|
41
|
+
[Michael Grosser](http://pragmatig.wordpress.com)
|
|
42
|
+
grosser.michael@gmail.com
|
|
43
|
+
Hereby placed under public domain, do what you want, just do not hold me accountable...
|
data/Rakefile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
task :default => :spec
|
|
2
|
+
require 'spec/rake/spectask'
|
|
3
|
+
Spec::Rake::SpecTask.new {|t| t.spec_opts = ['--color']}
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
require 'jeweler'
|
|
7
|
+
project_name = 'ar_merge'
|
|
8
|
+
Jeweler::Tasks.new do |gem|
|
|
9
|
+
gem.name = project_name
|
|
10
|
+
gem.summary = "Merge 2 ActiveRecords, preserving associations and attributes"
|
|
11
|
+
gem.email = "grosser.michael@gmail.com"
|
|
12
|
+
gem.homepage = "http://github.com/grosser/#{project_name}"
|
|
13
|
+
gem.authors = ["Michael Grosser"]
|
|
14
|
+
gem.add_dependency ['activerecord']
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
Jeweler::GemcutterTasks.new
|
|
18
|
+
rescue LoadError
|
|
19
|
+
puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
|
20
|
+
end
|
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.2
|
data/ar_merge.gemspec
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Generated by jeweler
|
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
|
4
|
+
# -*- encoding: utf-8 -*-
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |s|
|
|
7
|
+
s.name = %q{ar_merge}
|
|
8
|
+
s.version = "0.1.2"
|
|
9
|
+
|
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
|
11
|
+
s.authors = ["Michael Grosser"]
|
|
12
|
+
s.date = %q{2009-11-22}
|
|
13
|
+
s.email = %q{grosser.michael@gmail.com}
|
|
14
|
+
s.extra_rdoc_files = [
|
|
15
|
+
"README.markdown"
|
|
16
|
+
]
|
|
17
|
+
s.files = [
|
|
18
|
+
"README.markdown",
|
|
19
|
+
"Rakefile",
|
|
20
|
+
"VERSION",
|
|
21
|
+
"ar_merge.gemspec",
|
|
22
|
+
"init.rb",
|
|
23
|
+
"lib/ar_merge.rb",
|
|
24
|
+
"spec/ar_merge_spec.rb",
|
|
25
|
+
"spec/setup_test_model.rb",
|
|
26
|
+
"spec/spec_helper.rb"
|
|
27
|
+
]
|
|
28
|
+
s.homepage = %q{http://github.com/grosser/ar_merge}
|
|
29
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
|
30
|
+
s.require_paths = ["lib"]
|
|
31
|
+
s.rubygems_version = %q{1.3.5}
|
|
32
|
+
s.summary = %q{Merge 2 ActiveRecords, preserving associations and attributes}
|
|
33
|
+
s.test_files = [
|
|
34
|
+
"spec/spec_helper.rb",
|
|
35
|
+
"spec/ar_merge_spec.rb",
|
|
36
|
+
"spec/setup_test_model.rb"
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
if s.respond_to? :specification_version then
|
|
40
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
|
41
|
+
s.specification_version = 3
|
|
42
|
+
|
|
43
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
|
44
|
+
s.add_runtime_dependency(%q<activerecord>, [">= 0"])
|
|
45
|
+
else
|
|
46
|
+
s.add_dependency(%q<activerecord>, [">= 0"])
|
|
47
|
+
end
|
|
48
|
+
else
|
|
49
|
+
s.add_dependency(%q<activerecord>, [">= 0"])
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
data/init.rb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require 'ar_merge'
|
data/lib/ar_merge.rb
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require 'activerecord'
|
|
2
|
+
|
|
3
|
+
module ARMerge
|
|
4
|
+
VERSION = File.read( File.join(File.dirname(__FILE__),'..','VERSION') ).strip
|
|
5
|
+
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend ClassMethods
|
|
8
|
+
base.send(:include, InstanceMethods)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module InstanceMethods
|
|
12
|
+
def merge!(other, options={})
|
|
13
|
+
raise "cannot merge wit a new record" if other.new_record?
|
|
14
|
+
raise "cannot merge with myself" if other == self
|
|
15
|
+
|
|
16
|
+
#merge associations
|
|
17
|
+
(options[:associations]||[]).each do |association_name|
|
|
18
|
+
other.send(association_name).each do |associated|
|
|
19
|
+
send(association_name) << associated
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
#update counters, this is very basic/hacky/not secure for customized counters...
|
|
23
|
+
counter = "#{association_name}_count"
|
|
24
|
+
next unless other.respond_to?(counter)
|
|
25
|
+
self.class.update_counters(id, counter => other.send(counter))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
#merge attributes
|
|
29
|
+
(options[:attributes]||[]).each do |attr|
|
|
30
|
+
send("#{attr}=", other.send(attr)) if send(attr).blank?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
#cleanup
|
|
34
|
+
other.reload.destroy
|
|
35
|
+
save!
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
module ClassMethods
|
|
40
|
+
def merge_duplicates!(records,options)
|
|
41
|
+
records.each do |record|
|
|
42
|
+
next if record.nil?
|
|
43
|
+
records.each do |other|
|
|
44
|
+
next if other.nil?
|
|
45
|
+
next if other == record
|
|
46
|
+
is_comparable = other.send(options[:compare]) == record.send(options[:compare])
|
|
47
|
+
next unless is_comparable
|
|
48
|
+
|
|
49
|
+
#merge and remove the other
|
|
50
|
+
records[records.index(other)]=nil
|
|
51
|
+
record.merge!(other)
|
|
52
|
+
end
|
|
53
|
+
end.compact
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
ActiveRecord::Base.send(:include, ARMerge)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
require "spec/spec_helper"
|
|
2
|
+
|
|
3
|
+
describe ARMerge do
|
|
4
|
+
describe :merge! do
|
|
5
|
+
before do
|
|
6
|
+
@user = User.create!(:name=>'x')
|
|
7
|
+
@u2 = User.create!(:name=>'y')
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
describe :removal do
|
|
11
|
+
it "removes the merged user" do
|
|
12
|
+
@user.merge!(@u2)
|
|
13
|
+
lambda{@u2.reload}.should raise_error(ActiveRecord::RecordNotFound)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "saves the merging user" do
|
|
17
|
+
@user.should_receive(:save!)
|
|
18
|
+
@user.merge!(@u2)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe :attributes do
|
|
23
|
+
it "merges and overtakes attributes" do
|
|
24
|
+
@user.name = ''
|
|
25
|
+
@user.merge!(@u2,:attributes=>['name'])
|
|
26
|
+
@user.name.should == 'y'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "does not overtake attributes when current is not blank" do
|
|
30
|
+
@user.merge!(@u2,:attributes=>['name'])
|
|
31
|
+
@user.name.should == 'x'
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe :associations do
|
|
36
|
+
before do
|
|
37
|
+
Movie.delete_all
|
|
38
|
+
@user.movies << Movie.new
|
|
39
|
+
@user.save!
|
|
40
|
+
@user.movies.size.should == Movie.count
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "overtakes asociated objects" do
|
|
44
|
+
@u2.merge!(@user,:associations=>[:movies])
|
|
45
|
+
@u2.reload.should have(Movie.count).movies
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "does not create new objects" do
|
|
49
|
+
lambda{@u2.merge!(@user,:associations=>['movies'])}.should_not change(Movie,:count)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "keeps counters in sync" do
|
|
53
|
+
user, merged_user = CountingUser.create!, CountingUser.create!
|
|
54
|
+
user.movies_count.should == 0
|
|
55
|
+
merged_user.movies << Movie.new
|
|
56
|
+
merged_user.save!
|
|
57
|
+
merged_user.reload.movies_count.should == 1
|
|
58
|
+
|
|
59
|
+
user.merge!(merged_user,:associations=>[:movies])
|
|
60
|
+
user.reload.movies_count.should == 1
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "does no merge with new" do
|
|
65
|
+
lambda{@user.merge!(User.new)}.should raise_error
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "does no merge with self" do
|
|
69
|
+
lambda{@user.merge!(User.find(@user.id))}.should raise_error
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe :merge_duplicates! do
|
|
74
|
+
before do
|
|
75
|
+
@u1 = User.create!(:name=>'a')
|
|
76
|
+
@u2 = User.create!(:name=>'b')
|
|
77
|
+
@u3 = User.create!(:name=>'a')
|
|
78
|
+
@u4 = User.create!(:name=>'a')
|
|
79
|
+
@users=[@u1,@u2,@u3]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "merges all records that have the same attributes" do
|
|
83
|
+
User.merge_duplicates!(@users,:compare=>:name).should == [@u1,@u2]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "destroys the duplicates" do
|
|
87
|
+
User.merge_duplicates!(@users,:compare=>:name)
|
|
88
|
+
lambda{@u3.reload}.should raise_error(ActiveRecord::RecordNotFound)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "merges the first with each of its duplicates" do
|
|
92
|
+
@users = [@u2,@u3,@u1,@u4]
|
|
93
|
+
@u3.should_receive(:merge!).with(@u1)
|
|
94
|
+
@u3.should_receive(:merge!).with(@u4)
|
|
95
|
+
User.merge_duplicates!(@users,:compare=>:name).should == [@u2,@u3]
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "has a VERSION" do
|
|
100
|
+
ARMerge::VERSION.should =~ /^\d+\.\d+\.\d+$/
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# connect
|
|
2
|
+
ActiveRecord::Base.configurations = {"test" => {
|
|
3
|
+
:adapter => "sqlite3",
|
|
4
|
+
:database => ":memory:",
|
|
5
|
+
}.with_indifferent_access}
|
|
6
|
+
|
|
7
|
+
ActiveRecord::Base.establish_connection(:test)
|
|
8
|
+
|
|
9
|
+
# create tables
|
|
10
|
+
ActiveRecord::Schema.define(:version => 1) do
|
|
11
|
+
create_table :users do |t|
|
|
12
|
+
t.string :name
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
create_table :counting_users do |t|
|
|
17
|
+
t.integer :movies_count, :default=>0, :null=>false
|
|
18
|
+
t.string :name
|
|
19
|
+
t.timestamps
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
create_table :movies do |t|
|
|
23
|
+
t.integer :user_id
|
|
24
|
+
t.integer :counting_user_id
|
|
25
|
+
t.timestamps
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# create models
|
|
30
|
+
class User < ActiveRecord::Base
|
|
31
|
+
has_many :movies
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class CountingUser < ActiveRecord::Base
|
|
35
|
+
has_many :movies
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class Movie < ActiveRecord::Base
|
|
39
|
+
belongs_to :user
|
|
40
|
+
belongs_to :counting_user, :counter_cache => true
|
|
41
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ar_merge
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Michael Grosser
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
|
|
12
|
+
date: 2009-11-22 00:00:00 +01:00
|
|
13
|
+
default_executable:
|
|
14
|
+
dependencies:
|
|
15
|
+
- !ruby/object:Gem::Dependency
|
|
16
|
+
name: activerecord
|
|
17
|
+
type: :runtime
|
|
18
|
+
version_requirement:
|
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
20
|
+
requirements:
|
|
21
|
+
- - ">="
|
|
22
|
+
- !ruby/object:Gem::Version
|
|
23
|
+
version: "0"
|
|
24
|
+
version:
|
|
25
|
+
description:
|
|
26
|
+
email: grosser.michael@gmail.com
|
|
27
|
+
executables: []
|
|
28
|
+
|
|
29
|
+
extensions: []
|
|
30
|
+
|
|
31
|
+
extra_rdoc_files:
|
|
32
|
+
- README.markdown
|
|
33
|
+
files:
|
|
34
|
+
- README.markdown
|
|
35
|
+
- Rakefile
|
|
36
|
+
- VERSION
|
|
37
|
+
- ar_merge.gemspec
|
|
38
|
+
- init.rb
|
|
39
|
+
- lib/ar_merge.rb
|
|
40
|
+
- spec/ar_merge_spec.rb
|
|
41
|
+
- spec/setup_test_model.rb
|
|
42
|
+
- spec/spec_helper.rb
|
|
43
|
+
has_rdoc: true
|
|
44
|
+
homepage: http://github.com/grosser/ar_merge
|
|
45
|
+
licenses: []
|
|
46
|
+
|
|
47
|
+
post_install_message:
|
|
48
|
+
rdoc_options:
|
|
49
|
+
- --charset=UTF-8
|
|
50
|
+
require_paths:
|
|
51
|
+
- lib
|
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
53
|
+
requirements:
|
|
54
|
+
- - ">="
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: "0"
|
|
57
|
+
version:
|
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: "0"
|
|
63
|
+
version:
|
|
64
|
+
requirements: []
|
|
65
|
+
|
|
66
|
+
rubyforge_project:
|
|
67
|
+
rubygems_version: 1.3.5
|
|
68
|
+
signing_key:
|
|
69
|
+
specification_version: 3
|
|
70
|
+
summary: Merge 2 ActiveRecords, preserving associations and attributes
|
|
71
|
+
test_files:
|
|
72
|
+
- spec/spec_helper.rb
|
|
73
|
+
- spec/ar_merge_spec.rb
|
|
74
|
+
- spec/setup_test_model.rb
|