delta 1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b03dae554a7f32d4fc3826fd35aa36f82549c2b1
4
+ data.tar.gz: 9b0bf35d479c98b32d2eacc38c4f71128464a2ea
5
+ SHA512:
6
+ metadata.gz: 55edc3a5987c896b0c296db1171a39bf80a24ada940b7bdaa59a5d5ba5464971f4ccbccfc206bd0e4bd5e330684b8c96d21124667eaf812e4bae0ce71eabb4d7
7
+ data.tar.gz: ad09180100f9018bd3feee520ae5343512d7a174efc6f633bac594650ed3d15af965196321b1cc04efdc3b1bf28739210c6b4323faed9ccb6d507f261eaf69cc
data/README.md ADDED
@@ -0,0 +1,190 @@
1
+ ## Delta
2
+
3
+ [![Build Status](https://travis-ci.org/tuzz/delta.svg?branch=master)](https://travis-ci.org/tuzz/delta)
4
+
5
+ Calculates the delta between two collections of objects.
6
+
7
+ ## Usage
8
+
9
+ In its simplest form, Delta calculates the additions and deletions for arbitrary
10
+ collections of objects:
11
+
12
+ ```ruby
13
+ delta = Delta.new(
14
+ from: [pikachu, pidgey, magikarp],
15
+ to: [raichu, pidgey, butterfree]
16
+ )
17
+
18
+ delta.additions
19
+ #=> #<Enumerator: ... >
20
+
21
+ delta.additions.to_a
22
+ #=> [
23
+ #<Pokemon @species="Raichu", @name="Zappy", @type="Electric">,
24
+ #<Pokemon @species="Butterfree", @name="Flappy", @type="Flying">
25
+ ]
26
+
27
+ delta.deletions.to_a
28
+ #=> [
29
+ #<Pokemon @species="Pikachu", @name="Zappy", @type="Electric">,
30
+ #<Pokemon @species="Magikarp", @name="Splashy", @type="Water">
31
+ ]
32
+ ```
33
+
34
+ ## Pluck
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:
39
+
40
+ ```ruby
41
+ delta = Delta.new(
42
+ from: [pikachu, pidgey, magikarp],
43
+ to: [raichu, pidgey, butterfree],
44
+ pluck: [:name, :species]
45
+ )
46
+
47
+ delta.additions.to_a
48
+ #=> [
49
+ #<struct name="Zappy", species="Pikachu">,
50
+ #<struct name="Flappy", species="Butterfree">
51
+ ]
52
+
53
+ delta.deletions.to_a
54
+ #=> [
55
+ #<struct name="Zappy", species="Raichu">,
56
+ #<struct name="Splashy", species="Magikarp">
57
+ ]
58
+ ```
59
+
60
+ ## Modifications
61
+
62
+ 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'.
66
+
67
+ Delta supports setting the `keys` that uniquely identify each object in the
68
+ collection:
69
+
70
+ ```ruby
71
+ delta = Delta.new(
72
+ from: [pikachu, pidgey, magikarp],
73
+ to: [raichu, pidgey, butterfree],
74
+ pluck: [:species, :name],
75
+ keys: [:name]
76
+ )
77
+
78
+ delta.additions.to_a
79
+ #=> [#<struct species="Butterfree", name="Flappy">]
80
+
81
+ delta.modifications.to_a
82
+ #=> [#<struct species="Raichu", name="Zappy">]
83
+
84
+ delta.deletions.to_a
85
+ #=> [#<struct species="Magikarp", name="Splashy">]
86
+ ```
87
+
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.
96
+
97
+ ## Composite Keys
98
+
99
+ In some cases, objects will be uniquely identified by a combination of things.
100
+ 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').
103
+
104
+ In these cases, you can specify multiple keys:
105
+
106
+ ```ruby
107
+ delta = Delta.new(
108
+ from: [pikachu, pidgey, magikarp],
109
+ to: [raichu, pidgey, butterfree],
110
+ pluck: [:species, :name, :type],
111
+ keys: [:name, :type] # <--
112
+ )
113
+
114
+ delta.additions.to_a
115
+ #=> [#<struct species="Butterfree", name="FANG!", type="Flying">]
116
+
117
+ delta.modifications.to_a
118
+ #=> [#<struct species="Raichu", name="Zappy", type="Electric">]
119
+
120
+ delta.deletions.to_a
121
+ #=> [#<struct species="Magikarp", name="FANG!", type="Water">]
122
+ ```
123
+
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?
129
+
130
+ ## Many-to-one Deltas
131
+
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.
135
+
136
+ This may be useful if there are objects in your collections that share some
137
+ property. In this example, all of the Pokemon have a 'type'. We can build a
138
+ Delta that shows the difference in types between collections, like so:
139
+
140
+ ```ruby
141
+ delta = Delta.new(
142
+ from: [pikachu, pidgey, magikarp],
143
+ to: [raichu, pidgey, butterfree],
144
+ keys: [:type],
145
+ pluck: [:type]
146
+ )
147
+
148
+ delta.additions.to_a
149
+ #=> []
150
+
151
+ delta.modifications.to_a
152
+ #=> []
153
+
154
+ delta.deletions.to_a
155
+ #=> [#<struct type="Water">]
156
+ ```
157
+
158
+ 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.
161
+
162
+ ## Rails Support
163
+
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.
168
+
169
+ ## In the Wild
170
+
171
+ So far, we've talked a lot about Pokemon, but how is this useful in the
172
+ real-world?
173
+
174
+ Consider an application that is backed by an external content management system.
175
+ You have some kind of extract-transform-load process that takes content from the
176
+ CMS and pushes it into your database. Your application is serving live traffic
177
+ and needs to remain available at all times.
178
+
179
+ To minimise the amount of churn on your database, it makes sense to calculate
180
+ just the things that have changed, rather than rebuilding the entire set of
181
+ content each time. To do this, you could push the CMS content into a staging
182
+ database where you calculate a Delta without touching your live application.
183
+
184
+ Once this Delta is calculated, you can then stream the minimal set of changes
185
+ to the database that backs your application.
186
+
187
+ ## Contribution
188
+
189
+ Thanks for the using this gem. If you think of a great new feature or you find a
190
+ problem, please open an issue or a pull request and I'll try to get back to you.
data/lib/delta/base.rb ADDED
@@ -0,0 +1,38 @@
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
6
+ end
7
+
8
+ def additions
9
+ Enumerator.new do |y|
10
+ set.subtract_a_from_b.each do |b|
11
+ y.yield plucker.pluck(b)
12
+ end
13
+ end
14
+ end
15
+
16
+ def modifications
17
+ 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
23
+ end
24
+ end
25
+ end
26
+
27
+ def deletions
28
+ Enumerator.new do |y|
29
+ set.subtract_b_from_a.each do |a|
30
+ y.yield plucker.pluck(a)
31
+ end
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ attr_accessor :set, :plucker
38
+ end
@@ -0,0 +1,37 @@
1
+ class Delta
2
+ class Identifier
3
+ def initialize(keys)
4
+ self.keys = keys
5
+ end
6
+
7
+ def identity(object)
8
+ cache(object) { Hash[keys.map { |k| [k, object.public_send(k)] }] }
9
+ end
10
+
11
+ def identities(collection)
12
+ cache(collection) { Hash[keys.map { |k| [k, collection.pluck(k).uniq] }] }
13
+ end
14
+
15
+ private
16
+
17
+ attr_accessor :keys
18
+
19
+ def cache(key)
20
+ @cache ||= {}
21
+ @cache.key?(key) ? @cache.fetch(key) : @cache[key] = yield
22
+ end
23
+
24
+ class Null < Identifier
25
+ def initialize
26
+ end
27
+
28
+ def identity(object)
29
+ object
30
+ end
31
+
32
+ def identities(collection)
33
+ cache(collection) { { id: collection.pluck(:id) } }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,23 @@
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
@@ -0,0 +1,46 @@
1
+ class Delta
2
+ class SetOperator
3
+ class ActiveRecord < SetOperator
4
+ def self.compatible?(a, b)
5
+ a.is_a?(b.class) && a.class.name.include?("ActiveRecord")
6
+ end
7
+
8
+ private
9
+
10
+ def subtract(a, b)
11
+ a.where.not(identifier.identities(b))
12
+ end
13
+
14
+ def intersect(a, b)
15
+ keys = identity_keys(a)
16
+
17
+ a_records = left_of_intersection(a, b)
18
+ a_records = unique_records(a_records, keys)
19
+
20
+ Enumerator.new do |y|
21
+ a_records.each do |record|
22
+ y.yield find_pair!(record, keys)
23
+ end
24
+ end
25
+ end
26
+
27
+ def identity_keys(collection)
28
+ identifier.identities(collection).keys
29
+ end
30
+
31
+ def left_of_intersection(a, b)
32
+ b_identities = identifier.identities(b)
33
+ a.where(b_identities)
34
+ end
35
+
36
+ def unique_records(collection, keys)
37
+ collection.select(*keys).distinct
38
+ end
39
+
40
+ def find_pair!(record, keys)
41
+ attributes = record.slice(*keys)
42
+ [a.find_by!(attributes), b.find_by!(attributes)]
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,35 @@
1
+ class Delta
2
+ class SetOperator
3
+ class Enumerable < SetOperator
4
+ def self.compatible?(_a, _b)
5
+ true
6
+ end
7
+
8
+ def initialize(a:, b:, identifier:)
9
+ super
10
+
11
+ self.a = a.lazy
12
+ self.b = b.lazy
13
+ end
14
+
15
+ private
16
+
17
+ def subtract(a, b)
18
+ a.reject { |a_object| other_object(b, a_object) }
19
+ end
20
+
21
+ def intersect(a, b)
22
+ a.map { |a_object| [a_object, other_object(b, a_object)] }
23
+ .select { |_, b_object| b_object }
24
+ end
25
+
26
+ def other_object(collection, object)
27
+ identity = identifier.identity(object)
28
+
29
+ collection.find do |other_object|
30
+ identity == identifier.identity(other_object)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,46 @@
1
+ class Delta
2
+ class SetOperator
3
+ ADAPTERS = [ActiveRecord, Enumerable]
4
+
5
+ def self.adapt(a:, b:, identifier:)
6
+ adapter = ADAPTERS.find { |klass| klass.compatible?(a, b) }
7
+ adapter = ADAPTERS.last unless adapter
8
+
9
+ adapter.new(a: a, b: b, identifier: identifier)
10
+ end
11
+
12
+ def initialize(a:, b:, identifier:)
13
+ self.a = a
14
+ self.b = b
15
+ self.identifier = identifier
16
+ end
17
+
18
+ def subtract_a_from_b
19
+ subtract(b, a)
20
+ end
21
+
22
+ def subtract_b_from_a
23
+ subtract(a, b)
24
+ end
25
+
26
+ def intersection
27
+ intersect(a, b)
28
+ end
29
+
30
+ protected
31
+
32
+ attr_accessor :a, :b, :identifier
33
+
34
+ private
35
+
36
+ # a - b
37
+ def subtract(_a, _b)
38
+ fail NotImplementedError, "override me"
39
+ end
40
+
41
+ # a & b
42
+ def intersect(_a, _b)
43
+ fail NotImplementedError, "override me"
44
+ end
45
+ end
46
+ end
data/lib/delta.rb ADDED
@@ -0,0 +1,6 @@
1
+ require "delta/base"
2
+ require "delta/set_operator/enumerable"
3
+ require "delta/set_operator/active_record"
4
+ require "delta/set_operator"
5
+ require "delta/plucker"
6
+ require "delta/identifier"
metadata ADDED
@@ -0,0 +1,135 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: delta
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Chris Patuzzo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-05-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '10.4'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '10.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.2'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.31'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.31'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.10'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.10'
69
+ - !ruby/object:Gem::Dependency
70
+ name: activerecord
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '4.2'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '4.2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sqlite3
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.3'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.3'
97
+ description: Calculates the delta between two collections of objects.
98
+ email: chris@patuzzo.co.uk
99
+ executables: []
100
+ extensions: []
101
+ extra_rdoc_files: []
102
+ files:
103
+ - README.md
104
+ - lib/delta.rb
105
+ - lib/delta/base.rb
106
+ - lib/delta/identifier.rb
107
+ - lib/delta/plucker.rb
108
+ - lib/delta/set_operator.rb
109
+ - lib/delta/set_operator/active_record.rb
110
+ - lib/delta/set_operator/enumerable.rb
111
+ homepage: https://github.com/tuzz/delta
112
+ licenses:
113
+ - MIT
114
+ metadata: {}
115
+ post_install_message:
116
+ rdoc_options: []
117
+ require_paths:
118
+ - lib
119
+ required_ruby_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ required_rubygems_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ requirements: []
130
+ rubyforge_project:
131
+ rubygems_version: 2.4.5
132
+ signing_key:
133
+ specification_version: 4
134
+ summary: Delta
135
+ test_files: []