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 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