Skip to content
This repository has been archived by the owner on Oct 17, 2023. It is now read-only.

Commit

Permalink
add find_duplicates method to Rails, remove dedupe method from Rails,…
Browse files Browse the repository at this point in the history
… add minitest assertions `assert_changed` and `assert_not_changed`
  • Loading branch information
westonganger committed Aug 29, 2016
1 parent 16f4878 commit 2c5d288
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 47 deletions.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
source 'https://rubygems.org'
gemspec

gem 'minitest'

group :rails do
gem 'rails', '>= 3.2.0'
end
34 changes: 26 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ Rearmed.enabled_patches = {
pluck_to_hash: false,
pluck_to_struct: false,
find_or_create: false,
find_duplicates: false,
reset_table: false,
reset_auto_increment: false,
dedupe: false,
find_relation_each: false,
find_in_relation_batches: false,
},
Expand Down Expand Up @@ -66,6 +66,10 @@ Rearmed.enabled_patches = {
},
date: {
now: false
},
minitest: {
assert_changed: false,
assert_not_changed: false
}
}

Expand Down Expand Up @@ -157,6 +161,15 @@ Post.pluck_to_struct(:name, :category, :id)
Post.find_or_create(name: 'foo', content: 'bar') # use this instead of the super confusing first_or_create method
Post.find_or_create!(name: 'foo', content: 'bar')

Post.find_duplicates # return active record relation of all records that have duplicates. By default it skips the primary_key, created_at, updated_at, & deleted_at columns
Post.find_duplicates(:name) # find duplicates based on the name attribute
Post.find_duplicates(:name, :category) # find duplicates based on the name & category attribute
Post.find_duplicates(self.column_names.reject{|x| ['id','created_at','updated_at','deleted_at'].include?(x)})

# It also can delete duplicates. Valid values for keep are :first & :last. Valid values for delete_method are :destroy & :delete. soft_delete is only used if you are using acts_as_paranoid on your model.
Post.find_duplicates(:name, :category, delete: true)
Post.find_duplicates(:name, :category, delete: {keep: :first, delete_method: :destroy, soft_delete: true}) # these are the default settings for delete: true

Post.reset_table # delete all records from table and reset autoincrement column (id), works with mysql/mariadb/postgresql/sqlite
# or with options
Post.reset_table(delete_method: :destroy) # to ensure all callbacks are fired
Expand All @@ -165,13 +178,6 @@ Post.reset_auto_increment # reset mysql/mariadb/postgresql/sqlite auto-increment
# or with options
Post.reset_auto_increment(value: 1, column: :id) # column option is only relevant for postgresql

Post.dedupe # remove all duplicate records, defaults to all of the models column_names except timestamps
# or with options
Post.dedupe(delete_method: :destroy) # to ensure all callbacks are fired
Post.dedupe(columns: [:name, :content, :category_id]
Post.dedupe(skip_timestamps: false) # skip timestamps defaults to true (created_at, updated_at, deleted_at)
Post.dedupe(keep: :last) # Keep the last duplicate instead of the first duplicate by default

Post.find_in_relation_batches # this returns a relation instead of an array
Post.find_relation_each # this returns a relation instead of an array
```
Expand All @@ -197,13 +203,25 @@ my_hash.compact # See Hash methods above
my_hash.compact!
```
### Minitest Method
```ruby
assert_changed -> { user.name } do
user.update(user_params)
end
assert_not_changed lambda{ user.name } do
user.update(user_params)
end
```
# Contributing / Todo
If you want to request a method please raise an issue and we can discuss the implementation.
If you want to contribute here are a couple of things you could do:
- Add Tests for Rails methods
- Get the `natural_sort` method to accept a block
- Get the `assert_changed` and `assert_not_changed` method to accept a String with the variable name similar to Rails `assert_difference`
# Credits
Expand Down
4 changes: 4 additions & 0 deletions lib/generators/rearmed/setup_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ def setup
},
date: {
now: false
},
minitest: {
assert_changed: false,
assert_not_changed: false
}
}
Expand Down
32 changes: 24 additions & 8 deletions lib/rearmed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,40 @@ module Rearmed
@enabled_patches = {
rails_4: {
or: false,
link_to_confirm: false,
find_relation_each: false,
find_in_relation_batches: false
link_to_confirm: false
},
rails_3: {
hash_compact: false,
pluck: false,
update_columns: false,
all: false
},
rails: {
pluck_to_hash: false,
pluck_to_struct: false,
find_or_create: false,
find_duplicates: false,
reset_table: false,
reset_auto_increment: false,
find_relation_each: false,
find_in_relation_batches: false,
},
string: {
to_bool: false,
valid_integer: false,
valid_float: false
valid_float: false,
to_bool: false,
starts_with: false,
begins_with: false,
ends_with: false
},
hash: {
only: false,
dig: false
dig: false,
compact: false
},
array: {
dig: false,
delete_first: false
delete_first: false,
not_empty: false
},
enumerable: {
natural_sort: false,
Expand All @@ -36,6 +48,10 @@ module Rearmed
},
date: {
now: false
},
minitest: {
assert_changed: false,
assert_not_changed: false
}
}

Expand Down
1 change: 1 addition & 0 deletions lib/rearmed/apply_patches.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
require 'rearmed/monkey_patches/rails_3'
require 'rearmed/monkey_patches/rails_4'
require 'rearmed/monkey_patches/date'
require 'rearmed/monkey_patches/minitest'
32 changes: 32 additions & 0 deletions lib/rearmed/monkey_patches/minitest.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
if defined?(Minitest::Assertions)

enabled = Rearmed.enabled_patches[:minitest] == true

Minitest::Assertions.module_eval do

if enabled || Rearmed.dig(Rearmed.enabled_patches, :minitest, :assert_changed)
def assert_changed(expression, &block)
#unless expression.respond_to?(:call)
# expression = lambda{ eval(expression, block.binding) }
# expression = lambda{ block.binding.eval("#{expression}") }
#end
old = expression.call
block.call
refute_equal old, expression.call
end
end

if enabled || Rearmed.dig(Rearmed.enabled_patches, :minitest, :assert_not_changed)
def assert_not_changed(expression, &block)
unless expression.respond_to?(:call)
expression = lambda{ eval(expression, block.binding) }
end
old = expression.call
block.call
assert_equal old, expression.call
end
end

end

end
63 changes: 46 additions & 17 deletions lib/rearmed/monkey_patches/rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,41 +46,70 @@ def self.reset_auto_increment(opts={})
self.connection.execute("ALTER TABLE #{self.table_name} AUTO_INCREMENT = #{opts[:value]}")
when :postgresql
opts[:value] = 1 if opts[:value].blank?
self.connection.execute("ALTER SEQUENCE #{self.table_name}_#{opts[:column].to_s || 'id'}_seq RESTART WITH #{opts[:value]}")
self.connection.execute("ALTER SEQUENCE #{self.table_name}_#{opts[:column].to_s || self.primary_key}_seq RESTART WITH #{opts[:value]}")
when :sqlite3
opts[:value] = 0 if opts[:value].blank?
self.connection.execute("UPDATE SQLITE_SEQUENCE SET SEQ=#{opts[:value]} WHERE NAME='#{self.table_name}'")
end
end
end

if enabled || Rearmed.dig(Rearmed.enabled_patches, :rails, :dedupe)
def self.dedupe(opts={})
if !opts[:columns]
opts[:columns] = self.column_names.reject{|x| x == 'id'}
if enabled || Rearmed.dig(Rearmed.enabled_patches, :rails, :find_duplicates)
def self.find_duplicates(*args)
options = {}

unless args.empty?
if args.last.is_a?(Hash)
options = args.pop
end

unless args.empty?
if args.count == 1 && args.first.is_a?(Array)
args = args.first
end

options[:columns] = args
end
end

if opts[:skip_timestamps] != false
opts[:columns].reject!{|x| ['created_at','updated_at','deleted_at'].include?(x)}
options[:columns] ||= self.column_names.reject{|x| [self.primary_key, :created_at, :updated_at, :deleted_at].include?(x)}

if options[:delete]
if options[:delete] == true
options[:delete] = {keep: :first, delete_method: :destroy, soft_delete: true}
else
options[:delete][:delete_method] ||= :destroy
options[:delete][:keep] ||= :first
if options[:delete][:soft_delete] != false
options[:delete][:soft_delete] = true
end
end

end

self.all.group_by{|model| opts[:columns].map{|x| model[x]}}.values.each do |duplicates|
(opts[:keep] && opts[:keep].to_sym == :last) ? duplicates.pop : duplicates.shift
if opts[:delete_method] && opts[:delete_method].to_sym == :destroy
ids = self.select("#{options[:keep].to_sym == :last ? 'MAX' : 'MIN'}(#{self.primary_key}) as #{self.primary_key}").group(options[:columns]).pluck(self.primary_key)

if options[:delete]
duplicates = self.where.not(self.primary_key => ids)

if options[:delete][:delete_method].to_sym == :delete
if x.respond_to?(:delete_all!)
duplicates.delete_all!
else
duplicates.delete_all
end
else
duplicates.each do |x|
if x.respond_to?(:really_destroy!)
if !options[:soft_delete] && x.respond_to?(:really_destroy!)
x.really_destroy!
else
x.destroy!
end
end
else
if defined?(ActsAsParanoid) && self.try(:paranoid?)
self.unscoped.where(id: duplicates.collect(&:id)).delete_all!
else
self.unscoped.where(id: duplicates.collect(&:id)).delete_all
end
end
return nil
else
return self.where.not(self.primary_key => ids)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/rearmed/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Rearmed
VERSION = "1.1.1"
VERSION = "1.2.0"
end
60 changes: 47 additions & 13 deletions test/tc_rearmed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ def setup
enumerable: true,
rails_3: true,
rails_4: true,
rails: true
rails: true,
minitest: true
}
require 'rearmed/apply_patches'
end
Expand Down Expand Up @@ -174,27 +175,60 @@ def test_object
eql(str.in?('a real string'), false)
end

def test_minitest
#str = 'first'
#assert_changed "str" do
# str = 'second'
#end

str = 'first'
assert_changed ->{ str } do
str = 'second'
end

name = 'first'
assert_changed lambda{ name } do
name = 'second'
end

#name = 'first'
#assert_not_changed 'name' do
# name = 'first'
#end

name = 'first'
assert_not_changed ->{ name } do
name = 'first'
end

name = 'first'
assert_not_changed lambda{ name } do
name = 'first'
end
end

def test_general_rails
# THE MOST IMPORTANT TESTS HERE WOULD BE dedupe, reset_auto_increment, reset_table

#Post.reset_table # delete all records from table and reset autoincrement column (id), works with mysql/mariadb/postgresql/sqlite
# or with options
#Post.reset_table(delete_method: :destroy) # use destroy_all to ensure all callbacks are fired

#Post.reset_auto_increment # reset mysql/mariadb/postgresql/sqlite auto-increment column
# or with options
#Post.reset_auto_increment(value: 1, column: :id)

#Post.dedupe # remove all duplicate records, defaults to the models column_names list
# or with options
#Post.dedupe(columns: [:name, :content, :category_id])
#Post.dedupe(skip_timestamps: true)
#Post.pluck_to_hash(:name, :category, :id)
#Post.pluck_to_struct(:name, :category, :id)

#Post.find_or_create(name: 'foo', content: 'bar') # use this instead of the super confusing first_or_create method
#Post.find_or_create!(name: 'foo', content: 'bar')

#Post.find_duplicates # return active record relation of all records that have duplicates
#Post.find_duplicates(:name) # find duplicates based on the name attribute
#Post.find_duplicates([:name, :category]) # find duplicates based on the name & category attribute
#Post.find_duplicates(name: 'A Specific Name')

#Post.reset_table # delete all records from table and reset autoincrement column (id), works with mysql/mariadb/postgresql/sqlite
## or with options
#Post.reset_table(delete_method: :destroy) # to ensure all callbacks are fired

#Post.reset_auto_increment # reset mysql/mariadb/postgresql/sqlite auto-increment column, if contains records then defaults to starting from next available number
## or with options
#Post.reset_auto_increment(value: 1, column: :id) # column option is only relevant for postgresql

#Post.find_in_relation_batches # this returns a relation instead of an array
#Post.find_relation_each # this returns a relation instead of an array
end
Expand Down

0 comments on commit 2c5d288

Please sign in to comment.