delta 1.0.0 → 2.0.0
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.
- checksums.yaml +4 -4
- data/README.md +45 -58
- data/lib/delta/base.rb +14 -14
- data/lib/delta/set_operator/active_record.rb +65 -21
- data/lib/delta/set_operator/enumerable.rb +37 -3
- data/lib/delta/set_operator.rb +15 -9
- data/lib/delta.rb +0 -2
- metadata +2 -3
- data/lib/delta/plucker.rb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 030d9b3e370fd7a222a7b065e0340056a93a3ec8
|
4
|
+
data.tar.gz: 898fff561d39066111462b0e7c6dfc233c5c6bea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f45a17ae319a21011519c0c0d6d804e630e39e425baf30484df5b19d8dc4fb21e8ffe9e8e81a5db390ce82c9795dd92be0fb432fe0182a4f88db83c92fa2161f
|
7
|
+
data.tar.gz: 6ea4a6e6154f88f5901bc1f61b7ba03745f20922a1ed584549762ebf5316d590a75f6c704579e3656f1e330c8667ede3de482bf71f8d34fd82f782793f0c111c
|
data/README.md
CHANGED
@@ -31,109 +31,97 @@ delta.deletions.to_a
|
|
31
31
|
]
|
32
32
|
```
|
33
33
|
|
34
|
-
##
|
34
|
+
## Identifiers
|
35
35
|
|
36
|
-
|
37
|
-
|
38
|
-
|
36
|
+
By default, Delta uses
|
37
|
+
[object_id](http://ruby-doc.org/core-2.2.2/Object.html#method-i-object_id) to
|
38
|
+
compare the collections. You can tell Delta to use something else by setting the
|
39
|
+
`identifiers` parameter:
|
39
40
|
|
40
41
|
```ruby
|
41
42
|
delta = Delta.new(
|
42
43
|
from: [pikachu, pidgey, magikarp],
|
43
44
|
to: [raichu, pidgey, butterfree],
|
44
|
-
|
45
|
+
identifiers: [:name]
|
45
46
|
)
|
46
47
|
|
47
48
|
delta.additions.to_a
|
48
|
-
#=> [
|
49
|
-
#<struct name="Zappy", species="Pikachu">,
|
50
|
-
#<struct name="Flappy", species="Butterfree">
|
51
|
-
]
|
49
|
+
#=> [#<Pokemon @species="Butterfree", @name="Flappy", @type="Flying">]
|
52
50
|
|
53
51
|
delta.deletions.to_a
|
54
|
-
#=> [
|
55
|
-
#<struct name="Zappy", species="Raichu">,
|
56
|
-
#<struct name="Splashy", species="Magikarp">
|
57
|
-
]
|
52
|
+
#=> [#<Pokemon @species="Magikarp", @name="Splashy", @type="Water">]
|
58
53
|
```
|
59
54
|
|
55
|
+
In this case, 'Raichu' and 'Pikachu' don't appear in the additions or deletions
|
56
|
+
because they share the same name and Delta thinks that they are the same object.
|
57
|
+
|
60
58
|
## Modifications
|
61
59
|
|
62
60
|
In most cases, it is more appropriate to think in terms of modifications rather
|
63
|
-
than additions and deletions.
|
64
|
-
deletion and 'Raichu' appeared as an addition. It might make more sense to model
|
65
|
-
this as a modification, i.e. 'Zappy' changing its 'species'.
|
61
|
+
than additions and deletions.
|
66
62
|
|
67
|
-
|
68
|
-
|
63
|
+
To calculate modifications, Delta needs to know which things might change on the
|
64
|
+
objects. You can tell Delta which things are subject to change by setting the
|
65
|
+
`changes` parameter:
|
69
66
|
|
70
67
|
```ruby
|
71
68
|
delta = Delta.new(
|
72
69
|
from: [pikachu, pidgey, magikarp],
|
73
70
|
to: [raichu, pidgey, butterfree],
|
74
|
-
|
75
|
-
|
71
|
+
identifiers: [:name],
|
72
|
+
changes: [:species]
|
76
73
|
)
|
77
74
|
|
78
75
|
delta.additions.to_a
|
79
|
-
#=> [#<
|
76
|
+
#=> [#<Pokemon @species="Butterfree", @name="Flappy", @type="Flying">]
|
80
77
|
|
81
78
|
delta.modifications.to_a
|
82
|
-
#=> [#<
|
79
|
+
#=> [#<Pokemon @species="Raichu", @name="Zappy", @type="Electric">]
|
83
80
|
|
84
81
|
delta.deletions.to_a
|
85
|
-
#=> [#<
|
82
|
+
#=> [#<Pokemon @species="Magikarp", @name="Splashy", @type="Water">]
|
86
83
|
```
|
87
84
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
If you do not specifiy any attributes to `pluck`, Delta will fall back to using
|
93
|
-
object equality to determine which objects have changed. For ActiveRecord
|
94
|
-
relations, Delta will use the database id as its fall back as it is more
|
95
|
-
performant to do so.
|
85
|
+
In the above example, 'Zappy' appears as a modification because Delta thinks
|
86
|
+
that 'Pikachu' and 'Raichu' are the same object because they share the same
|
87
|
+
name.
|
96
88
|
|
97
89
|
## Composite Keys
|
98
90
|
|
99
91
|
In some cases, objects will be uniquely identified by a combination of things.
|
100
92
|
For example, consider an application that enforces the uniqueness of names, but
|
101
|
-
only in the context of a 'type'.
|
102
|
-
same name, as long as they are not of the same type (e.g. 'Water').
|
93
|
+
only in the context of a 'type'.
|
103
94
|
|
104
|
-
In these cases, you can specify multiple
|
95
|
+
In these cases, you can specify multiple `identifiers`:
|
105
96
|
|
106
97
|
```ruby
|
107
98
|
delta = Delta.new(
|
108
99
|
from: [pikachu, pidgey, magikarp],
|
109
100
|
to: [raichu, pidgey, butterfree],
|
110
|
-
|
111
|
-
|
101
|
+
identifiers: [:name, :type]
|
102
|
+
changes: [:species]
|
112
103
|
)
|
113
104
|
|
114
105
|
delta.additions.to_a
|
115
|
-
#=> [#<
|
106
|
+
#=> [#<Pokemon @species="Butterfree", @name="FANG!", @type="Flying">]
|
116
107
|
|
117
108
|
delta.modifications.to_a
|
118
|
-
#=> [#<
|
109
|
+
#=> [#<Pokemon @species="Raichu", @name="Zappy", @type="Electric">]
|
119
110
|
|
120
111
|
delta.deletions.to_a
|
121
|
-
#=> [#<
|
112
|
+
#=> [#<Pokemon @species="Magikarp", @name="FANG!", @type="Water">]
|
122
113
|
```
|
123
114
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
TODO: What would happen if you did that? Should it raise an error?
|
115
|
+
If 'name' were the only identifier, Delta would think that 'Butterfree' and
|
116
|
+
'Magikarp' are the same object and they would not appear in additions or
|
117
|
+
deletions.
|
129
118
|
|
130
|
-
## Many
|
119
|
+
## Many to One
|
131
120
|
|
132
|
-
|
133
|
-
|
134
|
-
the collection.
|
121
|
+
It is possible to build deltas that aren't necessarily related to a single
|
122
|
+
object, but instead, span multiple objects within the collections.
|
135
123
|
|
136
|
-
This may be useful if there are objects in your
|
124
|
+
This may be useful if there are objects in your collection that share some
|
137
125
|
property. In this example, all of the Pokemon have a 'type'. We can build a
|
138
126
|
Delta that shows the difference in types between collections, like so:
|
139
127
|
|
@@ -141,8 +129,7 @@ Delta that shows the difference in types between collections, like so:
|
|
141
129
|
delta = Delta.new(
|
142
130
|
from: [pikachu, pidgey, magikarp],
|
143
131
|
to: [raichu, pidgey, butterfree],
|
144
|
-
|
145
|
-
pluck: [:type]
|
132
|
+
identifiers: [:type]
|
146
133
|
)
|
147
134
|
|
148
135
|
delta.additions.to_a
|
@@ -152,19 +139,19 @@ delta.modifications.to_a
|
|
152
139
|
#=> []
|
153
140
|
|
154
141
|
delta.deletions.to_a
|
155
|
-
#=> [#<
|
142
|
+
#=> [#<Pokemon @species="Magikarp", @name="Splashy", @type="Water">]
|
156
143
|
```
|
157
144
|
|
158
145
|
In this example, both 'Electric' and 'Flying' types appear in both collections.
|
159
|
-
The only difference is the deletion of all 'Water' type Pokemon
|
160
|
-
|
146
|
+
The only difference is the deletion of all 'Water' type Pokemon. Note that Delta
|
147
|
+
will only return the first object that matches the deleted type.
|
161
148
|
|
162
149
|
## Rails Support
|
163
150
|
|
164
|
-
Delta has added support for Rails applications. If the
|
165
|
-
|
166
|
-
|
167
|
-
|
151
|
+
Delta has added support for Rails applications. If the collections are of class
|
152
|
+
ActiveRecord::Relation, Delta will try to improve performance by reducing the
|
153
|
+
number of select queries on the database. This may not work with old versions of
|
154
|
+
Rails.
|
168
155
|
|
169
156
|
## In the Wild
|
170
157
|
|
data/lib/delta/base.rb
CHANGED
@@ -1,38 +1,38 @@
|
|
1
1
|
class Delta
|
2
|
-
def initialize(from:, to:,
|
3
|
-
|
4
|
-
|
5
|
-
|
2
|
+
def initialize(from:, to:, identifiers: nil, changes: [])
|
3
|
+
self.set = SetOperator.adapt(
|
4
|
+
a: from,
|
5
|
+
b: to,
|
6
|
+
identifiers: identifiers,
|
7
|
+
changes: changes
|
8
|
+
)
|
6
9
|
end
|
7
10
|
|
8
11
|
def additions
|
9
12
|
Enumerator.new do |y|
|
10
|
-
set.
|
11
|
-
y.yield
|
13
|
+
set.b_minus_a.each do |b|
|
14
|
+
y.yield b
|
12
15
|
end
|
13
16
|
end
|
14
17
|
end
|
15
18
|
|
16
19
|
def modifications
|
17
20
|
Enumerator.new do |y|
|
18
|
-
set.intersection.each do |
|
19
|
-
|
20
|
-
b_attributes = plucker.pluck(b)
|
21
|
-
|
22
|
-
y.yield b_attributes unless a_attributes == b_attributes
|
21
|
+
set.intersection.each do |b|
|
22
|
+
y.yield b
|
23
23
|
end
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
27
|
def deletions
|
28
28
|
Enumerator.new do |y|
|
29
|
-
set.
|
30
|
-
y.yield
|
29
|
+
set.a_minus_b.each do |a|
|
30
|
+
y.yield a
|
31
31
|
end
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
35
|
private
|
36
36
|
|
37
|
-
attr_accessor :set
|
37
|
+
attr_accessor :set
|
38
38
|
end
|
@@ -5,41 +5,85 @@ class Delta
|
|
5
5
|
a.is_a?(b.class) && a.class.name.include?("ActiveRecord")
|
6
6
|
end
|
7
7
|
|
8
|
+
def initialize(a:, b:, identifiers: nil, changes:)
|
9
|
+
super
|
10
|
+
|
11
|
+
self.identifiers = identifiers || [:id]
|
12
|
+
end
|
13
|
+
|
8
14
|
private
|
9
15
|
|
10
|
-
def subtract(
|
11
|
-
a
|
16
|
+
def subtract(a_scope, b_scope)
|
17
|
+
a = arel_table(a_scope, "a")
|
18
|
+
b = arel_table(b_scope, "b")
|
19
|
+
|
20
|
+
query = build_query(a_scope, b_scope, left_join)
|
21
|
+
|
22
|
+
query = query.project(a[Arel.star])
|
23
|
+
query = query.where(b[identifiers.first].eq(nil))
|
24
|
+
|
25
|
+
execute(query, a_scope)
|
12
26
|
end
|
13
27
|
|
14
|
-
def intersect(
|
15
|
-
|
28
|
+
def intersect(a_scope, b_scope)
|
29
|
+
return [] if changes.empty?
|
30
|
+
|
31
|
+
a = arel_table(a_scope, "a")
|
32
|
+
b = arel_table(b_scope, "b")
|
33
|
+
|
34
|
+
query = build_query(a_scope, b_scope, inner_join)
|
35
|
+
|
36
|
+
query = query.project(Arel.star)
|
37
|
+
query = query.where(attribute_clause(a, b))
|
38
|
+
|
39
|
+
execute(query, a_scope)
|
40
|
+
end
|
16
41
|
|
17
|
-
|
18
|
-
|
42
|
+
def build_query(a_scope, b_scope, join_type)
|
43
|
+
a_query = nested_query(a_scope, "a")
|
44
|
+
b_query = nested_query(b_scope, "b")
|
45
|
+
|
46
|
+
a = arel_table(a_scope, "a")
|
47
|
+
b = arel_table(b_scope, "b")
|
48
|
+
|
49
|
+
query = a.from(a_query)
|
50
|
+
query = query.join(b.join(b_query).join_sources, join_type)
|
51
|
+
query = query.on(*identity_clauses(a, b))
|
52
|
+
query = query.group(*group_clauses(a, b))
|
53
|
+
|
54
|
+
query
|
55
|
+
end
|
56
|
+
|
57
|
+
def identity_clauses(a, b)
|
58
|
+
identifiers.map { |k| a[k].eq(b[k]) }
|
59
|
+
end
|
60
|
+
|
61
|
+
def attribute_clause(a, b)
|
62
|
+
changes.map { |k| a[k].not_eq(b[k]) }.inject(&:or)
|
63
|
+
end
|
64
|
+
|
65
|
+
def group_clauses(a, _b)
|
66
|
+
identifiers.map { |k| a[k] }
|
67
|
+
end
|
19
68
|
|
20
|
-
|
21
|
-
|
22
|
-
y.yield find_pair!(record, keys)
|
23
|
-
end
|
24
|
-
end
|
69
|
+
def execute(query, scope)
|
70
|
+
scope.model.find_by_sql(query.to_sql)
|
25
71
|
end
|
26
72
|
|
27
|
-
def
|
28
|
-
|
73
|
+
def arel_table(scope, name)
|
74
|
+
Arel::Table.new(scope.arel_table.name, as: name)
|
29
75
|
end
|
30
76
|
|
31
|
-
def
|
32
|
-
|
33
|
-
a.where(b_identities)
|
77
|
+
def nested_query(scope, name)
|
78
|
+
Arel.sql("(#{scope.to_sql}) as #{name}")
|
34
79
|
end
|
35
80
|
|
36
|
-
def
|
37
|
-
|
81
|
+
def left_join
|
82
|
+
Arel::Nodes::OuterJoin
|
38
83
|
end
|
39
84
|
|
40
|
-
def
|
41
|
-
|
42
|
-
[a.find_by!(attributes), b.find_by!(attributes)]
|
85
|
+
def inner_join
|
86
|
+
Arel::Nodes::InnerJoin
|
43
87
|
end
|
44
88
|
end
|
45
89
|
end
|
@@ -5,9 +5,10 @@ class Delta
|
|
5
5
|
true
|
6
6
|
end
|
7
7
|
|
8
|
-
def initialize(a:, b:,
|
8
|
+
def initialize(a:, b:, identifiers: nil, changes:)
|
9
9
|
super
|
10
10
|
|
11
|
+
self.identifiers = identifiers || [:object_id]
|
11
12
|
self.a = a.lazy
|
12
13
|
self.b = b.lazy
|
13
14
|
end
|
@@ -19,15 +20,48 @@ class Delta
|
|
19
20
|
end
|
20
21
|
|
21
22
|
def intersect(a, b)
|
23
|
+
return [] if changes.empty?
|
24
|
+
|
25
|
+
pairs = pairs(a, b).reject do |a_object, b_object|
|
26
|
+
attributes(a_object) == attributes(b_object)
|
27
|
+
end
|
28
|
+
|
29
|
+
pairs.map { |_, b_object| b_object }
|
30
|
+
end
|
31
|
+
|
32
|
+
def pairs(a, b)
|
22
33
|
a.map { |a_object| [a_object, other_object(b, a_object)] }
|
23
34
|
.select { |_, b_object| b_object }
|
24
35
|
end
|
25
36
|
|
26
37
|
def other_object(collection, object)
|
27
|
-
identity =
|
38
|
+
identity = identity(object)
|
28
39
|
|
29
40
|
collection.find do |other_object|
|
30
|
-
identity ==
|
41
|
+
identity == identity(other_object)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def identity(object)
|
46
|
+
cache(:identity, object) do
|
47
|
+
identifiers.map { |m| object.public_send(m) }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def attributes(object)
|
52
|
+
cache(:attributes, object) do
|
53
|
+
changes.map { |m| object.public_send(m) }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def cache(name, key)
|
58
|
+
@cache ||= {}
|
59
|
+
@cache[name] ||= {}
|
60
|
+
|
61
|
+
if @cache[name].key?(key)
|
62
|
+
@cache[name][key]
|
63
|
+
else
|
64
|
+
@cache[name][key] = yield
|
31
65
|
end
|
32
66
|
end
|
33
67
|
end
|
data/lib/delta/set_operator.rb
CHANGED
@@ -2,25 +2,31 @@ class Delta
|
|
2
2
|
class SetOperator
|
3
3
|
ADAPTERS = [ActiveRecord, Enumerable]
|
4
4
|
|
5
|
-
def self.adapt(a:, b:,
|
5
|
+
def self.adapt(a:, b:, identifiers:, changes:)
|
6
6
|
adapter = ADAPTERS.find { |klass| klass.compatible?(a, b) }
|
7
7
|
adapter = ADAPTERS.last unless adapter
|
8
8
|
|
9
|
-
adapter.new(
|
9
|
+
adapter.new(
|
10
|
+
a: a,
|
11
|
+
b: b,
|
12
|
+
identifiers: identifiers,
|
13
|
+
changes: changes
|
14
|
+
)
|
10
15
|
end
|
11
16
|
|
12
|
-
def initialize(a:, b:,
|
17
|
+
def initialize(a:, b:, identifiers:, changes:)
|
13
18
|
self.a = a
|
14
19
|
self.b = b
|
15
|
-
self.
|
20
|
+
self.identifiers = identifiers
|
21
|
+
self.changes = changes
|
16
22
|
end
|
17
23
|
|
18
|
-
def
|
19
|
-
subtract(
|
24
|
+
def a_minus_b
|
25
|
+
subtract(a, b)
|
20
26
|
end
|
21
27
|
|
22
|
-
def
|
23
|
-
subtract(
|
28
|
+
def b_minus_a
|
29
|
+
subtract(b, a)
|
24
30
|
end
|
25
31
|
|
26
32
|
def intersection
|
@@ -29,7 +35,7 @@ class Delta
|
|
29
35
|
|
30
36
|
protected
|
31
37
|
|
32
|
-
attr_accessor :a, :b, :
|
38
|
+
attr_accessor :a, :b, :identifiers, :changes
|
33
39
|
|
34
40
|
private
|
35
41
|
|
data/lib/delta.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: delta
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Patuzzo
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-05-
|
11
|
+
date: 2015-05-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -104,7 +104,6 @@ files:
|
|
104
104
|
- lib/delta.rb
|
105
105
|
- lib/delta/base.rb
|
106
106
|
- lib/delta/identifier.rb
|
107
|
-
- lib/delta/plucker.rb
|
108
107
|
- lib/delta/set_operator.rb
|
109
108
|
- lib/delta/set_operator/active_record.rb
|
110
109
|
- lib/delta/set_operator/enumerable.rb
|
data/lib/delta/plucker.rb
DELETED
@@ -1,23 +0,0 @@
|
|
1
|
-
class Delta
|
2
|
-
class Plucker
|
3
|
-
def initialize(pluck)
|
4
|
-
self.array = pluck
|
5
|
-
self.struct = Struct.new(*array)
|
6
|
-
end
|
7
|
-
|
8
|
-
def pluck(object)
|
9
|
-
attributes = array.map { |k| object.public_send(k) }
|
10
|
-
struct.new(*attributes)
|
11
|
-
end
|
12
|
-
|
13
|
-
private
|
14
|
-
|
15
|
-
attr_accessor :array, :struct
|
16
|
-
|
17
|
-
class Null
|
18
|
-
def pluck(object)
|
19
|
-
object
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|