quickening 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.rdoc +54 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +145 -0
  5. data/Rakefile +36 -0
  6. data/app/assets/javascripts/quickening/application.js +15 -0
  7. data/app/assets/stylesheets/quickening/application.css +13 -0
  8. data/app/controllers/quickening/application_controller.rb +4 -0
  9. data/app/helpers/quickening/application_helper.rb +4 -0
  10. data/app/views/layouts/quickening/application.html.erb +14 -0
  11. data/config/routes.rb +2 -0
  12. data/lib/quickening/engine.rb +16 -0
  13. data/lib/quickening/model.rb +178 -0
  14. data/lib/quickening/orm/active_record.rb +36 -0
  15. data/lib/quickening/version.rb +5 -0
  16. data/lib/quickening.rb +17 -0
  17. data/lib/tasks/quickening_tasks.rake +8 -0
  18. data/spec/dummy/README.rdoc +261 -0
  19. data/spec/dummy/Rakefile +7 -0
  20. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  21. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  22. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  23. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  24. data/spec/dummy/app/models/admin.rb +3 -0
  25. data/spec/dummy/app/models/user.rb +6 -0
  26. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  27. data/spec/dummy/config/application.rb +65 -0
  28. data/spec/dummy/config/boot.rb +10 -0
  29. data/spec/dummy/config/database.yml +25 -0
  30. data/spec/dummy/config/environment.rb +5 -0
  31. data/spec/dummy/config/environments/development.rb +37 -0
  32. data/spec/dummy/config/environments/production.rb +67 -0
  33. data/spec/dummy/config/environments/test.rb +38 -0
  34. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  35. data/spec/dummy/config/initializers/inflections.rb +15 -0
  36. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  37. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  38. data/spec/dummy/config/initializers/session_store.rb +8 -0
  39. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  40. data/spec/dummy/config/locales/en.yml +5 -0
  41. data/spec/dummy/config/routes.rb +4 -0
  42. data/spec/dummy/config.ru +4 -0
  43. data/spec/dummy/db/development.sqlite3 +0 -0
  44. data/spec/dummy/db/migrate/20130302150624_create_users.rb +12 -0
  45. data/spec/dummy/db/migrate/20130302153106_create_admins.rb +12 -0
  46. data/spec/dummy/db/schema.rb +36 -0
  47. data/spec/dummy/db/test.sqlite3 +0 -0
  48. data/spec/dummy/log/development.log +404 -0
  49. data/spec/dummy/log/test.log +67550 -0
  50. data/spec/dummy/public/404.html +26 -0
  51. data/spec/dummy/public/422.html +26 -0
  52. data/spec/dummy/public/500.html +25 -0
  53. data/spec/dummy/public/favicon.ico +0 -0
  54. data/spec/dummy/script/rails +6 -0
  55. data/spec/factories/admin_factory.rb +13 -0
  56. data/spec/factories/user_factory.rb +13 -0
  57. data/spec/quickening_spec.rb +309 -0
  58. data/spec/spec_helper.rb +50 -0
  59. data/spec/support/shared_examples.rb +156 -0
  60. 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,4 @@
1
+ module Quickening
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Quickening
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -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,2 @@
1
+ Quickening::Engine.routes.draw do
2
+ end
@@ -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
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+
3
+ module Quickening # :nodoc:
4
+ VERSION = "0.1.1"
5
+ end
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
@@ -0,0 +1,8 @@
1
+ # encoding: utf-8
2
+
3
+ namespace :quickening do
4
+ # desc "Explaining what the task does"
5
+ # task :quickening do
6
+ # # Task goes here
7
+ # end
8
+ end