delta 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b03dae554a7f32d4fc3826fd35aa36f82549c2b1
4
- data.tar.gz: 9b0bf35d479c98b32d2eacc38c4f71128464a2ea
3
+ metadata.gz: 030d9b3e370fd7a222a7b065e0340056a93a3ec8
4
+ data.tar.gz: 898fff561d39066111462b0e7c6dfc233c5c6bea
5
5
  SHA512:
6
- metadata.gz: 55edc3a5987c896b0c296db1171a39bf80a24ada940b7bdaa59a5d5ba5464971f4ccbccfc206bd0e4bd5e330684b8c96d21124667eaf812e4bae0ce71eabb4d7
7
- data.tar.gz: ad09180100f9018bd3feee520ae5343512d7a174efc6f633bac594650ed3d15af965196321b1cc04efdc3b1bf28739210c6b4323faed9ccb6d507f261eaf69cc
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
- ## Pluck
34
+ ## Identifiers
35
35
 
36
- Sometimes, you'll only be interested in a few of the attributes on an object.
37
- Delta supports `pluck`, which selects only the attributes you're interested in,
38
- instead of returning full objects:
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
- pluck: [:name, :species]
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. In the example above, 'Pikachu' appeared as a
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
- Delta supports setting the `keys` that uniquely identify each object in the
68
- collection:
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
- pluck: [:species, :name],
75
- keys: [:name]
71
+ identifiers: [:name],
72
+ changes: [:species]
76
73
  )
77
74
 
78
75
  delta.additions.to_a
79
- #=> [#<struct species="Butterfree", name="Flappy">]
76
+ #=> [#<Pokemon @species="Butterfree", @name="Flappy", @type="Flying">]
80
77
 
81
78
  delta.modifications.to_a
82
- #=> [#<struct species="Raichu", name="Zappy">]
79
+ #=> [#<Pokemon @species="Raichu", @name="Zappy", @type="Electric">]
83
80
 
84
81
  delta.deletions.to_a
85
- #=> [#<struct species="Magikarp", name="Splashy">]
82
+ #=> [#<Pokemon @species="Magikarp", @name="Splashy", @type="Water">]
86
83
  ```
87
84
 
88
- You should specify which attributes to `pluck` so that Delta can distinguish
89
- between objects that have changed and objects that have not changed, but appear
90
- in both collections.
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'. This would mean that two Pokemon can have the
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 keys:
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
- pluck: [:species, :name, :type],
111
- keys: [:name, :type] # <--
101
+ identifiers: [:name, :type]
102
+ changes: [:species]
112
103
  )
113
104
 
114
105
  delta.additions.to_a
115
- #=> [#<struct species="Butterfree", name="FANG!", type="Flying">]
106
+ #=> [#<Pokemon @species="Butterfree", @name="FANG!", @type="Flying">]
116
107
 
117
108
  delta.modifications.to_a
118
- #=> [#<struct species="Raichu", name="Zappy", type="Electric">]
109
+ #=> [#<Pokemon @species="Raichu", @name="Zappy", @type="Electric">]
119
110
 
120
111
  delta.deletions.to_a
121
- #=> [#<struct species="Magikarp", name="FANG!", type="Water">]
112
+ #=> [#<Pokemon @species="Magikarp", @name="FANG!", @type="Water">]
122
113
  ```
123
114
 
124
- Consider the alternative where 'name' is used as the only key. This would mean
125
- that the Pokemon with species' 'Butterfree' and 'Magikarp' would be considered
126
- the same. This is semantically incorrect for this particular domain.
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-to-one Deltas
119
+ ## Many to One
131
120
 
132
- By combining the use of `pluck` and `keys`, you can build deltas that aren't
133
- necessarily related to a single object, but instead span multiple objects within
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 collections that share some
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
- keys: [:type],
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
- #=> [#<struct type="Water">]
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 as a result of
160
- removing 'Magikarp' from the first collection.
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 given collections are of
165
- class ActiveRecord::Relation, Delta will try to improve performance by reducing
166
- the number of select queries on the database. This isn't perfect and may not
167
- work with old versions of Rails.
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:, pluck: nil, keys: nil)
3
- identifier = keys ? Identifier.new(keys) : Identifier::Null.new
4
- self.set = SetOperator.adapt(a: from, b: to, identifier: identifier)
5
- self.plucker = pluck ? Plucker.new(pluck) : Plucker::Null.new
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.subtract_a_from_b.each do |b|
11
- y.yield plucker.pluck(b)
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 |a, b|
19
- a_attributes = plucker.pluck(a)
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.subtract_b_from_a.each do |a|
30
- y.yield plucker.pluck(a)
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, :plucker
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(a, b)
11
- a.where.not(identifier.identities(b))
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(a, b)
15
- keys = identity_keys(a)
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
- a_records = left_of_intersection(a, b)
18
- a_records = unique_records(a_records, keys)
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
- Enumerator.new do |y|
21
- a_records.each do |record|
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 identity_keys(collection)
28
- identifier.identities(collection).keys
73
+ def arel_table(scope, name)
74
+ Arel::Table.new(scope.arel_table.name, as: name)
29
75
  end
30
76
 
31
- def left_of_intersection(a, b)
32
- b_identities = identifier.identities(b)
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 unique_records(collection, keys)
37
- collection.select(*keys).distinct
81
+ def left_join
82
+ Arel::Nodes::OuterJoin
38
83
  end
39
84
 
40
- def find_pair!(record, keys)
41
- attributes = record.slice(*keys)
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:, identifier:)
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 = identifier.identity(object)
38
+ identity = identity(object)
28
39
 
29
40
  collection.find do |other_object|
30
- identity == identifier.identity(other_object)
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
@@ -2,25 +2,31 @@ class Delta
2
2
  class SetOperator
3
3
  ADAPTERS = [ActiveRecord, Enumerable]
4
4
 
5
- def self.adapt(a:, b:, identifier:)
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(a: a, b: b, identifier: identifier)
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:, identifier:)
17
+ def initialize(a:, b:, identifiers:, changes:)
13
18
  self.a = a
14
19
  self.b = b
15
- self.identifier = identifier
20
+ self.identifiers = identifiers
21
+ self.changes = changes
16
22
  end
17
23
 
18
- def subtract_a_from_b
19
- subtract(b, a)
24
+ def a_minus_b
25
+ subtract(a, b)
20
26
  end
21
27
 
22
- def subtract_b_from_a
23
- subtract(a, b)
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, :identifier
38
+ attr_accessor :a, :b, :identifiers, :changes
33
39
 
34
40
  private
35
41
 
data/lib/delta.rb CHANGED
@@ -2,5 +2,3 @@ require "delta/base"
2
2
  require "delta/set_operator/enumerable"
3
3
  require "delta/set_operator/active_record"
4
4
  require "delta/set_operator"
5
- require "delta/plucker"
6
- require "delta/identifier"
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: 1.0.0
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-24 00:00:00.000000000 Z
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