delta 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []