acts_as_scrubbable 1.0.2 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a11d2fbbda7f3984de9911cba7b449d3f6c6720b17430f6511cf46407cd8a5c9
4
- data.tar.gz: 69a93697794fc1c7abd9b2acd2f9c068639b04f03391b5cc028492ebdebf44a1
3
+ metadata.gz: 98b03e06478fe5c46d1be95047d60b28b91f3e21699743940e18ae0ee6ac8075
4
+ data.tar.gz: 28e6b08a628f43b121b4419b1571466f4810c9ad619849ce8f1b7bdc7700a658
5
5
  SHA512:
6
- metadata.gz: 4b40343ddb3c0b7f764fd48167d3ce3c0dd2cc91f2221652f515849effd1b6da67040fe376e2543240a0efd391aa91eb0347d9e2780b184bee500ac5cad2a319
7
- data.tar.gz: 18c59e0bb668fe98bbf584297d56fb41b14d2bef2e3f4e9e052b330e012a61b5eb9988ab493233db953959776390e529218bff2e0bc8203033712a48054cad7f
6
+ metadata.gz: 74246e4b9ea4209288e2709cf86a0b35e8d5f4abc1f7548c6a5cac4d628fbfc11c1d5a69c61d2de357748e4b6157a107ad7bc34551c3734f9d1630f62b0986e2
7
+ data.tar.gz: 4c5407c1f0689663ff28562880784b89442f1bce5d0cdb26e9ae0c089d4a0435ac516b692399dd8eb4cf15a115c771545bad2367aa75a8b0e9b727e6dcc43fe6
data/.gitignore CHANGED
@@ -1 +1,3 @@
1
1
  Gemfile.lock
2
+
3
+ .idea/*
data/Gemfile CHANGED
@@ -1,5 +1,3 @@
1
- ruby '2.4.3'
2
-
3
1
  source 'https://rubygems.org'
4
2
 
5
3
  gemspec
data/README.md CHANGED
@@ -97,3 +97,22 @@ ActsAsScrubbable.configure do |c|
97
97
  end
98
98
  end
99
99
  ```
100
+
101
+ ### UPDATE VS UPSERT
102
+
103
+ By default, the scrubbing proces will run independent UPDATE statements for each scrubbed model. This can be time
104
+ consuming if you are scrubbing a large number of records. As an alternative, some
105
+ databases support doing bulk database updates using an upsert using the INSERT command. `activerecord-import`
106
+ gives us easy support for this and as such it is a requirement to using
107
+ this feature. Some details about the implementation can be found here. https://github.com/zdennis/activerecord-import#duplicate-key-update
108
+
109
+ Note that we only support the MySQL implementation at this time.
110
+
111
+ To use UPSERT over UPDATE, it can be enabled by specifying the environment variable `USE_UPSERT='true'` or through configuration.
112
+
113
+ ```ruby
114
+ ActsAsScrubbable.configure do |c|
115
+ c.use_upsert = true
116
+ end
117
+ ```
118
+
@@ -0,0 +1,36 @@
1
+ require 'acts_as_scrubbable/import_processor'
2
+ require 'acts_as_scrubbable/update_processor'
3
+ require 'term/ansicolor'
4
+
5
+ module ActsAsScrubbable
6
+ class ArClassProcessor
7
+
8
+ attr_reader :ar_class, :query_processor
9
+
10
+ def initialize(ar_class)
11
+ @ar_class = ar_class
12
+
13
+ if ActsAsScrubbable.use_upsert
14
+ ActsAsScrubbable.logger.info Term::ANSIColor.white("Using Upsert")
15
+ @query_processor = ImportProcessor.new(ar_class)
16
+ else
17
+ ActsAsScrubbable.logger.info Term::ANSIColor.white("Using Update")
18
+ @query_processor = UpdateProcessor.new(ar_class)
19
+ end
20
+ end
21
+
22
+ def process(num_of_batches)
23
+ ActsAsScrubbable.logger.info Term::ANSIColor.green("Scrubbing #{ar_class} ...")
24
+
25
+ num_of_batches = Integer(ENV.fetch("SCRUB_BATCHES", "256")) if num_of_batches.nil?
26
+ scrubbed_count = ActsAsScrubbable::ParallelTableScrubber.new(ar_class, num_of_batches).each_query do |query|
27
+ query_processor.scrub_query(query)
28
+ end
29
+
30
+ ActsAsScrubbable.logger.info Term::ANSIColor.blue("#{scrubbed_count} #{ar_class} objects scrubbed")
31
+ ActiveRecord::Base.connection.verify!
32
+
33
+ ActsAsScrubbable.logger.info Term::ANSIColor.white("Scrub Complete!")
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,28 @@
1
+ module ActsAsScrubbable
2
+ module BaseProcessor
3
+ attr_reader :ar_class
4
+ private :ar_class
5
+
6
+ def initialize(ar_class)
7
+ @ar_class = ar_class
8
+ end
9
+
10
+ def scrub_query(query = nil)
11
+ scrubbed_count = 0
12
+ ActiveRecord::Base.connection_pool.with_connection do
13
+ if ar_class.respond_to?(:scrubbable_scope)
14
+ relation = ar_class.send(:scrubbable_scope)
15
+ else
16
+ relation = ar_class.all
17
+ end
18
+
19
+ relation.where(query).find_in_batches(batch_size: 1000) do |batch|
20
+ ActiveRecord::Base.transaction do
21
+ scrubbed_count += handle_batch(batch)
22
+ end
23
+ end
24
+ end
25
+ scrubbed_count
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ require 'acts_as_scrubbable/base_processor'
2
+
3
+ module ActsAsScrubbable
4
+ class ImportProcessor
5
+ include BaseProcessor
6
+
7
+ private
8
+ def handle_batch(batch)
9
+ scrubbed_count = 0
10
+ batch.each do |obj|
11
+ _updates = obj.scrubbed_values
12
+ obj.assign_attributes(_updates)
13
+ scrubbed_count += 1
14
+ end
15
+ ar_class.import(
16
+ batch,
17
+ on_duplicate_key_update: ar_class.scrubbable_fields.keys.map { |x| "`#{x}` = values(`#{x}`)" }.join(" , "),
18
+ validate: false,
19
+ timestamps: false
20
+ )
21
+ scrubbed_count
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,72 @@
1
+ require "parallel"
2
+
3
+ module ActsAsScrubbable
4
+ class ParallelTableScrubber
5
+ attr_reader :ar_class, :num_of_batches
6
+ private :ar_class, :num_of_batches
7
+
8
+ def initialize(ar_class, num_of_batches)
9
+ @ar_class = ar_class
10
+ @num_of_batches = num_of_batches
11
+ end
12
+
13
+ def each_query
14
+ # Removing any find or initialize callbacks from model
15
+ ar_class.reset_callbacks(:initialize)
16
+ ar_class.reset_callbacks(:find)
17
+
18
+ Parallel.map(parallel_queries) { |query|
19
+ yield(query)
20
+ }.reduce(:+) # returns the aggregated scrub count
21
+ end
22
+
23
+ private
24
+
25
+ # create even ID ranges for the table
26
+ def parallel_queries
27
+ raise "Model is missing id column" if ar_class.columns.none? { |column| column.name == "id" }
28
+
29
+ if ar_class.respond_to?(:scrubbable_scope)
30
+ num_records = ar_class.send(:scrubbable_scope).count
31
+ else
32
+ num_records = ar_class.count
33
+ end
34
+ return [] if num_records == 0 # no records to import
35
+
36
+ record_window_size, modulus = num_records.divmod(num_of_batches)
37
+ if record_window_size < 1
38
+ record_window_size = 1
39
+ modulus = 0
40
+ end
41
+
42
+ start_id = next_id(ar_class: ar_class, offset: 0)
43
+ queries = num_of_batches.times.each_with_object([]) do |_, queries|
44
+ next unless start_id
45
+
46
+ end_id = next_id(ar_class: ar_class, id: start_id, offset: record_window_size - 1)
47
+ if modulus > 0
48
+ end_id = next_id(ar_class: ar_class, id: end_id)
49
+ modulus -= 1
50
+ end
51
+ queries << { id: start_id..end_id } if end_id
52
+ start_id = next_id(ar_class: ar_class, id: end_id || start_id)
53
+ end
54
+
55
+ # just in case new records are added since we started, extend the end ID
56
+ queries[-1] = ["#{ar_class.quoted_table_name}.id >= ?", queries[-1][:id].begin] if queries.any?
57
+
58
+ queries
59
+ end
60
+
61
+ def next_id(ar_class:, id: nil, offset: 1)
62
+ if ar_class.respond_to?(:scrubbable_scope)
63
+ collection = ar_class.send(:scrubbable_scope)
64
+ else
65
+ collection = ar_class.all
66
+ end
67
+ collection = collection.reorder(:id)
68
+ collection = collection.where("#{ar_class.quoted_table_name}.id >= :id", id: id) if id
69
+ collection.offset(offset).limit(1).pluck(:id).first
70
+ end
71
+ end
72
+ end
@@ -1,7 +1,7 @@
1
1
  module ActsAsScrubbable
2
2
  module Scrub
3
3
 
4
- def scrub!
4
+ def scrubbed_values
5
5
  return unless self.class.scrubbable?
6
6
 
7
7
  run_callbacks(:scrub) do
@@ -20,7 +20,7 @@ module ActsAsScrubbable
20
20
  end
21
21
  end
22
22
 
23
- self.update_columns(_updates) unless _updates.empty?
23
+ _updates
24
24
  end
25
25
  end
26
26
  end
@@ -0,0 +1,66 @@
1
+ require 'acts_as_scrubbable/parallel_table_scrubber'
2
+ require 'highline/import'
3
+ require 'acts_as_scrubbable/ar_class_processor'
4
+ require 'term/ansicolor'
5
+
6
+ module ActsAsScrubbable
7
+ class TaskRunner
8
+ attr_reader :ar_classes
9
+ private :ar_classes
10
+
11
+ def initialize
12
+ @ar_classes = []
13
+ end
14
+
15
+ def prompt_db_configuration
16
+ db_host = ActiveRecord::Base.connection_config[:host]
17
+ db_name = ActiveRecord::Base.connection_config[:database]
18
+
19
+ ActsAsScrubbable.logger.warn Term::ANSIColor.red("Please verify the information below to continue")
20
+ ActsAsScrubbable.logger.warn Term::ANSIColor.red("Host: ") + Term::ANSIColor.white(" #{db_host}")
21
+ ActsAsScrubbable.logger.warn Term::ANSIColor.red("Database: ") + Term::ANSIColor.white("#{db_name}")
22
+ end
23
+
24
+ def confirmed_configuration?
25
+ db_host = ActiveRecord::Base.connection_config[:host]
26
+
27
+ unless ENV["SKIP_CONFIRM"] == "true"
28
+ answer = ask("Type '#{db_host}' to continue. \n".red + '-> '.white)
29
+ unless answer == db_host
30
+ ActsAsScrubbable.logger.error Term::ANSIColor.red("exiting ...")
31
+ return false
32
+ end
33
+ end
34
+ true
35
+ end
36
+
37
+ def extract_ar_classes
38
+ Rails.application.eager_load! # make sure all the classes are loaded
39
+ @ar_classes = ActiveRecord::Base.descendants.select { |d| d.scrubbable? }.sort_by { |d| d.to_s }
40
+
41
+ if ENV["SCRUB_CLASSES"].present?
42
+ class_list = ENV["SCRUB_CLASSES"].split(",")
43
+ class_list = class_list.map { |_class_str| _class_str.constantize }
44
+ @ar_classes = ar_classes & class_list
45
+ end
46
+ end
47
+
48
+ def set_ar_class(ar_class)
49
+ ar_classes << ar_class
50
+ end
51
+
52
+ def scrub(num_of_batches: nil)
53
+ Parallel.each(ar_classes) do |ar_class|
54
+ ActsAsScrubbable::ArClassProcessor.new(ar_class).process(num_of_batches)
55
+ end
56
+ ActiveRecord::Base.connection.verify!
57
+ end
58
+
59
+ def after_hooks
60
+ if ENV["SKIP_AFTERHOOK"].blank?
61
+ ActsAsScrubbable.logger.info Term::ANSIColor.red("Running after hook")
62
+ ActsAsScrubbable.execute_after_hook
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,96 +1,25 @@
1
-
2
1
  require 'rake'
2
+ require 'acts_as_scrubbable/task_runner'
3
3
 
4
4
  namespace :scrub do
5
5
 
6
- desc "scrub all"
6
+ desc "scrub all scrubbable tables"
7
7
  task all: :environment do
8
+ task_runner = ActsAsScrubbable::TaskRunner.new
9
+ task_runner.prompt_db_configuration
10
+ exit unless task_runner.confirmed_configuration?
11
+ task_runner.extract_ar_classes
12
+ task_runner.scrub(num_of_batches: 1)
13
+ task_runner.after_hooks
14
+ end
8
15
 
9
- require 'highline/import'
10
- require 'term/ansicolor'
11
- require 'logger'
12
- require 'parallel'
13
-
14
-
15
- include Term::ANSIColor
16
-
17
- @logger = Logger.new($stdout)
18
- @logger.formatter = proc do |severity, datetime, progname, msg|
19
- "#{datetime}: [#{severity}] - #{msg}\n"
20
- end
21
-
22
- db_host = ActiveRecord::Base.connection_config[:host]
23
- db_name = ActiveRecord::Base.connection_config[:database]
24
-
25
- @logger.warn "Please verify the information below to continue".red
26
- @logger.warn "Host: ".red + " #{db_host}".white
27
- @logger.warn "Database: ".red + "#{db_name}".white
28
-
29
- unless ENV["SKIP_CONFIRM"] == "true"
30
-
31
- answer = ask("Type '#{db_host}' to continue. \n".red + '-> '.white)
32
- unless answer == db_host
33
- @logger.error "exiting ...".red
34
- exit
35
- end
36
- end
37
-
38
- @logger.warn "Scrubbing classes".red
39
-
40
- Rails.application.eager_load! # make sure all the classes are loaded
41
-
42
- @total_scrubbed = 0
43
-
44
- ar_classes = ActiveRecord::Base.descendants.select{|d| d.scrubbable? }.sort_by{|d| d.to_s }
45
-
46
-
47
- # if the ENV variable is set
48
-
49
- unless ENV["SCRUB_CLASSES"].blank?
50
- class_list = ENV["SCRUB_CLASSES"].split(",")
51
- class_list = class_list.map {|_class_str| _class_str.constantize }
52
- ar_classes = ar_classes & class_list
53
- end
54
-
55
- @logger.info "Srubbable Classes: #{ar_classes.join(', ')}".white
56
-
57
- Parallel.each(ar_classes) do |ar_class|
58
-
59
- # Removing any find or initialize callbacks from model
60
- ar_class.reset_callbacks(:initialize)
61
- ar_class.reset_callbacks(:find)
62
-
63
- @logger.info "Scrubbing #{ar_class} ...".green
64
-
65
- scrubbed_count = 0
66
-
67
- ActiveRecord::Base.connection_pool.with_connection do
68
- if ar_class.respond_to?(:scrubbable_scope)
69
- relation = ar_class.send(:scrubbable_scope)
70
- else
71
- relation = ar_class.all
72
- end
73
-
74
- relation.find_in_batches(batch_size: 1000) do |batch|
75
- ActiveRecord::Base.transaction do
76
- batch.each do |obj|
77
- obj.scrub!
78
- scrubbed_count += 1
79
- end
80
- end
81
- end
82
- end
83
-
84
- @logger.info "#{scrubbed_count} #{ar_class} objects scrubbed".blue
85
- end
86
- ActiveRecord::Base.connection.verify!
87
-
88
- if ENV["SKIP_AFTERHOOK"].blank?
89
- @logger.info "Running after hook".red
90
- ActsAsScrubbable.execute_after_hook
91
- end
92
-
93
- @logger.info "Scrub Complete!".white
16
+ desc "Scrub one table"
17
+ task :model, [:ar_class] => :environment do |_, args|
18
+ task_runner = ActsAsScrubbable::TaskRunner.new
19
+ task_runner.prompt_db_configuration
20
+ exit unless task_runner.confirmed_configuration?
21
+ task_runner.set_ar_class(args[:ar_class].constantize)
22
+ task_runner.scrub
94
23
  end
95
24
  end
96
25
 
@@ -0,0 +1,18 @@
1
+ require 'acts_as_scrubbable/base_processor'
2
+
3
+ module ActsAsScrubbable
4
+ class UpdateProcessor
5
+ include BaseProcessor
6
+
7
+ private
8
+ def handle_batch(batch)
9
+ scrubbed_count = 0
10
+ batch.each do |obj|
11
+ _updates = obj.scrubbed_values
12
+ obj.update_columns(_updates) unless _updates.empty?
13
+ scrubbed_count += 1
14
+ end
15
+ scrubbed_count
16
+ end
17
+ end
18
+ end
@@ -1,3 +1,3 @@
1
1
  module ActsAsScrubbable
2
- VERSION = '1.0.2'
2
+ VERSION = '1.2.1'
3
3
  end
@@ -2,59 +2,76 @@ require 'active_record'
2
2
  require 'active_record/version'
3
3
  require 'active_support/core_ext/module'
4
4
  require 'acts_as_scrubbable/tasks'
5
-
5
+ require 'term/ansicolor'
6
+ require 'logger'
6
7
 
7
8
  module ActsAsScrubbable
9
+ extend self
8
10
  extend ActiveSupport::Autoload
11
+ include Term::ANSIColor
9
12
 
10
13
  autoload :Scrubbable
11
14
  autoload :Scrub
12
15
  autoload :VERSION
13
16
 
17
+ attr_accessor :use_upsert
18
+
19
+ class << self
20
+ def configure(&block)
21
+ self.use_upsert = ENV["USE_UPSERT"] == "true"
14
22
 
15
- def self.configure(&block)
16
- yield self
23
+ yield self
24
+ end
17
25
  end
18
26
 
19
- def self.after_hook(&block)
27
+ def after_hook(&block)
20
28
  @after_hook = block
21
29
  end
22
30
 
23
- def self.execute_after_hook
31
+ def execute_after_hook
24
32
  @after_hook.call if @after_hook
25
33
  end
26
34
 
27
- def self.add(key, value)
35
+ def logger
36
+ @logger ||= begin
37
+ loggger = Logger.new($stdout)
38
+ loggger.formatter = proc do |severity, datetime, progname, msg|
39
+ "#{datetime}: [#{severity}] - #{msg}\n"
40
+ end
41
+ loggger
42
+ end
43
+ end
44
+
45
+ def add(key, value)
28
46
  ActsAsScrubbable.scrub_map[key] = value
29
47
  end
30
48
 
31
- def self.scrub_map
49
+ def scrub_map
32
50
  require 'faker'
33
51
 
34
52
  @_scrub_map ||= {
35
- :first_name => -> { Faker::Name.first_name },
36
- :last_name => -> { Faker::Name.last_name },
37
- :middle_name => -> { Faker::Name.name },
38
- :name => -> { Faker::Name.name },
39
- :email => -> { Faker::Internet.email },
40
- :name_title => -> { Faker::Name.title },
41
- :company_name => -> { Faker::Company.name },
42
- :street_address => -> { Faker::Address.street_address },
53
+ :first_name => -> { Faker::Name.first_name },
54
+ :last_name => -> { Faker::Name.last_name },
55
+ :middle_name => -> { Faker::Name.name },
56
+ :name => -> { Faker::Name.name },
57
+ :email => -> { Faker::Internet.email },
58
+ :name_title => -> { defined? Faker::Job ? Faker::Job.title : Faker::Name.title },
59
+ :company_name => -> { Faker::Company.name },
60
+ :street_address => -> { Faker::Address.street_address },
43
61
  :secondary_address => -> { Faker::Address.secondary_address },
44
- :zip_code => -> { Faker::Address.zip_code },
45
- :state_abbr => -> { Faker::Address.state_abbr },
46
- :state => -> { Faker::Address.state },
47
- :city => -> { Faker::Address.city },
48
- :latitude => -> { Faker::Address.latitude },
49
- :longitude => -> { Faker::Address.longitude },
50
- :username => -> { Faker::Internet.user_name },
51
- :boolean => -> { [true, false ].sample },
52
- :school => -> { Faker::University.name }
62
+ :zip_code => -> { Faker::Address.zip_code },
63
+ :state_abbr => -> { Faker::Address.state_abbr },
64
+ :state => -> { Faker::Address.state },
65
+ :city => -> { Faker::Address.city },
66
+ :latitude => -> { Faker::Address.latitude },
67
+ :longitude => -> { Faker::Address.longitude },
68
+ :username => -> { Faker::Internet.user_name },
69
+ :boolean => -> { [true, false].sample },
70
+ :school => -> { Faker::University.name }
53
71
  }
54
72
  end
55
73
  end
56
74
 
57
-
58
75
  ActiveSupport.on_load(:active_record) do
59
76
  extend ActsAsScrubbable::Scrubbable
60
77
  end
data/spec/db/schema.rb CHANGED
@@ -2,8 +2,23 @@ ActiveRecord::Schema.define(version: 20150421224501) do
2
2
 
3
3
  create_table "scrubbable_models", force: true do |t|
4
4
  t.string "first_name"
5
+ t.string "last_name"
6
+ t.string "middle_name"
7
+ t.string "name"
8
+ t.string "email"
9
+ t.string "title"
10
+ t.string "company_name"
5
11
  t.string "address1"
12
+ t.string "address2"
13
+ t.string "zip_code"
14
+ t.string "state"
15
+ t.string "state_short"
16
+ t.string "city"
6
17
  t.string "lat"
18
+ t.string "lon"
19
+ t.string "username"
20
+ t.boolean "active"
21
+ t.string "school"
7
22
  end
8
23
 
9
24
  end
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe ActsAsScrubbable::ArClassProcessor do
4
+ let(:ar_class) { ScrubbableModel }
5
+
6
+ describe "#initialize" do
7
+ subject { described_class.new(ar_class) }
8
+
9
+ context "with upsert enabled" do
10
+ before do
11
+ allow(ActsAsScrubbable).to receive(:use_upsert).and_return(true)
12
+ end
13
+
14
+ it "includes the ImportProcessor module" do
15
+ expect(subject.query_processor).to be_kind_of(ActsAsScrubbable::ImportProcessor)
16
+ end
17
+ end
18
+
19
+ context "without upsert enabled" do
20
+ it "includes the UpdateProcessor module" do
21
+ expect(subject.query_processor).to be_kind_of(ActsAsScrubbable::UpdateProcessor)
22
+ end
23
+ end
24
+ end
25
+
26
+ describe "#process" do
27
+ let(:num_of_batches) { nil }
28
+ let(:query) { nil }
29
+ let(:parallel_table_scrubber_mock) { instance_double("ParallelTableScrubber") }
30
+ let(:update_processor_mock) { instance_double("UpdateProcessor", scrub_query: nil) }
31
+ subject { described_class.new(ar_class) }
32
+
33
+ before do
34
+ allow(ActsAsScrubbable::ParallelTableScrubber).to receive(:new).and_return(parallel_table_scrubber_mock)
35
+ allow(ActsAsScrubbable::UpdateProcessor).to receive(:new).and_return(update_processor_mock)
36
+ allow(parallel_table_scrubber_mock).to receive(:each_query).and_yield(query)
37
+ end
38
+
39
+ it "calls the expected helper classes with the expected batch size" do
40
+ expect(ActiveRecord::Base.connection).to receive(:verify!)
41
+ expect(update_processor_mock).to receive(:scrub_query).with(query)
42
+ subject.process(num_of_batches)
43
+ expect(ActsAsScrubbable::ParallelTableScrubber).to have_received(:new).with(ar_class, 256)
44
+ end
45
+
46
+ context "with an inputted batch size" do
47
+ let(:num_of_batches) { 10 }
48
+
49
+ it "calls ParallelTableScrubber with the passed batch size" do
50
+ subject.process(num_of_batches)
51
+ expect(ActsAsScrubbable::ParallelTableScrubber).to have_received(:new).with(ar_class, num_of_batches)
52
+ end
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe ActsAsScrubbable::ImportProcessor do
4
+ let(:ar_class) { ScrubbableModel }
5
+ let(:model) { ar_class.new }
6
+ subject { described_class.new(ar_class) }
7
+
8
+ before do
9
+ ar_class.extend(ImportSupport)
10
+ end
11
+
12
+ describe "#handle_batch" do
13
+ it "calls import with the correct parameters" do
14
+ expect(model).to receive(:scrubbed_values).and_call_original
15
+ expect(ar_class).to receive(:import).with(
16
+ [model],
17
+ on_duplicate_key_update: "`first_name` = values(`first_name`) , `last_name` = values(`last_name`) , `middle_name` = values(`middle_name`) , `name` = values(`name`) , `email` = values(`email`) , `company_name` = values(`company_name`) , `zip_code` = values(`zip_code`) , `state` = values(`state`) , `city` = values(`city`) , `username` = values(`username`) , `school` = values(`school`) , `title` = values(`title`) , `address1` = values(`address1`) , `address2` = values(`address2`) , `state_short` = values(`state_short`) , `lat` = values(`lat`) , `lon` = values(`lon`) , `active` = values(`active`)",
18
+ validate: false,
19
+ timestamps: false
20
+ )
21
+
22
+ expect(subject.send(:handle_batch, [model])).to eq 1
23
+ end
24
+ end
25
+ end
@@ -5,34 +5,60 @@ RSpec.describe ActsAsScrubbable::Scrub do
5
5
  describe '.scrub' do
6
6
 
7
7
  # update_columns cannot be run on a new record
8
- subject{ ScrubbableModel.new }
8
+ subject { ScrubbableModel.new }
9
9
  before(:each) { subject.save }
10
10
 
11
+ it 'scrubs all columns' do
12
+ subject.attributes = {
13
+ first_name: "Ted",
14
+ last_name: "Lowe",
15
+ middle_name: "Cassidy",
16
+ name: "Miss Vincenzo Smitham",
17
+ email: "trentdibbert@wiza.com",
18
+ title: "Internal Consultant",
19
+ company_name: "Greenfelder, Collier and Lesch",
20
+ address1: "86780 Watsica Flats",
21
+ address2: "Apt. 913",
22
+ zip_code: "49227",
23
+ state: "Ohio",
24
+ state_short: "OH",
25
+ city: "Port Hildegard",
26
+ lat: -79.5855309778974,
27
+ lon: 13.517352691513906,
28
+ username: "oscar.hermann",
29
+ active: false,
30
+ school: "Eastern Lebsack",
31
+ }
32
+ expect {
33
+ subject.scrubbed_values
34
+ }.not_to raise_error
35
+ end
36
+
11
37
  it 'changes the first_name attribute when scrub is run' do
12
38
  subject.first_name = "Ted"
13
39
  allow(Faker::Name).to receive(:first_name).and_return("John")
14
- subject.scrub!
15
- expect(subject.first_name).to eq "John"
40
+ _updates = subject.scrubbed_values
41
+ expect(_updates[:first_name]).to eq "John"
16
42
  end
17
43
 
18
44
  it 'calls street address on faker and updates address1' do
19
45
  subject.address1 = "123 abc"
20
46
  subject.save
21
47
  allow(Faker::Address).to receive(:street_address).and_return("1 Embarcadero")
22
- subject.scrub!
23
- expect(subject.address1).to eq "1 Embarcadero"
48
+ _updates = subject.scrubbed_values
49
+ expect(_updates[:address1]).to eq "1 Embarcadero"
24
50
  end
25
51
 
26
52
  it "doesn't update the field if it's blank" do
27
53
  subject.address1 = nil
28
54
  subject.save
29
55
  allow(Faker::Address).to receive(:street_address).and_return("1 Embarcadero")
30
- subject.scrub!
31
- expect(subject.address1).to be_nil
56
+ _updates = subject.scrubbed_values
57
+ expect(_updates[:address1]).to be_nil
32
58
  end
33
59
 
34
60
  it 'runs scrub callbacks' do
35
- subject.scrub!
61
+ subject.scrubbed_values
36
62
  expect(subject.scrubbing_begun).to be(true)
37
63
  expect(subject.scrubbing_finished).to be(true)
38
64
  end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe ActsAsScrubbable::UpdateProcessor do
4
+ let(:ar_class) { ScrubbableModel }
5
+ let(:model) { ar_class.create(
6
+ first_name: "Ted",
7
+ last_name: "Lowe",
8
+ ) }
9
+ subject { described_class.new(ar_class) }
10
+
11
+ describe "#handle_batch" do
12
+ it "calls update with the updated attributes" do
13
+ expect(model).to receive(:scrubbed_values).and_call_original
14
+ expect(model).to receive(:update_columns).and_call_original
15
+
16
+ expect(subject.send(:handle_batch, [model])).to eq 1
17
+ end
18
+ end
19
+ end
@@ -11,11 +11,32 @@ RSpec.configure do |config|
11
11
  config.include include NullDB::RSpec::NullifiedDatabase
12
12
  end
13
13
 
14
+ module ImportSupport
15
+ def import(*, **); end
16
+ end
17
+
14
18
 
15
19
  class NonScrubbableModel < ActiveRecord::Base; end
16
20
 
17
21
  class ScrubbableModel < ActiveRecord::Base
18
- acts_as_scrubbable :first_name, :address1 => :street_address, :lat => :latitude
22
+ acts_as_scrubbable :first_name,
23
+ :last_name,
24
+ :middle_name,
25
+ :name,
26
+ :email,
27
+ :company_name,
28
+ :zip_code,
29
+ :state,
30
+ :city,
31
+ :username,
32
+ :school,
33
+ :title => :name_title,
34
+ :address1 => :street_address,
35
+ :address2 => :secondary_address,
36
+ :state_short => :state_abbr,
37
+ :lat => :latitude,
38
+ :lon => :longitude,
39
+ :active => :boolean
19
40
  attr_accessor :scrubbing_begun, :scrubbing_finished
20
41
  set_callback :scrub, :before do
21
42
  self.scrubbing_begun = true
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acts_as_scrubbable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samer Masry
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-08-22 00:00:00.000000000 Z
11
+ date: 2021-08-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -226,21 +226,30 @@ files:
226
226
  - README.md
227
227
  - acts_as_scrubbable.gemspec
228
228
  - lib/acts_as_scrubbable.rb
229
+ - lib/acts_as_scrubbable/ar_class_processor.rb
230
+ - lib/acts_as_scrubbable/base_processor.rb
231
+ - lib/acts_as_scrubbable/import_processor.rb
232
+ - lib/acts_as_scrubbable/parallel_table_scrubber.rb
229
233
  - lib/acts_as_scrubbable/scrub.rb
230
234
  - lib/acts_as_scrubbable/scrubbable.rb
235
+ - lib/acts_as_scrubbable/task_runner.rb
231
236
  - lib/acts_as_scrubbable/tasks.rb
237
+ - lib/acts_as_scrubbable/update_processor.rb
232
238
  - lib/acts_as_scrubbable/version.rb
233
239
  - spec/db/database.yml
234
240
  - spec/db/schema.rb
241
+ - spec/lib/acts_as_scrubbable/ar_class_processor_spec.rb
242
+ - spec/lib/acts_as_scrubbable/import_processor_spec.rb
235
243
  - spec/lib/acts_as_scrubbable/scrub_spec.rb
236
244
  - spec/lib/acts_as_scrubbable/scrubbable_spec.rb
245
+ - spec/lib/acts_as_scrubbable/update_processor_spec.rb
237
246
  - spec/spec_helper.rb
238
247
  - spec/support/database.rb
239
248
  homepage: https://github.com/smasry/acts_as_scrubbable
240
249
  licenses:
241
250
  - MIT
242
251
  metadata: {}
243
- post_install_message:
252
+ post_install_message:
244
253
  rdoc_options: []
245
254
  require_paths:
246
255
  - lib
@@ -255,15 +264,17 @@ required_rubygems_version: !ruby/object:Gem::Requirement
255
264
  - !ruby/object:Gem::Version
256
265
  version: '0'
257
266
  requirements: []
258
- rubyforge_project:
259
- rubygems_version: 2.7.6
260
- signing_key:
267
+ rubygems_version: 3.1.4
268
+ signing_key:
261
269
  specification_version: 4
262
270
  summary: Scrubbing data made easy
263
271
  test_files:
264
272
  - spec/db/database.yml
265
273
  - spec/db/schema.rb
274
+ - spec/lib/acts_as_scrubbable/ar_class_processor_spec.rb
275
+ - spec/lib/acts_as_scrubbable/import_processor_spec.rb
266
276
  - spec/lib/acts_as_scrubbable/scrub_spec.rb
267
277
  - spec/lib/acts_as_scrubbable/scrubbable_spec.rb
278
+ - spec/lib/acts_as_scrubbable/update_processor_spec.rb
268
279
  - spec/spec_helper.rb
269
280
  - spec/support/database.rb