mongoid-collection-snapshot 1.3.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fdda543f7183d461cf4b7068b97c7c9bba6523b9
4
+ data.tar.gz: a83c53f21498ad03d49089382dd9e519a9451d79
5
+ SHA512:
6
+ metadata.gz: de91fdde1b8c8b6457cdcecb4e91eec67f4eb3fddf0f96702092ef3a8df9e4747401571d1475478e1f82e28b33133287df422fce178ac245c238c6474de2be93
7
+ data.tar.gz: c9682df34fa35b1e444f6f10ccbf28a15517ca141c397d689493aca9de664c0c524cebb11c1a58f7a3abafd15fca855ac233fa3c1d538e408c3fc494392558c4
@@ -0,0 +1,54 @@
1
+ # rcov generated
2
+ coverage
3
+
4
+ # rdoc generated
5
+ rdoc
6
+
7
+ # yard generated
8
+ doc
9
+ .yardoc
10
+
11
+ # bundler
12
+ .bundle
13
+
14
+ # bundler
15
+ pkg
16
+
17
+ # Gemfile lock
18
+ Gemfile.lock
19
+
20
+ # rvm
21
+ .rvmrc
22
+
23
+ # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
24
+ #
25
+ # * Create a file at ~/.gitignore
26
+ # * Include files you want ignored
27
+ # * Run: git config --global core.excludesfile ~/.gitignore
28
+ #
29
+ # After doing this, these files will be ignored in all your git projects,
30
+ # saving you from having to 'pollute' every project you touch with them
31
+ #
32
+ # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
33
+ #
34
+ # For MacOS:
35
+ #
36
+ #.DS_Store
37
+
38
+ # For TextMate
39
+ *.tmproj
40
+ tmtags
41
+
42
+ # For emacs:
43
+ *~
44
+ \#*
45
+ .\#*
46
+
47
+ # For vim:
48
+ *.swp
49
+
50
+ # For redcar:
51
+ .redcar
52
+
53
+ # For rubinius:
54
+ *.rbc
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format=documentation
@@ -0,0 +1 @@
1
+ inherit_from: .rubocop_todo.yml
@@ -0,0 +1,72 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2017-01-20 11:17:06 -0500 using RuboCop version 0.47.1.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 4
10
+ # Configuration parameters: Include.
11
+ # Include: **/Gemfile, **/gems.rb
12
+ Bundler/DuplicatedGem:
13
+ Exclude:
14
+ - 'Gemfile'
15
+
16
+ # Offense count: 1
17
+ Lint/IneffectiveAccessModifier:
18
+ Exclude:
19
+ - 'spec/models/custom_connection_snapshot.rb'
20
+
21
+ # Offense count: 1
22
+ # Configuration parameters: ContextCreatingMethods, MethodCreatingMethods.
23
+ Lint/UselessAccessModifier:
24
+ Exclude:
25
+ - 'spec/models/custom_connection_snapshot.rb'
26
+
27
+ # Offense count: 3
28
+ Metrics/AbcSize:
29
+ Max: 34
30
+
31
+ # Offense count: 5
32
+ # Configuration parameters: CountComments, ExcludedMethods.
33
+ Metrics/BlockLength:
34
+ Max: 145
35
+
36
+ # Offense count: 1
37
+ Metrics/CyclomaticComplexity:
38
+ Max: 8
39
+
40
+ # Offense count: 42
41
+ # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
42
+ # URISchemes: http, https
43
+ Metrics/LineLength:
44
+ Max: 192
45
+
46
+ # Offense count: 3
47
+ # Configuration parameters: CountComments.
48
+ Metrics/MethodLength:
49
+ Max: 31
50
+
51
+ # Offense count: 1
52
+ # Configuration parameters: CountComments.
53
+ Metrics/ModuleLength:
54
+ Max: 147
55
+
56
+ # Offense count: 1
57
+ Metrics/PerceivedComplexity:
58
+ Max: 10
59
+
60
+ # Offense count: 2
61
+ Style/Documentation:
62
+ Exclude:
63
+ - 'spec/**/*'
64
+ - 'test/**/*'
65
+ - 'lib/mongoid-collection-snapshot.rb'
66
+
67
+ # Offense count: 1
68
+ # Configuration parameters: ExpectMatchingDefinition, Regex, IgnoreExecutableScripts, AllowedAcronyms.
69
+ # AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS
70
+ Style/FileName:
71
+ Exclude:
72
+ - 'lib/mongoid-collection-snapshot.rb'
@@ -0,0 +1,27 @@
1
+ sudo: false
2
+
3
+ matrix:
4
+ include:
5
+ - rvm: 2.3.1
6
+ env:
7
+ - MONGOID_VERSION=6
8
+ before_script:
9
+ - bundle exec danger
10
+ - rvm: 2.3.1
11
+ env:
12
+ - MONGOID_VERSION=5
13
+ - rvm: 2.2
14
+ env:
15
+ - MONGOID_VERSION=4
16
+ - rvm: 2.1
17
+ env:
18
+ - MONGOID_VERSION=3
19
+
20
+ services: mongodb
21
+
22
+ addons:
23
+ apt:
24
+ sources:
25
+ - mongodb-3.2-precise
26
+ packages:
27
+ - mongodb-org-server
@@ -0,0 +1,35 @@
1
+ ### 1.3.0 (1/20/2017)
2
+
3
+ * [#2](https://github.com/mongoid/mongoid-collection-snapshot/pull/2): Support for Mongoid 6 - [@dblock](https://github.com/dblock).
4
+ * [#1](https://github.com/mongoid/mongoid-collection-snapshot/pull/1): Gem renamed to mongoid-collection-snapshot and forked to the [mongoid org](https://github.com/mongoid) - [@dblock](https://github.com/dblock).
5
+ * [#18](https://github.com/aaw/mongoid_collection_snapshot/pull/18): Fix: classes that include `Mongoid::CollectionSnapshot` can be inherited - [@dblock](https://github.com/dblock).
6
+ * [#3](https://github.com/mongoid/mongoid-collection-snapshot/pull/3): Added Danger, PR linter - [@dblock](https://github.com/dblock).
7
+ * [#4](https://github.com/mongoid/mongoid-collection-snapshot/pull/4): Added RuboCop - [@dblock](https://github.com/dblock).
8
+
9
+ ### 1.2.0
10
+
11
+ * [#14](https://github.com/aaw/mongoid_collection_snapshot/pull/14): Compatibility with Mongoid 5 - [@dblock](https://github.com/dblock).
12
+
13
+ ### 1.1.0
14
+
15
+ * [#11](https://github.com/aaw/mongoid_collection_snapshot/pull/10): Added support for accessing snapshot collection documents via Mongoid::CollectionSnapshot#documents - [@dblock](https://github.com/dblock).
16
+ * [#11](https://github.com/aaw/mongoid_collection_snapshot/pull/11): Upgraded RSpec - [@dblock](https://github.com/dblock).
17
+
18
+ ### 1.0.1
19
+
20
+ * [#8](https://github.com/aaw/mongoid_collection_snapshot/pull/8): Fixed .gemspec for compatibility with Mongoid 4.x - [@dblock](https://github.com/dblock).
21
+
22
+ ### 1.0.0
23
+
24
+ * Expose `snapshot_session` for custom snapshot storage - [@joeyAghion](https://github.com/joeyAghion).
25
+ * Compatibility with Mongoid 4.x - [@dblock](https://github.com/dblock).
26
+
27
+ ### 0.2.0
28
+
29
+ * Added ability to maintain a snapshot of multiple collections atomically - [@aaw](https://github.com/aaw).
30
+ * Added support for [Mongoid 3.0](https://github.com/mongoid/mongoid) - [@dblock](https://github.com/dblock).
31
+ * Relaxed version limitations of [mongoid_slug](https://github.com/digitalplaywright/mongoid-slug) - [@dblock](https://github.com/dblock).
32
+
33
+ ### 0.1.0
34
+
35
+ * Initial public release - [@aaw](https://github.com/aaw).
@@ -0,0 +1 @@
1
+ danger.import_dangerfile(gem: "mongoid-danger")
data/Gemfile ADDED
@@ -0,0 +1,24 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ case version = ENV['MONGOID_VERSION'] || '6.0'
6
+ when /^6/
7
+ gem 'mongoid', '~> 6.0'
8
+ when /^5/
9
+ gem 'mongoid', '~> 5.0'
10
+ when /^4/
11
+ gem 'mongoid', '~> 4.0'
12
+ when /^3/
13
+ gem 'mongoid', '~> 3.1'
14
+ else
15
+ gem 'mongoid', version
16
+ end
17
+
18
+ group :development, :test do
19
+ gem 'mongoid-danger', '~> 0.1.1'
20
+ gem 'rake'
21
+ gem 'rspec', '~> 3.1'
22
+ gem 'rubocop', '0.47.1'
23
+ gem 'timecop'
24
+ end
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011-2017 Art.sy Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,223 @@
1
+ Mongoid Collection Snapshot
2
+ ===========================
3
+
4
+ Easy maintenance of collections of processed data in MongoDB with Mongoid 3, 4, 5 and 6.
5
+
6
+ This is a forked, renamed, maintained and supported version of [mongoid_collection_snapshot](https://github.com/aaw/mongoid_collection_snapshot).
7
+
8
+ [![Gem Version](https://badge.fury.io/rb/mongoid-collection-snapshot.svg)](https://badge.fury.io/rb/mongoid-collection-snapshot)
9
+ [![Build Status](https://travis-ci.org/mongoid/mongoid-collection-snapshot.svg)](https://travis-ci.org/mongoid/mongoid-collection-snapshot)
10
+
11
+ Quick example:
12
+ --------------
13
+
14
+ Suppose that you have a Mongoid model called `Artwork`, stored in a MongoDB collection called `artworks` and the underlying documents look something like:
15
+
16
+ { name: 'Flowers', artist: 'Andy Warhol', price: 3000000 }
17
+
18
+ From time to time, your system runs a map/reduce job to compute the average price of each artist's works, resulting in a collection called `artist_average_price` that contains documents that look like:
19
+
20
+ { _id: { artist: 'Andy Warhol' }, value: { price: 1500000 } }
21
+
22
+ If your system wants to maintain and use this average price data, it has to do so at the level of raw MongoDB operations, since map/reduce result documents don't map well to models in Mongoid.
23
+ Furthermore, even though map/reduce jobs can take some time to run, you probably want the entire `artist_average_price` collection populated atomically from the point of view of your system, since otherwise you don't ever know the state of the data in the collection - you could access it in the middle of a map/reduce and get partial, incorrect results.
24
+
25
+ A mongoid-collection-snapshot solves this problem by providing an atomic view of collections of data like map/reduce results that live outside of Mongoid.
26
+
27
+ In the example above, we'd set up our average artist price collection like:
28
+
29
+ ``` ruby
30
+ class AverageArtistPrice
31
+ include Mongoid::CollectionSnapshot
32
+
33
+ def build
34
+
35
+ map = <<-EOS
36
+ function() {
37
+ emit({ artist_id: this['artist_id']}, { count: 1, sum: this['price'] })
38
+ }
39
+ EOS
40
+
41
+ reduce = <<-EOS
42
+ function(key, values) {
43
+ var sum = 0;
44
+ var count = 0;
45
+ values.forEach(function(value) {
46
+ sum += value['sum'];
47
+ count += value['count'];
48
+ });
49
+ return({ count: count, sum: sum });
50
+ }
51
+ EOS
52
+
53
+ Artwork.map_reduce(map, reduce).out(inline: 1).each do |doc|
54
+ collection_snapshot.insert_one(
55
+ artist_id: doc['_id']['artist_id'],
56
+ count: doc['value']['count'],
57
+ sum: doc['value']['sum']
58
+ )
59
+ end
60
+ end
61
+ end
62
+
63
+ ```
64
+
65
+ Now, if you want to schedule a recomputation, just call `AverageArtistPrice.create`. You can define other methods on collection snapshots.
66
+
67
+ ```ruby
68
+ class AverageArtistPrice
69
+ ...
70
+
71
+ def average_price(artist_name)
72
+ artist = Artist.where(name: artist_name).first
73
+ doc = collection_snapshot.where(artist_id: artist.id).first
74
+ doc['sum'] / doc['count']
75
+ end
76
+ end
77
+ ```
78
+
79
+ The latest snapshot is always available as `AverageArtistPrice.latest`, so you can write code like:
80
+
81
+ ```ruby
82
+ warhol_expected_price = AverageArtistPrice.latest.average_price('Andy Warhol')
83
+ ```
84
+
85
+ And always be sure that you'll never be looking at partial results. The only thing you need to do to hook into mongoid-collection-snapshot is implement the method `build`, which populates the collection snapshot and any indexes you need.
86
+
87
+ By default, mongoid-collection-snapshot maintains the most recent two snapshots computed any given time.
88
+
89
+ ery Snapshot Data with Mongoid
90
+ --------------------------------
91
+
92
+ You can do better than the average price example above and define first-class models for your collection snapshot data, then access them as any other Mongoid collection via collection snapshot's `.documents` method.
93
+
94
+ ```ruby
95
+ class AverageArtistPrice
96
+ document do
97
+ belongs_to :artist, inverse_of: nil
98
+ field :sum, type: Integer
99
+ field :count, type: Integer
100
+ end
101
+
102
+ def average_price(artist_name)
103
+ artist = Artist.where(name: artist_name).first
104
+ doc = documents.where(artist: artist).first
105
+ doc.sum / doc.count
106
+ end
107
+ end
108
+ ```
109
+
110
+ Another example iterates through all latest artist price averages.
111
+
112
+ ```ruby
113
+ AverageArtistPrice.latest.documents.each do |doc|
114
+ puts "#{doc.artist.name}: #{doc.sum / doc.count}"
115
+ end
116
+ ```
117
+
118
+ Multi-collection snapshots
119
+ --------------------------
120
+
121
+ You can maintain multiple collections atomically within the same snapshot by passing unique collection identifiers to `collection_snaphot` when you call it in your build or query methods:
122
+
123
+ ``` ruby
124
+ class ArtistStats
125
+ include Mongoid::CollectionSnapshot
126
+
127
+ def build
128
+ # ...
129
+ # define map/reduce for average and max aggregations
130
+ # ...
131
+ Mongoid.default_session.command('mapreduce' => 'artworks', map: map_avg, reduce: reduce_avg, out: collection_snapshot('average'))
132
+ Mongoid.default_session.command('mapreduce' => 'artworks', map: map_max, reduce: reduce_max, out: collection_snapshot('max'))
133
+ end
134
+
135
+ def average_price(artist)
136
+ doc = collection_snapshot('average').find('_id.artist' => artist).first
137
+ doc['value']['sum'] / doc['value']['count']
138
+ end
139
+
140
+ def max_price(artist)
141
+ doc = collection_snapshot('max').find('_id.artist' => artist).first
142
+ doc['value']['max']
143
+ end
144
+ end
145
+ ```
146
+
147
+ Specify the name of the collection to define first class Mongoid models.
148
+
149
+ ```ruby
150
+ class ArtistStats
151
+ document('average') do
152
+ field :value, type: Hash
153
+ end
154
+
155
+ document('max') do
156
+ field :value, type: Hash
157
+ end
158
+ end
159
+ ```
160
+
161
+ Access these by name.
162
+
163
+ ```ruby
164
+ ArtistStats.latest.documents('average')
165
+ ArtistStats.latest.documents('max')
166
+ ```
167
+
168
+ If fields across multiple collection snapshots are identical, a single default `document` is sufficient.
169
+
170
+ ```ruby
171
+ class ArtistStats
172
+ document do
173
+ field :value, type: Hash
174
+ end
175
+ end
176
+ ```
177
+
178
+ Custom database connections
179
+ ---------------------------
180
+
181
+ Your class can specify a custom database for storage of collection snapshots by overriding the `snapshot_session` instance method. In this example, we memoize the connection at the class level to avoid creating many separate connection instances.
182
+
183
+ ```ruby
184
+ class ArtistStats
185
+ include Mongoid::CollectionSnapshot
186
+
187
+ def build
188
+ # ...
189
+ end
190
+
191
+ def snapshot_session
192
+ self.class.snapshot_session
193
+ end
194
+
195
+ def self.snapshot_session
196
+ @@snapshot_session ||= Mongo::Client.new('mongodb://localhost:27017').tap do |c|
197
+ c.use :alternate_db
198
+ end
199
+ end
200
+ end
201
+ ```
202
+
203
+ Another common way of configuring this is through mongoid.yml.
204
+
205
+ ```yaml
206
+ development:
207
+ sessions:
208
+ default:
209
+ database: dev_data
210
+ imports:
211
+ database: dev_imports
212
+ ```
213
+
214
+ ```ruby
215
+ def snapshot_session
216
+ Mongoid.session('imports')
217
+ end
218
+ ```
219
+
220
+ License
221
+ =======
222
+
223
+ MIT License, see [LICENSE.txt](LICENSE.txt) for details.
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+
5
+ require 'bundler'
6
+ require 'bundler/gem_tasks'
7
+
8
+ begin
9
+ Bundler.setup(:default, :development)
10
+ rescue Bundler::BundlerError => e
11
+ $stderr.puts e.message
12
+ $stderr.puts 'Run `bundle install` to install missing gems'
13
+ exit e.status_code
14
+ end
15
+
16
+ require 'rake'
17
+
18
+ require 'rspec/core'
19
+ require 'rspec/core/rake_task'
20
+ RSpec::Core::RakeTask.new(:spec) do |spec|
21
+ spec.pattern = FileList['spec/**/*_spec.rb']
22
+ end
23
+
24
+ require 'rubocop/rake_task'
25
+ RuboCop::RakeTask.new(:rubocop)
26
+
27
+ task default: [:rubocop, :spec]
@@ -0,0 +1,8 @@
1
+ ### Upgrading from 0.1.0 to >= 0.2.0
2
+
3
+ When upgrading from 0.1.0 (pre-Mongoid 3.0) to 0.2.0 (Mongoid 3.x), you'll need to upgrade any existing snapshots created by mongoid_collection_snapshot 0.1.0 in your database before they're usable. You can do this by renaming the 'workspace_slug' attribute to 'slug' in MongoDB after upgrading. For example, to upgrade the snapshot class "MySnapshot", you'd enter the following at the mongo shell.
4
+
5
+ ```
6
+ db.my_snapshot.rename({ 'workspace_slug' : { '$exists' : true } }, {'$rename' : {'workspace_slug' : 'slug' } })
7
+ ```
8
+
@@ -0,0 +1,100 @@
1
+ require 'mongoid-collection-snapshot/version'
2
+
3
+ module Mongoid
4
+ module CollectionSnapshot
5
+ extend ActiveSupport::Concern
6
+
7
+ DEFAULT_COLLECTION_KEY_NAME = '*'.freeze
8
+
9
+ included do
10
+ require 'mongoid_slug'
11
+
12
+ include Mongoid::Document
13
+ include Mongoid::Timestamps::Created
14
+ include Mongoid::Slug
15
+
16
+ field :workspace_basename, default: 'snapshot'
17
+ slug :workspace_basename
18
+
19
+ field :max_collection_snapshot_instances, default: 2
20
+
21
+ before_create :build
22
+ after_create :ensure_at_most_two_instances_exist
23
+ before_destroy :drop_snapshot_collections
24
+
25
+ class_attribute :document_blocks
26
+ class_attribute :document_classes
27
+
28
+ # Mongoid documents on this snapshot.
29
+ def documents(name = nil)
30
+ self.class.document_classes ||= {}
31
+ class_name = "#{self.class.name}#{id}#{name}".underscore.camelize
32
+ key = "#{class_name}-#{name || DEFAULT_COLLECTION_KEY_NAME}"
33
+ self.class.document_classes[key] ||= begin
34
+ document_block = document_blocks[name || DEFAULT_COLLECTION_KEY_NAME] if document_blocks
35
+ collection_name = collection_snapshot(name).name
36
+ klass = Class.new do
37
+ include Mongoid::Document
38
+ if Mongoid::Compatibility::Version.mongoid5?
39
+ cattr_accessor :mongo_client
40
+ else
41
+ cattr_accessor :mongo_session
42
+ end
43
+ instance_eval(&document_block) if document_block
44
+ store_in collection: collection_name
45
+ end
46
+ if Mongoid::Compatibility::Version.mongoid6?
47
+ PersistenceContext.set(klass, database: snapshot_session.database.name)
48
+ elsif Mongoid::Compatibility::Version.mongoid5?
49
+ klass.mongo_client = snapshot_session
50
+ else
51
+ klass.mongo_session = snapshot_session
52
+ end
53
+ Object.const_set(class_name, klass)
54
+ klass
55
+ end
56
+ end
57
+ end
58
+
59
+ module ClassMethods
60
+ def latest
61
+ order_by([[:created_at, :desc]]).first
62
+ end
63
+
64
+ def document(name = nil, &block)
65
+ self.document_blocks ||= {}
66
+ self.document_blocks[name || DEFAULT_COLLECTION_KEY_NAME] = block
67
+ end
68
+ end
69
+
70
+ def collection_snapshot(name = nil)
71
+ if name
72
+ snapshot_session["#{collection.name}.#{name}.#{slug}"]
73
+ else
74
+ snapshot_session["#{collection.name}.#{slug}"]
75
+ end
76
+ end
77
+
78
+ def drop_snapshot_collections
79
+ collections = Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6? ? snapshot_session.database.collections : snapshot_session.collections
80
+ collections.each do |collection|
81
+ collection.drop if collection.name =~ /^#{self.collection.name}\.([^\.]+\.)?#{slug}$/
82
+ end
83
+ end
84
+
85
+ # Since we should always be using the latest instance of this class, this method is
86
+ # called after each save - making sure only at most two instances exists should be
87
+ # sufficient to ensure that this data can be rebuilt live without corrupting any
88
+ # existing computations that might have a handle to the previous "latest" instance.
89
+ def ensure_at_most_two_instances_exist
90
+ all_instances = self.class.order_by([[:created_at, :desc]]).to_a
91
+ return unless all_instances.length > max_collection_snapshot_instances
92
+ all_instances[max_collection_snapshot_instances..-1].each(&:destroy)
93
+ end
94
+
95
+ # Override to supply custom database connection for snapshots
96
+ def snapshot_session
97
+ Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6? ? Mongoid.default_client : Mongoid.default_session
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,5 @@
1
+ module Mongoid
2
+ module CollectionSnapshot
3
+ VERSION = '1.3.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
2
+ require 'mongoid-collection-snapshot/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'mongoid-collection-snapshot'
6
+ s.version = Mongoid::CollectionSnapshot::VERSION
7
+ s.authors = ['Aaron Windsor']
8
+ s.email = 'aaron.windsor@gmail.com'
9
+ s.platform = Gem::Platform::RUBY
10
+ s.required_rubygems_version = '>= 1.3.6'
11
+ s.files = `git ls-files`.split("\n")
12
+ s.require_paths = ['lib']
13
+ s.homepage = 'http://github.com/mongoid/mongoid-collection-snapshot'
14
+ s.licenses = ['MIT']
15
+ s.summary = 'Easy maintenence of collections of processed data in MongoDB with the Mongoid ODM.'
16
+ s.add_dependency 'mongoid', '>= 3.0'
17
+ s.add_dependency 'mongoid-compatibility'
18
+ s.add_dependency 'mongoid-slug'
19
+ end
@@ -0,0 +1,7 @@
1
+ class Artist
2
+ include Mongoid::Document
3
+
4
+ field :name
5
+
6
+ has_many :artworks
7
+ end
@@ -0,0 +1,9 @@
1
+ class Artwork
2
+ include Mongoid::Document
3
+
4
+ field :name
5
+ field :price
6
+
7
+ belongs_to :artist
8
+ belongs_to :partner
9
+ end
@@ -0,0 +1,107 @@
1
+ class AveragePrice
2
+ include Mongoid::CollectionSnapshot
3
+ end
4
+
5
+ class AverageArtistPrice < AveragePrice
6
+ document do
7
+ belongs_to :artist, inverse_of: nil
8
+ field :sum, type: Integer
9
+ field :count, type: Integer
10
+ end
11
+
12
+ def build
13
+ map = <<-EOS
14
+ function() {
15
+ emit({ artist_id: this['artist_id']}, { count: 1, sum: this['price'] })
16
+ }
17
+ EOS
18
+
19
+ reduce = <<-EOS
20
+ function(key, values) {
21
+ var sum = 0;
22
+ var count = 0;
23
+ values.forEach(function(value) {
24
+ sum += value['sum'];
25
+ count += value['count'];
26
+ });
27
+ return({ count: count, sum: sum });
28
+ }
29
+ EOS
30
+
31
+ Artwork.map_reduce(map, reduce).out(inline: 1).each do |doc|
32
+ if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
33
+ collection_snapshot.insert_one(
34
+ artist_id: doc['_id']['artist_id'],
35
+ count: doc['value']['count'],
36
+ sum: doc['value']['sum']
37
+ )
38
+ else
39
+ collection_snapshot.insert(
40
+ artist_id: doc['_id']['artist_id'],
41
+ count: doc['value']['count'],
42
+ sum: doc['value']['sum']
43
+ )
44
+ end
45
+ end
46
+ end
47
+
48
+ def average_price(artist_name)
49
+ artist = Artist.where(name: artist_name).first
50
+ raise 'missing artist' unless artist
51
+ doc = documents.where(artist: artist).first
52
+ raise 'missing record' unless doc
53
+ doc.sum / doc.count
54
+ end
55
+ end
56
+
57
+ class AveragePartnerPrice < AveragePrice
58
+ document do
59
+ belongs_to :partner, inverse_of: nil
60
+ field :sum, type: Integer
61
+ field :count, type: Integer
62
+ end
63
+
64
+ def build
65
+ map = <<-EOS
66
+ function() {
67
+ emit({ partner_id: this['partner_id']}, { count: 1, sum: this['price'] })
68
+ }
69
+ EOS
70
+
71
+ reduce = <<-EOS
72
+ function(key, values) {
73
+ var sum = 0;
74
+ var count = 0;
75
+ values.forEach(function(value) {
76
+ sum += value['sum'];
77
+ count += value['count'];
78
+ });
79
+ return({ count: count, sum: sum });
80
+ }
81
+ EOS
82
+
83
+ Artwork.map_reduce(map, reduce).out(inline: 1).each do |doc|
84
+ if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
85
+ collection_snapshot.insert_one(
86
+ partner_id: doc['_id']['partner_id'],
87
+ count: doc['value']['count'],
88
+ sum: doc['value']['sum']
89
+ )
90
+ else
91
+ collection_snapshot.insert(
92
+ partner_id: doc['_id']['partner_id'],
93
+ count: doc['value']['count'],
94
+ sum: doc['value']['sum']
95
+ )
96
+ end
97
+ end
98
+ end
99
+
100
+ def average_price(partner_name)
101
+ partner = Partner.where(name: partner_name).first
102
+ raise 'missing partner' unless partner
103
+ doc = documents.where(partner: partner).first
104
+ raise 'missing record' unless doc
105
+ doc.sum / doc.count
106
+ end
107
+ end
@@ -0,0 +1,33 @@
1
+ class CustomConnectionSnapshot
2
+ include Mongoid::CollectionSnapshot
3
+
4
+ def self.snapshot_session
5
+ @snapshot_session ||= new_snapshot_session
6
+ end
7
+
8
+ def snapshot_session
9
+ self.class.snapshot_session
10
+ end
11
+
12
+ def build
13
+ if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
14
+ collection_snapshot.insert_one('name' => 'foo')
15
+ collection_snapshot('foo').insert_one('name' => 'bar')
16
+ else
17
+ collection_snapshot.insert('name' => 'foo')
18
+ collection_snapshot('foo').insert('name' => 'bar')
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def self.new_snapshot_session
25
+ if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
26
+ Mongo::Client.new('mongodb://localhost:27017/snapshot_test')
27
+ else
28
+ Moped::Session.new(['127.0.0.1:27017']).tap do |session|
29
+ session.use :snapshot_test
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,34 @@
1
+ class MultiCollectionSnapshot
2
+ include Mongoid::CollectionSnapshot
3
+
4
+ document('foo') do
5
+ field :name, type: String
6
+ field :count, type: Integer
7
+ end
8
+
9
+ document('bar') do
10
+ field :name, type: String
11
+ field :number, type: Integer
12
+ end
13
+
14
+ document('baz') do
15
+ field :name, type: String
16
+ field :digit, type: Integer
17
+ end
18
+
19
+ def build
20
+ if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
21
+ collection_snapshot('foo').insert_one('name' => 'foo!', count: 1)
22
+ collection_snapshot('bar').insert_one('name' => 'bar!', number: 2)
23
+ collection_snapshot('baz').insert_one('name' => 'baz!', digit: 3)
24
+ else
25
+ collection_snapshot('foo').insert('name' => 'foo!', count: 1)
26
+ collection_snapshot('bar').insert('name' => 'bar!', number: 2)
27
+ collection_snapshot('baz').insert('name' => 'baz!', digit: 3)
28
+ end
29
+ end
30
+
31
+ def names
32
+ %w(foo bar baz).map { |x| collection_snapshot(x).find.first['name'] }.join('')
33
+ end
34
+ end
@@ -0,0 +1,7 @@
1
+ class Partner
2
+ include Mongoid::Document
3
+
4
+ field :name
5
+
6
+ has_many :artworks
7
+ end
@@ -0,0 +1,173 @@
1
+ require 'spec_helper'
2
+
3
+ module Mongoid
4
+ describe CollectionSnapshot do
5
+ it 'has a version' do
6
+ expect(Mongoid::CollectionSnapshot::VERSION).not_to be_nil
7
+ end
8
+
9
+ context 'creating a basic snapshot' do
10
+ let!(:gagosian) { Partner.create!(name: 'Gagosian') }
11
+ let!(:pace) { Partner.create!(name: 'Pace') }
12
+ let!(:andy_warhol) { Artist.create!(name: 'Andy Warhol') }
13
+ let!(:damien_hirst) { Artist.create!(name: 'Damien Hirst') }
14
+ let!(:flowers) { Artwork.create!(name: 'Flowers', partner: gagosian, artist: andy_warhol, price: 3_000_000) }
15
+ let!(:guns) { Artwork.create!(name: 'Guns', partner: pace, artist: andy_warhol, price: 1_000_000) }
16
+ let!(:vinblastine) { Artwork.create!(name: 'Vinblastine', partner: gagosian, artist: damien_hirst, price: 1_500_000) }
17
+
18
+ it 'returns nil if no snapshot has been created' do
19
+ expect(AverageArtistPrice.latest).to be_nil
20
+ expect(AveragePartnerPrice.latest).to be_nil
21
+ end
22
+
23
+ it 'runs the build method on creation for average artist price' do
24
+ snapshot = AverageArtistPrice.create
25
+ expect(snapshot.average_price('Andy Warhol')).to eq(2_000_000)
26
+ expect(snapshot.average_price('Damien Hirst')).to eq(1_500_000)
27
+ end
28
+
29
+ it 'runs the build method on creation for average partner price' do
30
+ snapshot = AveragePartnerPrice.create
31
+ expect(snapshot.average_price('Gagosian')).to eq(2_250_000)
32
+ expect(snapshot.average_price('Pace')).to eq(1_000_000)
33
+ end
34
+
35
+ it 'returns the most recent snapshot through the latest methods' do
36
+ first = AverageArtistPrice.create
37
+ expect(first).to eq(AverageArtistPrice.latest)
38
+ # "latest" only works up to a resolution of 1 second since it relies on Mongoid::Timestamp. But this
39
+ # module is meant to snapshot long-running collection creation, so if you need a resolution of less
40
+ # than a second for "latest" then you're probably using the wrong gem. In tests, sleeping for a second
41
+ # makes sure we get what we expect.
42
+ Timecop.travel(1.second.from_now)
43
+ second = AverageArtistPrice.create
44
+ expect(AverageArtistPrice.latest).to eq(second)
45
+ Timecop.travel(1.second.from_now)
46
+ third = AverageArtistPrice.create
47
+ expect(AverageArtistPrice.latest).to eq(third)
48
+ end
49
+
50
+ it 'maintains at most two of the latest snapshots to support its calculations' do
51
+ AverageArtistPrice.create
52
+ 10.times do
53
+ AverageArtistPrice.create
54
+ expect(AverageArtistPrice.count).to eq(2)
55
+ end
56
+ end
57
+
58
+ context '#documents' do
59
+ it 'provides access to a Mongoid collection' do
60
+ snapshot = AverageArtistPrice.create
61
+ expect(snapshot.documents.count).to eq 2
62
+ document = snapshot.documents.where(artist: andy_warhol).first
63
+ expect(document.artist).to eq andy_warhol
64
+ expect(document.count).to eq 2
65
+ expect(document.sum).to eq 4_000_000
66
+
67
+ snapshot = AveragePartnerPrice.create
68
+ expect(snapshot.documents.count).to eq 2
69
+ document = snapshot.documents.where(partner: pace).first
70
+ expect(document.partner).to eq pace
71
+ expect(document.count).to eq 1
72
+ expect(document.sum).to eq 1_000_000
73
+ end
74
+
75
+ it 'only creates one global class reference' do
76
+ 3.times do
77
+ index = AverageArtistPrice.create
78
+ 2.times { expect(index.documents.count).to eq 2 }
79
+ index = AveragePartnerPrice.create
80
+ 2.times { expect(index.documents.count).to eq 2 }
81
+ end
82
+ expect(AveragePrice.document_classes).to be nil
83
+ expect(AverageArtistPrice.document_classes.count).to be >= 3
84
+ expect(AveragePartnerPrice.document_classes.count).to be >= 3
85
+ end
86
+ end
87
+ end
88
+
89
+ context 'creating a snapshot containing multiple collections' do
90
+ it 'populates several collections and allows them to be queried' do
91
+ expect(MultiCollectionSnapshot.latest).to be_nil
92
+ 10.times { MultiCollectionSnapshot.create }
93
+ expect(MultiCollectionSnapshot.latest.names).to eq('foo!bar!baz!')
94
+ end
95
+
96
+ it 'safely cleans up all collections used by the snapshot' do
97
+ # Create some collections with names close to the snapshots we'll create
98
+ if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
99
+ Mongoid.default_client["#{MultiCollectionSnapshot.collection.name}.do.not_delete"].insert_one('a' => 1)
100
+ Mongoid.default_client["#{MultiCollectionSnapshot.collection.name}.snapshorty"].insert_one('a' => 1)
101
+ Mongoid.default_client["#{MultiCollectionSnapshot.collection.name}.hello.1"].insert_one('a' => 1)
102
+ else
103
+ Mongoid.default_session["#{MultiCollectionSnapshot.collection.name}.do.not_delete"].insert('a' => 1)
104
+ Mongoid.default_session["#{MultiCollectionSnapshot.collection.name}.snapshorty"].insert('a' => 1)
105
+ Mongoid.default_session["#{MultiCollectionSnapshot.collection.name}.hello.1"].insert('a' => 1)
106
+ end
107
+
108
+ MultiCollectionSnapshot.create
109
+ collections = Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6? ? Mongoid.default_client.database.collections : Mongoid.default_session.collections
110
+ before_create = collections.map(&:name)
111
+ expect(before_create.length).to be > 0
112
+
113
+ Timecop.travel(1.second.from_now)
114
+ MultiCollectionSnapshot.create
115
+ collections = Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6? ? Mongoid.default_client.database.collections : Mongoid.default_session.collections
116
+ after_create = collections.map(&:name)
117
+ collections_created = (after_create - before_create).sort
118
+ expect(collections_created.length).to eq(3)
119
+
120
+ MultiCollectionSnapshot.latest.destroy
121
+ collections = Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6? ? Mongoid.default_client.database.collections : Mongoid.default_session.collections
122
+ after_destroy = collections.map(&:name)
123
+ collections_destroyed = (after_create - after_destroy).sort
124
+ expect(collections_created).to eq(collections_destroyed)
125
+ end
126
+ end
127
+
128
+ context 'with a custom snapshot connection' do
129
+ around(:each) do |example|
130
+ if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
131
+ CustomConnectionSnapshot.snapshot_session.database.drop
132
+ else
133
+ CustomConnectionSnapshot.snapshot_session.drop
134
+ end
135
+ example.run
136
+ if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
137
+ CustomConnectionSnapshot.snapshot_session.database.drop
138
+ else
139
+ CustomConnectionSnapshot.snapshot_session.drop
140
+ end
141
+ end
142
+
143
+ it 'builds snapshot in custom database' do
144
+ snapshot = CustomConnectionSnapshot.create
145
+ [
146
+ "#{CustomConnectionSnapshot.collection.name}.foo.#{snapshot.slug}",
147
+ "#{CustomConnectionSnapshot.collection.name}.#{snapshot.slug}"
148
+ ].each do |collection_name|
149
+ session = Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6? ? Mongoid.default_client : Mongoid.default_session
150
+ expect(session[collection_name].find.count).to eq(0)
151
+ expect(CustomConnectionSnapshot.snapshot_session[collection_name].find.count).to eq(1)
152
+ end
153
+ end
154
+
155
+ context '#documents' do
156
+ it 'uses the custom session' do
157
+ if Mongoid::Compatibility::Version.mongoid6?
158
+ expect(CustomConnectionSnapshot.new.documents.database_name).to eq :snapshot_test
159
+ elsif Mongoid::Compatibility::Version.mongoid5?
160
+ expect(CustomConnectionSnapshot.new.documents.mongo_client).to eq CustomConnectionSnapshot.snapshot_session
161
+ else
162
+ expect(CustomConnectionSnapshot.new.documents.mongo_session).to eq CustomConnectionSnapshot.snapshot_session
163
+ end
164
+ end
165
+ it 'provides access to a Mongoid collection' do
166
+ snapshot = CustomConnectionSnapshot.create
167
+ expect(snapshot.collection_snapshot.find.count).to eq 1
168
+ expect(snapshot.documents.count).to eq 1
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,34 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'rspec'
4
+
5
+ require 'mongoid'
6
+ require 'timecop'
7
+
8
+ Mongoid.configure do |config|
9
+ config.connect_to('mongoid-collection-snapshot_test')
10
+ end
11
+
12
+ require File.expand_path('../../lib/mongoid-collection-snapshot', __FILE__)
13
+ Dir["#{File.dirname(__FILE__)}/models/**/*.rb"].each { |f| require f }
14
+
15
+ require 'mongoid-compatibility'
16
+
17
+ RSpec.configure do |c|
18
+ c.before(:all) do
19
+ Mongoid.logger.level = Logger::INFO
20
+ Mongo::Logger.logger.level = Logger::INFO if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
21
+ end
22
+ c.before(:each) do
23
+ Mongoid.purge!
24
+ end
25
+ c.after(:all) do
26
+ if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
27
+ Mongoid.default_client.database.drop
28
+ else
29
+ Mongoid.default_session.drop
30
+ end
31
+ end
32
+ end
33
+
34
+ RSpec.configure(&:raise_errors_for_deprecations!)
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mongoid-collection-snapshot
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Aaron Windsor
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-01-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mongoid
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: mongoid-compatibility
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: mongoid-slug
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description:
56
+ email: aaron.windsor@gmail.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - ".gitignore"
62
+ - ".rspec"
63
+ - ".rubocop.yml"
64
+ - ".rubocop_todo.yml"
65
+ - ".travis.yml"
66
+ - CHANGELOG.md
67
+ - Dangerfile
68
+ - Gemfile
69
+ - LICENSE.txt
70
+ - README.md
71
+ - Rakefile
72
+ - UPGRADING.md
73
+ - lib/mongoid-collection-snapshot.rb
74
+ - lib/mongoid-collection-snapshot/version.rb
75
+ - mongoid-collection-snapshot.gemspec
76
+ - spec/models/artist.rb
77
+ - spec/models/artwork.rb
78
+ - spec/models/average_price.rb
79
+ - spec/models/custom_connection_snapshot.rb
80
+ - spec/models/multi_collection_snapshot.rb
81
+ - spec/models/partner.rb
82
+ - spec/mongoid/collection_snapshot_spec.rb
83
+ - spec/spec_helper.rb
84
+ homepage: http://github.com/mongoid/mongoid-collection-snapshot
85
+ licenses:
86
+ - MIT
87
+ metadata: {}
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: 1.3.6
102
+ requirements: []
103
+ rubyforge_project:
104
+ rubygems_version: 2.5.1
105
+ signing_key:
106
+ specification_version: 4
107
+ summary: Easy maintenence of collections of processed data in MongoDB with the Mongoid
108
+ ODM.
109
+ test_files: []