quickening 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.rdoc +54 -0
- data/MIT-LICENSE +20 -0
- data/README.md +145 -0
- data/Rakefile +36 -0
- data/app/assets/javascripts/quickening/application.js +15 -0
- data/app/assets/stylesheets/quickening/application.css +13 -0
- data/app/controllers/quickening/application_controller.rb +4 -0
- data/app/helpers/quickening/application_helper.rb +4 -0
- data/app/views/layouts/quickening/application.html.erb +14 -0
- data/config/routes.rb +2 -0
- data/lib/quickening/engine.rb +16 -0
- data/lib/quickening/model.rb +178 -0
- data/lib/quickening/orm/active_record.rb +36 -0
- data/lib/quickening/version.rb +5 -0
- data/lib/quickening.rb +17 -0
- data/lib/tasks/quickening_tasks.rake +8 -0
- data/spec/dummy/README.rdoc +261 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +15 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/models/admin.rb +3 -0
- data/spec/dummy/app/models/user.rb +6 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config/application.rb +65 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +67 -0
- data/spec/dummy/config/environments/test.rb +38 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +4 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/migrate/20130302150624_create_users.rb +12 -0
- data/spec/dummy/db/migrate/20130302153106_create_admins.rb +12 -0
- data/spec/dummy/db/schema.rb +36 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +404 -0
- data/spec/dummy/log/test.log +67550 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/factories/admin_factory.rb +13 -0
- data/spec/factories/user_factory.rb +13 -0
- data/spec/quickening_spec.rb +309 -0
- data/spec/spec_helper.rb +50 -0
- data/spec/support/shared_examples.rb +156 -0
- metadata +301 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8f984164b807e1a51e7096822c12774f45251ccf
|
4
|
+
data.tar.gz: 58be26b882800907ef800820f0d6a6d003177efc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b434a24e36457c65d4649bbd0ef80a67911d65486508581495e767623e97b90d23efe0b90dbc88eea20a910a323c5fac94b7b2602832fb0dc7f49710513f2ea7
|
7
|
+
data.tar.gz: c51609c7df98bd8772ef0bd3bb8002330518175f151f68e2b005eaea6694a16b529fc69b9ef978de37e75cd5b50f20ed8a8cf6ea3ac2e8a5c111a839365eaf25
|
data/CHANGELOG.rdoc
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
== 0.1.1
|
2
|
+
|
3
|
+
* Just commemorating moving away from a stupid name to a less silly one.
|
4
|
+
|
5
|
+
== 0.1.0
|
6
|
+
|
7
|
+
* Bugfix
|
8
|
+
* Results were looking correct for the total set <tt>User.duplicate(force: true)</tt> and <tt>User.duplicate.copies</tt> but there were no results returned for <tt>User.duplicate.originals</tt>. Re-oriented the direction of the joins to be more straightforward and now the results have been consistent from my tests against data.
|
9
|
+
|
10
|
+
* Todo
|
11
|
+
* Come up with a less silly name?
|
12
|
+
* Update: Changed name of gem
|
13
|
+
|
14
|
+
== 0.0.3
|
15
|
+
|
16
|
+
* Maintenance
|
17
|
+
* RDoc inline documentation in code
|
18
|
+
* Removing various extraneous development dependencies in gemspec
|
19
|
+
* Cleaning out commented-out code
|
20
|
+
* Separating ActiveRecord extension to ORM file
|
21
|
+
* Extracting shared spec behaviors to spec/support
|
22
|
+
* Correctly requiring the spec/support files
|
23
|
+
* UTF-8 encoding designations
|
24
|
+
* Preparing Rake task namespace
|
25
|
+
* Preparing base-module level setups
|
26
|
+
* Corrected mislinked script/rails
|
27
|
+
* Preparing integration with an actual app
|
28
|
+
|
29
|
+
== 0.0.2
|
30
|
+
|
31
|
+
* Features
|
32
|
+
* No longer need to require/include/set things to integrate with your model. Instead you can call the class method <tt>clone_wars(..)</tt>
|
33
|
+
* Setting up Rails Engine hooks into the Rails load process
|
34
|
+
* Significant improvement of SQL logic for determining whether or not records are duplicates
|
35
|
+
* Much safer way of determining subsets of the overarching query
|
36
|
+
|
37
|
+
* Maintenance
|
38
|
+
* Improved spec coverage over core queries
|
39
|
+
* Setup for upcoming usage of simplecov
|
40
|
+
* Some adjustments to spec_helper and Guardfile for Spork (still need to finish defining reload-hooks for Spork-Guard)
|
41
|
+
* Push to GitHub
|
42
|
+
* Integration with FactoryGirl for testing
|
43
|
+
* Dummy app linked for better coverage with an actual app
|
44
|
+
|
45
|
+
* Bugfixes
|
46
|
+
* Typos and minutiae in documentation
|
47
|
+
* Query for selecting just the originals of duplicates was flawed and unreliable
|
48
|
+
|
49
|
+
== 0.0.1
|
50
|
+
|
51
|
+
* Initial release
|
52
|
+
* Set up of the engine structure, hierarchy of folders, etc.
|
53
|
+
* Ironing out gemspec having given up integrating Jeweler.
|
54
|
+
* Integration of RSpec, Guard, Spork, testing frameworks in general.
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2013 caleon
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
# the Quickening
|
2
|
+
|
3
|
+
Quickening is a Rails gem for adding to your model a facility to query and manage duplicate records. It's been written to remain relatively abstract and adaptable to various models, and so the library should be an easy plugin for models you may have set up already (barring name clashes).
|
4
|
+
|
5
|
+
Beyond the abstracted query methods for searching efficiently throughout the table (but only tested against MySQL 5.5, sorry), your models gain access to methods for dispending with duplicates, chores varying in complexity ranging from the trivial deletes to customizable merges (future feature).
|
6
|
+
|
7
|
+
This gem was built against Rails 3.2.12 on Ruby 2.0.0-p0 (although only using things available as of 1.9.3). Rails 3.1 doesn't handle the `uniq` relational
|
8
|
+
query method, but that should not matter. I don't believe `from` is handled in either 3.0 or 3.1, so that might be a showstopper with pre-3.2 Rails setups. Ruby 1.9 syntaxes are prevalent, so this will not be compatible with Ruby 1.8, either.
|
9
|
+
|
10
|
+
But if you see the utility of this sort of gem, please feel free to contribute and help out.
|
11
|
+
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'quickening'
|
19
|
+
# or for edge
|
20
|
+
gem 'quickening', github: 'caleon/quickening'
|
21
|
+
```
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
|
25
|
+
```bash
|
26
|
+
$ bundle
|
27
|
+
```
|
28
|
+
|
29
|
+
Setup your model one way (the old way):
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
require 'quickening'
|
33
|
+
|
34
|
+
class User < ActiveRecord::Base
|
35
|
+
include Quickening::Model
|
36
|
+
self.duplicate_matchers = %w(name code).map(&:to_sym)
|
37
|
+
..
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
...or the other way (new way, using Rails Engines):
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
class User < ActiveRecord::Base
|
45
|
+
quickening :name, :code
|
46
|
+
..
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
And in your migration:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
add_index :users, [:name, :code]
|
54
|
+
```
|
55
|
+
|
56
|
+
The order by which your composite index is defined should match that of your class attribute value.
|
57
|
+
|
58
|
+
## Usage
|
59
|
+
|
60
|
+
### Retrieve all non-unique records
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
> User.duplicate
|
64
|
+
# => []
|
65
|
+
|
66
|
+
> User.duplicate(force: true)
|
67
|
+
# => [#<User id: 1 name: 'Bruce Wayne', code: nil ..>,
|
68
|
+
#<User id: 2 name: 'Bruce Wayne', code: nil ..>,
|
69
|
+
#<User id: 3 name: 'Syrio Forel', code: 50, died_on: "2013-03-01" ..>,
|
70
|
+
#<User id: 4 name: 'syrio forel', code: 50, died_on: nil ..>,
|
71
|
+
#<User id: 5 name: 'Marla Singer', code: 32 ..>,
|
72
|
+
#<User id: 7 name: 'tylerdurden', code: 1 ..>,
|
73
|
+
#<User id: 8 name: 'tyler durden', code: 1 ..>]
|
74
|
+
```
|
75
|
+
|
76
|
+
The conditions of their non-uniqueness is determined by checking the rest of the table using the `User.duplicate_matches` setting (which works with an Array only, with no special provisions yet for Procs or anything). If a record exists which is identical on all those fields, it is a part of the returned value. This means that both the "original" record as well as its (potentially multiple) copies will be included.
|
77
|
+
|
78
|
+
#### Force: true
|
79
|
+
|
80
|
+
Note that because tables requiring these operations could get potentially large, and because this library does not assume that you have properly applied indexes to the columns used for operational queries, the computation of this finder may be too strenuous. To prevent accidental triggers of this method which, on its own, provides less utility than the chained methods described below, it is initially restricted with `limit(0)` which will then be unrestricted when the follow-up methods are called.
|
81
|
+
|
82
|
+
Obviously you can override this yourself with something like `except(:limit)` or by calling another limit. Alternatively, you can pass the option for `force: true` to the method.
|
83
|
+
|
84
|
+
|
85
|
+
### Retrieve just the originals
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
> User.duplicate.originals
|
89
|
+
# => [#<User id: 1 name: 'Bruce Wayne', code: nil ..>,
|
90
|
+
#<User id: 3 name: 'Syrio Forel', code: 50, died_on: "2013-03-01" ..>]
|
91
|
+
```
|
92
|
+
|
93
|
+
So far as this initial version is concerned, the "originality" is determined by returning the record with the lowest ID among the matches. Also, it is not concerned a duplicate-original if it is a unique record to begin with.
|
94
|
+
|
95
|
+
Note that this does not return *the* original, since among many sets of matches, there is no single "original". Thus, to act on one original record out of a particular set of duplicates, you would need to scope down the returned set as follows:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
> User.duplicate.originals.where(name: 'Bruce Wayne').first
|
99
|
+
# => [#<User id: 1 name: 'Bruce Wayne', code: nil ..>]
|
100
|
+
```
|
101
|
+
|
102
|
+
For the sake of clarity, there should later be an aptly-named method for such individual cases.
|
103
|
+
|
104
|
+
|
105
|
+
### Retrieve just the copies
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
> User.duplicate.copies
|
109
|
+
# => [#<User id: 2 name: 'Bruce Wayne', code: nil ..>,
|
110
|
+
#<User id: 4 name: 'syrio forel', code: 50, died_on: nil ..>]
|
111
|
+
```
|
112
|
+
|
113
|
+
This can otherwise be described as the set of all duplicated records without the original records:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
> User.duplicate.copies == User.duplicate(force: true) - User.duplicate.originals
|
117
|
+
# => true
|
118
|
+
```
|
119
|
+
|
120
|
+
## Future
|
121
|
+
|
122
|
+
1. ~~Consider a class method as an alternative to requiring-including-setting.~~
|
123
|
+
2. Lower the version dependency for Rails (indirectly via ActiveRecord/ActiveSupport)
|
124
|
+
3. *Perhaps* rewrite hash syntaxes to allow Ruby 1.8 compatibility...
|
125
|
+
4. Write more utility functions for dealing with duplicates.
|
126
|
+
5. Allow customization of how to determine a record's "originality".
|
127
|
+
6. Create generators for automatically inputting the required lines into a model file as well as a new migration for adding indices to the appropriate columns.
|
128
|
+
7. Setup faux model classes to allow an instance of a returned set to behave in a special way, distinguishing it from normal records.
|
129
|
+
8. Further avoid MySQL-specific code and test against Postgres, SQLite, etc.
|
130
|
+
9. Controller at the engine- or Rack- level for pre-made administrative interface for managing and reporting duplicates.
|
131
|
+
10. Ability to turn on caching of duplicates per model instance.
|
132
|
+
|
133
|
+
## Contributing to the Quickening
|
134
|
+
|
135
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
136
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
137
|
+
* Fork the project.
|
138
|
+
* Start a feature/bugfix branch.
|
139
|
+
* Commit and push until you are happy with your contribution.
|
140
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
141
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
142
|
+
|
143
|
+
## Copyright
|
144
|
+
|
145
|
+
Copyright (c) 2013 caleon. See MIT-LICENSE for further details.
|
data/Rakefile
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'the Quickening'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('CHANGELOG.rdoc', 'README.md')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
APP_RAKEFILE = File.expand_path('../spec/dummy/Rakefile', __FILE__)
|
24
|
+
load 'rails/tasks/engine.rake'
|
25
|
+
|
26
|
+
Bundler::GemHelper.install_tasks
|
27
|
+
|
28
|
+
Dir[File.join(File.dirname(__FILE__), 'tasks/**/*.rake')].each { |f| load f }
|
29
|
+
|
30
|
+
require 'rspec/core'
|
31
|
+
require 'rspec/core/rake_task'
|
32
|
+
|
33
|
+
desc 'Run all specs in spec directory (excluding plugin specs)'
|
34
|
+
RSpec::Core::RakeTask.new(:spec => 'app:db:test:prepare')
|
35
|
+
|
36
|
+
task :default => :spec
|
@@ -0,0 +1,15 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// the compiled file.
|
9
|
+
//
|
10
|
+
// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
|
11
|
+
// GO AFTER THE REQUIRES BELOW.
|
12
|
+
//
|
13
|
+
//= require jquery
|
14
|
+
//= require jquery_ujs
|
15
|
+
//= require_tree .
|
@@ -0,0 +1,13 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the top of the
|
9
|
+
* compiled file, but it's generally better to create a new file per style scope.
|
10
|
+
*
|
11
|
+
*= require_self
|
12
|
+
*= require_tree .
|
13
|
+
*/
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Quickening</title>
|
5
|
+
<%= stylesheet_link_tag "quickening/application", :media => "all" %>
|
6
|
+
<%= javascript_include_tag "quickening/application" %>
|
7
|
+
<%= csrf_meta_tags %>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
|
11
|
+
<%= yield %>
|
12
|
+
|
13
|
+
</body>
|
14
|
+
</html>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Quickening # :nodoc:
|
4
|
+
|
5
|
+
class Engine < ::Rails::Engine # :nodoc:
|
6
|
+
isolate_namespace Quickening
|
7
|
+
|
8
|
+
config.quickening = Quickening
|
9
|
+
|
10
|
+
initializer 'quickening.active_record' do
|
11
|
+
ActiveSupport.on_load :active_record do
|
12
|
+
require 'quickening/orm/active_record'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
|
5
|
+
module Quickening
|
6
|
+
# == Quickening::Model
|
7
|
+
#
|
8
|
+
# Module to include within your ActiveRecord::Base class definitions, either
|
9
|
+
# manually or via the +quickening+ class method.
|
10
|
+
module Model
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
included do
|
14
|
+
# The <tt>limit(0)</tt> default exists now to prevent any inadvertent calls
|
15
|
+
# to what would amount to a very taxing call, for those on an app hooked
|
16
|
+
# to a large database. Besides, there is more utility to be had when
|
17
|
+
# following the scope call with one of the extensions.
|
18
|
+
#
|
19
|
+
#--
|
20
|
+
# I have avoided the practice of using a join table to create a self-
|
21
|
+
# referential association. Barring significant reasons to do so, a simple
|
22
|
+
# alias to itself appears to hold more promise of an elegant solution.
|
23
|
+
# Obviously there is a limit to how far SQL alone can go in terms of
|
24
|
+
# providing us an efficient way to query these things, and those options
|
25
|
+
# will be explored over time.
|
26
|
+
#
|
27
|
+
# In v0.0.1 the ARel methods and objects were directly utilized to avoid
|
28
|
+
# needing to hardcode table alias names. But even if that worked
|
29
|
+
# swimmingly as far as ARel was concerned, the interplay between it and
|
30
|
+
# ActiveRecord rendered these methods ineffective or even broken. Ended
|
31
|
+
# up interpolating code directly into strings, for a later time when a
|
32
|
+
# better solution is pursued.
|
33
|
+
#
|
34
|
+
# Also it's worth considering making +duplicate_matchers+ unwritable even
|
35
|
+
# at the class level once it's been set via +quickening+. The fact that it
|
36
|
+
# would be inserted as a raw string in the midst of a query is undesirable
|
37
|
+
# from a security standpoint.
|
38
|
+
#++
|
39
|
+
#
|
40
|
+
# Note the scope name is not pluralized ("duplicate", not "duplicates"),
|
41
|
+
# and a good way to think of this is to think of the word as an adjective
|
42
|
+
# to deocorate either of the follow-up methods.
|
43
|
+
#
|
44
|
+
# There is a degree of caution required when depending on these scopes and
|
45
|
+
# methods. Note that the <tt>#originals</tt> method performs a grouping
|
46
|
+
# query, and depending on your usage, it may interject its own overriding
|
47
|
+
# SELECT statements or table/column aliases.
|
48
|
+
#
|
49
|
+
# === Examples
|
50
|
+
#
|
51
|
+
# User.duplicate # => []
|
52
|
+
#
|
53
|
+
# User.duplicate(force: true)
|
54
|
+
# => [#<User id: 1 name: 'Bruce Wayne', code: nil ..>,
|
55
|
+
# #<User id: 2 name: 'Bruce Wayne', code: nil ..>,
|
56
|
+
# #<User id: 3 name: 'Syrio Forel', code: 50 died_on: "2013-03-01" ..>,
|
57
|
+
# #<User id: 4 name: 'syrio forel', code: 50 died_on: nil ..>,
|
58
|
+
# #<User id: 5 name: 'Marla Singer', code: 32 ..>,
|
59
|
+
# #<User id: 7 name: 'tylerdurden', code: 1 ..>,
|
60
|
+
# #<User id: 8 name: 'tyler durden', code: 1 ..>]
|
61
|
+
# scope :duplicate, ->(opts = {}) {
|
62
|
+
# select("`#{table_name}`.*").uniq.from("`#{table_name}` a2").
|
63
|
+
# joins("INNER JOIN `#{table_name}` USING (#{duplicate_matchers * ', '})").
|
64
|
+
# where("`#{table_name}`.`id` != `a2`.`id`").
|
65
|
+
# order("`#{table_name}`.`id`").
|
66
|
+
# limit(opts[:force] ? nil : 0)
|
67
|
+
# } do
|
68
|
+
|
69
|
+
scope :duplicate, ->(opts = {}) {
|
70
|
+
select("`#{table_name}`.*").
|
71
|
+
uniq.
|
72
|
+
joins("INNER JOIN `#{table_name}` a2 USING (#{duplicate_matchers * ', '})").
|
73
|
+
where("`#{table_name}`.`id` != `a2`.`id`").
|
74
|
+
order("`#{table_name}`.`id`").
|
75
|
+
limit(opts[:force] ? nil : 0)
|
76
|
+
} do
|
77
|
+
|
78
|
+
|
79
|
+
##
|
80
|
+
# Returns a collection of all originals within each respective set of
|
81
|
+
# duplicates. Make sure that part was clear. A read of the RSpec tests
|
82
|
+
# with the "documentation" formatter may be assistive in clarifying the
|
83
|
+
# intend of these methods.
|
84
|
+
#
|
85
|
+
# User.duplicate.originals
|
86
|
+
# # => [#<User id: 1 name: 'Bruce Wayne', code: nil ..>,
|
87
|
+
# #<User id: 3 name: 'Syrio Forel', code: 50 died_on: "2013-03-01" ..>]
|
88
|
+
def originals
|
89
|
+
except(:limit).group(duplicate_matchers).
|
90
|
+
having("`#{table_name}`.`id` = MIN(`#{table_name}`.`id`)")
|
91
|
+
end
|
92
|
+
|
93
|
+
##
|
94
|
+
# User.duplicate.copies
|
95
|
+
# # => [#<User id: 2 name: 'Bruce Wayne', code: nil ..>,
|
96
|
+
# #<User id: 4 name: 'syrio forel', code: 50 died_on: nil ..>]
|
97
|
+
def copies
|
98
|
+
# except(:limit).where("`a2`.`id` < `#{table_name}`.`id`")
|
99
|
+
except(:limit).where("`#{table_name}`.`id` > `a2`.`id`")
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
module ClassMethods #:nodoc:
|
105
|
+
# Looks for other records in the same table for items matching on all
|
106
|
+
# pre-defined columns. This has little benefit of usage except to act as
|
107
|
+
# a proxy for the instance-level methods, such as <tt>#duplicates</tt>.
|
108
|
+
#
|
109
|
+
# User.find_duplicates_for(user)
|
110
|
+
# # => [#<User id: 2 ..>]
|
111
|
+
def find_duplicates_for(item)
|
112
|
+
where(item._duplicate_conditions).
|
113
|
+
where("`#{table_name}`.`id` != ?", item.id)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Returns a collection of records belonging to the same class/table which
|
118
|
+
# matches on the designated columns.
|
119
|
+
#
|
120
|
+
# <%= render partial: 'user/duplicate', collection: @user.duplicates %>
|
121
|
+
def duplicates
|
122
|
+
self.class.find_duplicates_for(self)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Returns a hash meant to be used as a parameter to the query method
|
126
|
+
# <tt>where(..)</tt>. To clarify the return value, if your model was set up like
|
127
|
+
# this:
|
128
|
+
#
|
129
|
+
# class User < ActiveRecord::Base
|
130
|
+
# quickening :last_name, :ssn
|
131
|
+
# ..
|
132
|
+
# end
|
133
|
+
#
|
134
|
+
# Then when you call a method utilizing this helper, such as
|
135
|
+
# <tt>User#duplicates</tt>:
|
136
|
+
#
|
137
|
+
# @user.last_name # => 'Wayne'
|
138
|
+
# @user.ssn # => '987-65-321'
|
139
|
+
# @user.duplicates # => []
|
140
|
+
#
|
141
|
+
# ...the <tt>where(..)</tt> condition will receive the output of this method
|
142
|
+
# and end up with the following:
|
143
|
+
#
|
144
|
+
# where({ last_name: 'Wayne', ssn: '987-65-321' })
|
145
|
+
#
|
146
|
+
# Since the return of this method is simply injected into the +where+ method,
|
147
|
+
# you could override this method and, theoretically, do something as follows:
|
148
|
+
#
|
149
|
+
# def _duplicate_conditions
|
150
|
+
# ["first_name = ?, middle_name = ?, last_name = ?", *full_name.split]
|
151
|
+
# end
|
152
|
+
#
|
153
|
+
# However, as this breaks the unified definition of which columns are
|
154
|
+
# expected to match, be sure you won't be breaking other aspects of the
|
155
|
+
# integration of this library.
|
156
|
+
#
|
157
|
+
# ==== Alternate override
|
158
|
+
#
|
159
|
+
# If you want to maintain the library's method behavior but extend it a bit,
|
160
|
+
# you might just want to follow the module extension scheme instead:
|
161
|
+
#
|
162
|
+
# class User < ActiveRecord::Base
|
163
|
+
# quickening [..]
|
164
|
+
#
|
165
|
+
# # Custom overrides:
|
166
|
+
# module DuncanMacLeod
|
167
|
+
# def _duplicate_conditions
|
168
|
+
# @temporary_conditions || super
|
169
|
+
# end
|
170
|
+
# end
|
171
|
+
# include DuncanMacLeod
|
172
|
+
# ..
|
173
|
+
# end
|
174
|
+
def _duplicate_conditions
|
175
|
+
Hash[duplicate_matchers.map { |col| [col, send(col)] }]
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Quickening::ORM # :nodoc:
|
4
|
+
|
5
|
+
module ActiveRecord # :nodoc:
|
6
|
+
# In your model file, call +quickening+ method at the class-level, providing
|
7
|
+
# it the columns you want the library to use as the basis for determining
|
8
|
+
# whether or not records are "duplicates."
|
9
|
+
#
|
10
|
+
# In its present form, this comparison/matching is handled in a decidedly
|
11
|
+
# black-and-white manner (although it's likely that many MySQL setups will
|
12
|
+
# forgive case-insentivity).
|
13
|
+
#
|
14
|
+
# class User < ActiveRecord::Base
|
15
|
+
# quickening %w(first_name last_name birthdate).map(&:to_sym)
|
16
|
+
# ..
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# ==== Parameters
|
20
|
+
# * +attr_list+ - a list of symbolized attributes referencing column names
|
21
|
+
#
|
22
|
+
# It is only expecting a list, not an Array which could get flattened. For
|
23
|
+
# now please remember this.
|
24
|
+
def quickening(*attr_list)
|
25
|
+
include Quickening::Model
|
26
|
+
class_attribute :duplicate_matchers, instance_writer: false
|
27
|
+
self.duplicate_matchers = attr_list.map(&:to_sym) # Replace me in your models.
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# :singleton-method: duplicate_matchers
|
32
|
+
# :singleton-method: duplicate_matchers=
|
33
|
+
# :method: duplicate_matchers
|
34
|
+
end
|
35
|
+
|
36
|
+
ActiveRecord::Base.extend Quickening::ORM::ActiveRecord
|
data/lib/quickening.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'quickening/engine'
|
4
|
+
require 'quickening/version'
|
5
|
+
require 'active_support/dependencies'
|
6
|
+
|
7
|
+
##
|
8
|
+
# = the Quickening
|
9
|
+
#
|
10
|
+
# Please refer to the README file for documentation that is more likely to be
|
11
|
+
# up-to-date.
|
12
|
+
module Quickening
|
13
|
+
|
14
|
+
autoload :Model, 'quickening/model'
|
15
|
+
|
16
|
+
# Settings forthcoming
|
17
|
+
end
|