couchdiff 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gemtest +0 -0
- data/History.txt +9 -0
- data/Manifest.txt +9 -0
- data/README.md +62 -0
- data/Rakefile +19 -0
- data/bin/couchdiff +109 -0
- data/lib/copier.rb +87 -0
- data/lib/couchdiff.rb +135 -0
- data/spec/copier_spec.rb +1 -0
- data/spec/couchdiff_spec.rb +131 -0
- metadata +126 -0
data/.gemtest
ADDED
File without changes
|
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README.md
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# couchdiff
|
2
|
+
|
3
|
+
* http://github.com/mudynamics/couchdiff
|
4
|
+
|
5
|
+
## DESCRIPTION
|
6
|
+
|
7
|
+
* Diff/Merge utility to compare & synchronize two database, using couchrest. As this tool does not use the CouchDB replication protocol it can be used to transfer data between incompatible CouchDB versions and allows you to move data when replication fails.
|
8
|
+
|
9
|
+
## FEATURES
|
10
|
+
|
11
|
+
* Compare all docs or only docs matching a design view (must be present in both source and dst)
|
12
|
+
* Pluggable comparison logic (by default couchdiff compares all fields excluding "_rev" and "rev_pos")
|
13
|
+
* Some initial optimizations to avoid copying attachments that did not change.
|
14
|
+
|
15
|
+
## SYNOPSIS
|
16
|
+
|
17
|
+
* Usage: couchdiff [options] src-url dst-url
|
18
|
+
* s, --scope [SCOPE] name of couch view (e.g. User/all) used to limit scope of diff/merge.
|
19
|
+
* -f, --field [FIELD] json field used to compare docs (Default: Compare all fields except "_rev")
|
20
|
+
* -a, --apply Apply changes to destination DB (add, update and delete docs to match source DB)
|
21
|
+
* -c, --compact Compact destination DB after changes are applied.
|
22
|
+
* -v, --verbose List documents that were added, updated or deleted.
|
23
|
+
* -h, --help Display this screen
|
24
|
+
|
25
|
+
## REQUIREMENTS
|
26
|
+
|
27
|
+
* couchrest, progressbar, json or json_pure
|
28
|
+
* hoe (to build the gem)
|
29
|
+
|
30
|
+
## INSTALL
|
31
|
+
|
32
|
+
* Via rubyforge: `gem install couchdiff`
|
33
|
+
* Local installation: `rake install_gem`
|
34
|
+
|
35
|
+
## TODOs
|
36
|
+
|
37
|
+
* Optimize attachment copying speed
|
38
|
+
|
39
|
+
## LICENSE
|
40
|
+
|
41
|
+
(The MIT License)
|
42
|
+
|
43
|
+
Copyright (c) 2012 Mu Dynamics
|
44
|
+
|
45
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
46
|
+
a copy of this software and associated documentation files (the
|
47
|
+
'Software'), to deal in the Software without restriction, including
|
48
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
49
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
50
|
+
permit persons to whom the Software is furnished to do so, subject to
|
51
|
+
the following conditions:
|
52
|
+
|
53
|
+
The above copyright notice and this permission notice shall be
|
54
|
+
included in all copies or substantial portions of the Software.
|
55
|
+
|
56
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
57
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
58
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
59
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
60
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
61
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
62
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
require './lib/couchdiff.rb'
|
6
|
+
require './lib/copier.rb'
|
7
|
+
|
8
|
+
Hoe.plugin :bundler
|
9
|
+
Hoe.spec('couchdiff') do |p|
|
10
|
+
p.version = "0.0.2"
|
11
|
+
p.developer('Jens Schmidt (Mu Dynamics)', 'jens@mudynamics.com')
|
12
|
+
p.rubyforge_name = 'couchdiff'
|
13
|
+
p.author = 'Jens Schmidt'
|
14
|
+
p.url = 'http://github.com/mudynamics/couchdiff'
|
15
|
+
p.extra_deps << ['couchrest', '~> 1.1.2']
|
16
|
+
p.extra_deps << ['progressbar', '~> 0.9']
|
17
|
+
p.extra_deps << ['json', '~> 1.6.0']
|
18
|
+
p.extra_dev_deps << ['rspec','~> 2.7.0']
|
19
|
+
end
|
data/bin/couchdiff
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'couchrest'
|
3
|
+
require 'couchdiff'
|
4
|
+
require 'copier'
|
5
|
+
require 'optparse'
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
options = {}
|
9
|
+
|
10
|
+
optparse = OptionParser.new do|opts|
|
11
|
+
opts.banner = "Usage: couchdiff [options] src_url dst_url"
|
12
|
+
|
13
|
+
opts.on( '-s', '--scope [SCOPE]', 'Name of CouchDB View (e.g. User/all) used to limit scope of diff.' ) do|value|
|
14
|
+
options[:scope] = value
|
15
|
+
end
|
16
|
+
|
17
|
+
opts.on( '-f', '--field [FIELD]', 'JSON field used to compare docs (Default: Compare all fields except "_rev")' ) do|value|
|
18
|
+
options[:field] = value
|
19
|
+
end
|
20
|
+
|
21
|
+
opts.on( '-a', '--apply', 'Apply changes to destination DB (add, update and delete docs to match source DB)' ) do|value|
|
22
|
+
options[:apply] = value
|
23
|
+
end
|
24
|
+
|
25
|
+
opts.on( '-c', '--compact', 'Compact destination DB after changes are applied.' ) do
|
26
|
+
options[:compact] = true
|
27
|
+
end
|
28
|
+
|
29
|
+
opts.on( '-v', '--verbose', 'List documents that were added, updated or deleted.' ) do
|
30
|
+
options[:verbose] = true
|
31
|
+
end
|
32
|
+
|
33
|
+
opts.on( '--version', 'Print couchdiff version and exit.' ) do
|
34
|
+
options[:version] = true
|
35
|
+
end
|
36
|
+
|
37
|
+
opts.on_tail( '-h', '--help', 'Display this screen' ) do
|
38
|
+
puts opts
|
39
|
+
exit
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
optparse.parse!(ARGV)
|
44
|
+
|
45
|
+
# print version
|
46
|
+
(puts "Version: "+ CouchDiff::VERSION; exit) if (options[:version])
|
47
|
+
|
48
|
+
def error msg
|
49
|
+
puts "ERROR: #{msg}"
|
50
|
+
exit
|
51
|
+
end
|
52
|
+
|
53
|
+
FROM, TO = ARGV
|
54
|
+
|
55
|
+
# check arguments
|
56
|
+
(puts optparse; exit) if FROM == TO # handles null and same db cases
|
57
|
+
error("Database URLs (src and dst) are required") unless (FROM && TO)
|
58
|
+
FROM[URI.regexp] or error "Invalid src url syntax '#{FROM}'"
|
59
|
+
TO[URI.regexp] or error "Invalid dst url syntax '#{TO}'"
|
60
|
+
|
61
|
+
SCOPE = options[:scope]
|
62
|
+
|
63
|
+
puts "Generating diff\n between #{FROM}\n and #{TO}\n limited to '#{SCOPE ? SCOPE : 'all_docs'}'"
|
64
|
+
|
65
|
+
RE_SERVER = /(^.*)\//
|
66
|
+
RE_DB = /\w+$/
|
67
|
+
|
68
|
+
src_couch = CouchRest.new FROM[RE_SERVER, 1]
|
69
|
+
src_db = src_couch.database FROM[RE_DB]
|
70
|
+
|
71
|
+
dst_couch = CouchRest.new TO[RE_SERVER, 1]
|
72
|
+
dst_db = dst_couch.database TO[RE_DB]
|
73
|
+
|
74
|
+
# use a couch view as diff scope if one is provided, otherwise all_docs
|
75
|
+
fetcher = proc{|db| SCOPE ?
|
76
|
+
(db.view(SCOPE, :include_docs => true) rescue error "View '#{SCOPE}' does not exist in DB at #{db}"):
|
77
|
+
db.all_docs(:include_docs => true)
|
78
|
+
}
|
79
|
+
|
80
|
+
src_docs = fetcher.yield(src_db)['rows']
|
81
|
+
dst_docs = fetcher.yield(dst_db)['rows']
|
82
|
+
|
83
|
+
COMPARE_FIELD = options[:field]
|
84
|
+
if COMPARE_FIELD
|
85
|
+
puts "-> Using '#{COMPARE_FIELD}' to compare document versions."
|
86
|
+
diff = CouchDiff.new src_docs, dst_docs do |src, dst|
|
87
|
+
src[COMPARE_FIELD] && src[COMPARE_FIELD] != dst[COMPARE_FIELD]
|
88
|
+
end
|
89
|
+
else
|
90
|
+
diff = CouchDiff.new src_docs, dst_docs
|
91
|
+
end
|
92
|
+
|
93
|
+
VERBOSE = options[:verbose]
|
94
|
+
|
95
|
+
puts "ADDED: #{diff.added.size}"
|
96
|
+
VERBOSE && (puts diff.added * "\n")
|
97
|
+
|
98
|
+
puts "DELETED: #{diff.deleted.size}"
|
99
|
+
VERBOSE && (puts diff.deleted * "\n")
|
100
|
+
|
101
|
+
puts "UPDATED: #{diff.updated.size}"
|
102
|
+
VERBOSE && (puts diff.updated_as_diff * "\n")
|
103
|
+
|
104
|
+
puts "UNCHANGED: #{diff.unchanged.size}"
|
105
|
+
# VERBOSE && (puts diff.unchanged * "\n")
|
106
|
+
|
107
|
+
options[:apply] && diff.patch(src_db, dst_db)
|
108
|
+
|
109
|
+
options[:compact] && (puts "Compacting..."; dst_db.compact! )
|
data/lib/copier.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'couchrest'
|
2
|
+
|
3
|
+
class Copier
|
4
|
+
def initialize src_db, dst_db
|
5
|
+
@src_db, @dst_db = src_db, dst_db
|
6
|
+
end
|
7
|
+
|
8
|
+
# copy src_doc and all attachments, dst_doc is the current doc in the dst_db
|
9
|
+
def copy src, old_dst = nil
|
10
|
+
|
11
|
+
# get source doc if src is a string (key)
|
12
|
+
src_doc = src.is_a?(String) ? @src_db.get(src) :
|
13
|
+
(src.is_a?(Hash) ? CouchRest::Document.new(src) : src)
|
14
|
+
src_doc.database = @src_db # in case it's a hash, not a CouchRest::Document
|
15
|
+
|
16
|
+
raise "document not found '#{src}'" if src_doc.nil?
|
17
|
+
|
18
|
+
new_dst = src_doc.clone
|
19
|
+
|
20
|
+
attachments_changed = false
|
21
|
+
|
22
|
+
# if src has attachments, check whether they have changed or not
|
23
|
+
if src_doc['_attachments']
|
24
|
+
if (old_dst.nil? || old_dst["_attachments"] != src_doc['_attachments']) # attachments have changed
|
25
|
+
attachments_changed = true
|
26
|
+
# remove attachments from dst_doc - they are added back later
|
27
|
+
new_dst.delete "_attachments"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# set the rev to the previous rev or remove it if new
|
32
|
+
old_dst ? (new_dst["_rev"] = old_dst["_rev"]) : (new_dst.delete "_rev")
|
33
|
+
|
34
|
+
if attachments_changed
|
35
|
+
begin
|
36
|
+
# saving the doc removes all old attachments (if any)
|
37
|
+
# using regular save
|
38
|
+
@dst_db.save_doc(new_dst)
|
39
|
+
# copy attachments one by one
|
40
|
+
src_doc['_attachments'].keys.each {|file| copy_attachment(src_doc, new_dst, file)}
|
41
|
+
|
42
|
+
# attachment order might not be retained - reload doc again to make sure
|
43
|
+
reloaded_doc = @dst_db.get new_dst['_id']
|
44
|
+
if (reloaded_doc['_attachments'].keys != src_doc['_attachments'].keys)
|
45
|
+
puts "correcting attachment order..."
|
46
|
+
ordered_attachments = src_doc['_attachments'].keys.inject({}) do |memo, key|
|
47
|
+
memo[key] = reloaded_doc['_attachments'][key]
|
48
|
+
memo
|
49
|
+
end
|
50
|
+
reloaded_doc['_attachments'] = ordered_attachments
|
51
|
+
@dst_db.save_doc(reloaded_doc)
|
52
|
+
end
|
53
|
+
rescue RestClient::Conflict
|
54
|
+
puts "ERROR: document version conflict, skipping..."
|
55
|
+
return
|
56
|
+
end
|
57
|
+
else
|
58
|
+
# use bulk save if attachments have not changed
|
59
|
+
@dst_db.bulk_save_doc(new_dst)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def copy_attachment src_doc, dst_doc, filename
|
66
|
+
size = src_doc['_attachments'][filename]['length'].to_i
|
67
|
+
# puts " reading attachment '#{filename}' (#{format_size size}) ..."
|
68
|
+
data = src_doc.fetch_attachment filename
|
69
|
+
# puts " writing attachment '#{filename}' (#{format_size size}) ...'"
|
70
|
+
content_type = src_doc['_attachments'][filename]['content_type']
|
71
|
+
dst_doc.put_attachment filename, data, {
|
72
|
+
:raw => true,
|
73
|
+
:content_type => content_type,
|
74
|
+
"Content-Encoding" => "gzip",
|
75
|
+
"Accept-Encoding" => "gzip"
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
def format_size size_int
|
80
|
+
units = %w{B KB MB GB TB}
|
81
|
+
e = (Math.log(size_int)/Math.log(1024)).floor
|
82
|
+
s = "%.1f" % (size_int.to_f / 1024**e)
|
83
|
+
s.sub(/\.?0*$/, units[e])
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
data/lib/couchdiff.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'progressbar'
|
2
|
+
require 'json/ext'
|
3
|
+
|
4
|
+
# add "diff" method to Hash class. Exlude keys are used to exempt one or more keys (e.g. '_rev')
|
5
|
+
class Hash
|
6
|
+
def diff(other, keys_to_exclude = [])
|
7
|
+
|
8
|
+
# check that values for all keys in this and other hash or the same
|
9
|
+
result = ((self.keys - keys_to_exclude) + (other.keys - keys_to_exclude)).uniq.inject({}) do |memo, key|
|
10
|
+
|
11
|
+
# if we have hashes on both sides...
|
12
|
+
if self[key].kind_of?(Hash) && other[key].kind_of?(Hash)
|
13
|
+
memo[key] = self[key].diff(other[key], keys_to_exclude)
|
14
|
+
else
|
15
|
+
# compare values directly (arrays or simple Strings, Numbers, Objects etc.)
|
16
|
+
memo[key] = [self[key], other[key]] unless (self[key] == other[key])
|
17
|
+
end
|
18
|
+
# get rid of empty hashes
|
19
|
+
memo.delete_if { |k, v| v.kind_of?(Hash) && v.empty? }
|
20
|
+
memo
|
21
|
+
end
|
22
|
+
|
23
|
+
# Check that both hashes contain the same keys (in the same order)
|
24
|
+
# uses special identifier "_keys_changed"
|
25
|
+
result[:_keys_changed] = [self.keys, other.keys] if (self.keys != other.keys)
|
26
|
+
result
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
class CouchDiff
|
32
|
+
|
33
|
+
VERSION = '0.0.2'
|
34
|
+
EXCLUDED_KEYS = ['_rev','revpos']
|
35
|
+
|
36
|
+
attr_reader :added
|
37
|
+
attr_reader :updated
|
38
|
+
attr_reader :deleted
|
39
|
+
attr_reader :unchanged
|
40
|
+
|
41
|
+
|
42
|
+
def initialize src_docs, dst_docs, &changed
|
43
|
+
# default behavior is to compare all fields besides the _rev and revpos field
|
44
|
+
changed ||= proc{ |doc1, doc2| doc1.diff(doc2, EXCLUDED_KEYS).size > 0 }
|
45
|
+
|
46
|
+
@added, @updated, @deleted, @unchanged = [], [], [], []
|
47
|
+
|
48
|
+
dst_doc_map, src_doc_map = {}, {}
|
49
|
+
|
50
|
+
# create hash maps for fast lookup
|
51
|
+
src_docs.each {|doc| src_doc_map[doc['id']] = doc; assert_doc(doc)}
|
52
|
+
dst_docs.each {|doc| dst_doc_map[doc['id']] = doc; assert_doc(doc)}
|
53
|
+
|
54
|
+
# updated/added docs
|
55
|
+
src_docs.each do |doc|
|
56
|
+
id = doc['id']
|
57
|
+
if !(dst_doc = dst_doc_map[id]).nil?
|
58
|
+
if changed.yield(doc['doc'], dst_doc['doc']) || attachments_changed(doc['doc'], dst_doc['doc'])
|
59
|
+
@updated << [doc, dst_doc]
|
60
|
+
else
|
61
|
+
@unchanged << doc
|
62
|
+
end
|
63
|
+
else
|
64
|
+
@added << doc
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# deleted docs
|
69
|
+
dst_docs.each do |doc|
|
70
|
+
id = doc['id']
|
71
|
+
@deleted << doc unless src_doc_map[id]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# determine if there are any added/deleted/modified attachments between src and dst
|
76
|
+
def attachments_changed src_doc, dst_doc
|
77
|
+
(src_doc['_attachments'] || {}).diff(dst_doc['_attachments'] || {}, EXCLUDED_KEYS).length > 0
|
78
|
+
end
|
79
|
+
|
80
|
+
def patch src_db, dst_db, copier = Copier.new(src_db, dst_db)
|
81
|
+
|
82
|
+
p_bar = ProgressBar.new('Adding docs', @added.size)
|
83
|
+
# ADD new docs
|
84
|
+
until @added.empty?
|
85
|
+
doc_ids = (@added.shift(20)).map {|src| src['id'] }
|
86
|
+
src_docs = src_db.get_bulk(doc_ids)['rows']
|
87
|
+
src_docs.each do |src_doc|
|
88
|
+
puts "[Adding #{src_doc['doc']['_id']}]"
|
89
|
+
copier.copy src_doc['doc'], nil
|
90
|
+
p_bar.inc
|
91
|
+
end
|
92
|
+
end
|
93
|
+
p_bar.finish
|
94
|
+
|
95
|
+
p_bar = ProgressBar.new('Updating docs', @updated.size)
|
96
|
+
|
97
|
+
# UPDATE docs (if they changed)
|
98
|
+
until @updated.empty?
|
99
|
+
# process 25 docs at a time
|
100
|
+
doc_ids = (@updated.shift(25)).map {|src, dst| src['id'] }
|
101
|
+
src_docs = src_db.get_bulk(doc_ids)['rows']
|
102
|
+
dst_docs = dst_db.get_bulk(doc_ids)['rows']
|
103
|
+
|
104
|
+
# update dst_docs content
|
105
|
+
src_docs.each_with_index do |src_doc, index|
|
106
|
+
dst_doc = dst_docs[index]
|
107
|
+
|
108
|
+
# puts "[Updating #{src_doc['doc']['_id']}]"
|
109
|
+
copier.copy src_doc['doc'], dst_doc['doc']
|
110
|
+
p_bar.inc
|
111
|
+
end
|
112
|
+
end
|
113
|
+
dst_db.bulk_save # flush any pending docs
|
114
|
+
p_bar.finish
|
115
|
+
|
116
|
+
|
117
|
+
# REMOVE deleted docs
|
118
|
+
p_bar = ProgressBar.new('Deleting docs', @deleted.size)
|
119
|
+
dst_db.bulk_save @deleted.map { |doc| {"_id" => doc["id"], "_rev" => doc['doc']['_rev'], "_deleted" => true } } unless @deleted.empty?
|
120
|
+
p_bar.finish
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
def updated_as_diff
|
125
|
+
@updated.map {|doc1, doc2| doc1['doc'].diff(doc2['doc'], EXCLUDED_KEYS).merge({'id' => doc1['id']}) }
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
# verify that doc is valid
|
131
|
+
def assert_doc doc
|
132
|
+
raise "document '#{doc['id']}' does not contain a 'doc' element. Did you set :include_docs => true?\n#{doc}" unless doc['doc']
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
data/spec/copier_spec.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.expand_path("../spec_helper", __FILE__)
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require File.expand_path("../spec_helper", __FILE__)
|
2
|
+
require 'couchdiff.rb'
|
3
|
+
require 'json/ext'
|
4
|
+
|
5
|
+
describe CouchDiff do
|
6
|
+
describe "deleted doc" do
|
7
|
+
diff = CouchDiff.new [], [
|
8
|
+
doc1 = {'id' => '1', 'value' => {'rev' => '1-12345'}, 'doc' => { "a" => "b"}}
|
9
|
+
]
|
10
|
+
it "should mark doc as deleted and not report additions or deletions" do
|
11
|
+
diff.deleted.should == [doc1]
|
12
|
+
diff.added.should be_empty
|
13
|
+
diff.updated.should be_empty
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "added doc" do
|
18
|
+
diff = CouchDiff.new [
|
19
|
+
doc1 = {'id' => '1', 'value' => {'rev' => '1-12345'}, 'doc' => { "a" => "b"}}
|
20
|
+
], []
|
21
|
+
|
22
|
+
it "should mark doc as added and not report updates or deletions" do
|
23
|
+
diff.added.should == [doc1]
|
24
|
+
diff.deleted.should be_empty
|
25
|
+
diff.updated.should be_empty
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "updated content" do
|
30
|
+
it "should mark doc as updated and not report additions or updates" do
|
31
|
+
diff = CouchDiff.new [
|
32
|
+
doc1 = {'id' => '1', 'value' => {'rev' => '1-12345'}, 'doc' => { "a" => "b"}},
|
33
|
+
doc2 = {'id' => '2', 'value' => {'rev' => '1-11111'}, 'doc' => { "a" => "b"}}
|
34
|
+
], [
|
35
|
+
doc1,
|
36
|
+
doc2updated = {'id' => '2', 'value' => {'rev' => '1-22222'}, 'doc' => { "a" => "c"}}
|
37
|
+
]
|
38
|
+
|
39
|
+
diff.updated.should == [[doc2,doc2updated]]
|
40
|
+
diff.deleted.should be_empty
|
41
|
+
diff.added.should be_empty
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "unchanged content" do
|
46
|
+
it "should not mark doc as updated" do
|
47
|
+
diff = CouchDiff.new [
|
48
|
+
doc1 = {'id' => '1', 'value' => {'rev' => '1-12345'}, 'doc' => {'_rev' => '1-12345', "a" => "b"} },
|
49
|
+
doc2 = {'id' => '2', 'value' => {'rev' => '1-11111'}, 'doc' => { "a" => "b"}}
|
50
|
+
], [
|
51
|
+
# higher revision, but same content as doc1
|
52
|
+
{'id' => '1', 'value' => {'rev' => '3-45678'}, 'doc' => {'_rev' => '3-45678', "a" => "b"}},
|
53
|
+
doc2
|
54
|
+
]
|
55
|
+
|
56
|
+
diff.unchanged.should == [doc1, doc2]
|
57
|
+
diff.added.should be_empty
|
58
|
+
diff.updated.should be_empty
|
59
|
+
diff.deleted.should be_empty
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "custom 'changed' function" do
|
64
|
+
it "should update docs based on user condition" do
|
65
|
+
diff = CouchDiff.new [
|
66
|
+
doc1 = {'id' => '1', 'value' => {'rev' => '1'}, 'doc' => { "a" => "b", "version" => "1.0"}},
|
67
|
+
doc2 = {'id' => '2', 'value' => {'rev' => '1'}, 'doc' => { "a" => "b", "version" => "1.0"}}
|
68
|
+
], [
|
69
|
+
# doc changed (a => "b->c"), but not the version ==> unchanged
|
70
|
+
{'id' => '1', 'value' => {'rev' => '2'}, 'doc' => { "a" => "c", "version" => "1.0"}},
|
71
|
+
# version changed ==> changed
|
72
|
+
doc2updated = {'id' => '2', 'value' => {'rev' => '2'}, 'doc' => { "a" => "b", "version" => "2.0"}}
|
73
|
+
] do |src, dst|
|
74
|
+
# do not update if the doc's git revision has not changed
|
75
|
+
src['version'] != dst['version']
|
76
|
+
end
|
77
|
+
|
78
|
+
diff.added.should be_empty
|
79
|
+
diff.unchanged.should == [doc1]
|
80
|
+
diff.updated.should == [[doc2,doc2updated]]
|
81
|
+
diff.deleted.should be_empty
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should always copy missing attachments" do
|
86
|
+
doc = {'id' => '1', 'value' => {'rev' => '1'}, 'doc' => {
|
87
|
+
"id"=>"1", "_rev" => "1-12345", "version" => "1.0",
|
88
|
+
"_attachments"=>{"file.txt"=>{}, "revpos" => "1"}
|
89
|
+
}}
|
90
|
+
doc_no_attachment = {'id' => '1', 'value' => {'rev' => '1'}, 'doc' => {
|
91
|
+
"id"=>"1", "_rev" => "1-12345", "version" => "1.0",
|
92
|
+
"_attachments"=>{}
|
93
|
+
}}
|
94
|
+
|
95
|
+
diff = CouchDiff.new [ doc ],[ doc_no_attachment ] do |src, dst|
|
96
|
+
# do not update if the doc's git revision has not changed
|
97
|
+
src['version'] != dst['version']
|
98
|
+
end
|
99
|
+
|
100
|
+
diff.added.should be_empty
|
101
|
+
diff.unchanged.should be_empty
|
102
|
+
diff.updated.should == [[doc,doc_no_attachment]]
|
103
|
+
diff.deleted.should be_empty
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe "diff" do
|
108
|
+
|
109
|
+
it "should include all fields in diff" do
|
110
|
+
doc1 = {"id"=>"foo", "_rev" => "1-12345", "_attachments"=> {"file.txt" => {"revpos" => "1"}}}
|
111
|
+
doc2 = {"id"=>"foo", "_rev" => "2-12345", "_attachments"=> {"file.txt" => {"revpos" => "2"}}}
|
112
|
+
|
113
|
+
doc1.diff(doc2).should == {"_rev"=>["1-12345", "2-12345"], "_attachments" => {"file.txt" => {"revpos"=>["1", "2"]}}}
|
114
|
+
end
|
115
|
+
|
116
|
+
it "should exclude fields specified as excluded" do
|
117
|
+
doc1 = {"id"=>"foo", "_rev" => "1-12345", "_attachments"=>{"file.txt"=>{ "revpos" => "1" }}}
|
118
|
+
doc2 = {"id"=>"foo", "_rev" => "2-12345", "_attachments"=>{"file.txt"=>{ "revpos" => "2" }}}
|
119
|
+
# without
|
120
|
+
doc1.diff(doc2, ["_rev", "revpos"]).should == {}
|
121
|
+
end
|
122
|
+
|
123
|
+
it "should identify when order of attachments (= order of keys in hash) changes" do
|
124
|
+
doc1 = {"id"=>"foo", "_rev" => "1-12345", "_attachments"=>{"file.txt"=>{"revpos" => "1"}, "file2.txt"=>{"revpos" => "1"}}}
|
125
|
+
doc2 = {"id"=>"foo", "_rev" => "2-12345", "_attachments"=>{"file2.txt"=>{"revpos" => "1"}, "file.txt"=>{"revpos" => "1"}}}
|
126
|
+
doc1.diff(doc2, ["_rev", "revpos"]).should == {"_attachments"=> {:_keys_changed=>[["file.txt", "file2.txt"], ["file2.txt", "file.txt"]]}}
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
metadata
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: couchdiff
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jens Schmidt
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-01-23 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: couchrest
|
16
|
+
requirement: &2152664100 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.1.2
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2152664100
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: progressbar
|
27
|
+
requirement: &2152662580 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0.9'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2152662580
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: json
|
38
|
+
requirement: &2152661060 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 1.6.0
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *2152661060
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rspec
|
49
|
+
requirement: &2152659680 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.7.0
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *2152659680
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: hoe
|
60
|
+
requirement: &2152658280 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ~>
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '2.12'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *2152658280
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rdoc
|
71
|
+
requirement: &2156945080 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ~>
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '3.10'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *2156945080
|
80
|
+
description: ''
|
81
|
+
email:
|
82
|
+
- jens@mudynamics.com
|
83
|
+
executables:
|
84
|
+
- couchdiff
|
85
|
+
extensions: []
|
86
|
+
extra_rdoc_files:
|
87
|
+
- History.txt
|
88
|
+
- Manifest.txt
|
89
|
+
files:
|
90
|
+
- History.txt
|
91
|
+
- Manifest.txt
|
92
|
+
- README.md
|
93
|
+
- Rakefile
|
94
|
+
- bin/couchdiff
|
95
|
+
- lib/couchdiff.rb
|
96
|
+
- lib/copier.rb
|
97
|
+
- spec/copier_spec.rb
|
98
|
+
- spec/couchdiff_spec.rb
|
99
|
+
- .gemtest
|
100
|
+
homepage: http://github.com/mudynamics/couchdiff
|
101
|
+
licenses: []
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options:
|
104
|
+
- --main
|
105
|
+
- README.txt
|
106
|
+
require_paths:
|
107
|
+
- lib
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
109
|
+
none: false
|
110
|
+
requirements:
|
111
|
+
- - ! '>='
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0'
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
|
+
none: false
|
116
|
+
requirements:
|
117
|
+
- - ! '>='
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
requirements: []
|
121
|
+
rubyforge_project: couchdiff
|
122
|
+
rubygems_version: 1.8.10
|
123
|
+
signing_key:
|
124
|
+
specification_version: 3
|
125
|
+
summary: ''
|
126
|
+
test_files: []
|