table_warnings 0.0.7 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -4,3 +4,4 @@ pkg/*
4
4
  rdoc/*
5
5
  data_miner.log
6
6
  Gemfile.lock
7
+ .DS_Store
data/CHANGELOG ADDED
@@ -0,0 +1,14 @@
1
+ 1.0.0 / 2012-06-07
2
+
3
+ * Breaking changes
4
+
5
+ * Renamed plain warn -> warn_if to avoid conflict with Kernel.warn
6
+ * Got rid of _in and _is suffixes, so warn_if_blanks_in -> warn_if_blanks
7
+
8
+ * Enhancements
9
+
10
+ * Proper unit tests
11
+ * Added warn_if_nulls_except
12
+ * Added warn_if_nonexistent_owner[_except]
13
+ * Added warn_unless_range
14
+ * Got rid of autoload
data/Gemfile CHANGED
@@ -1,4 +1,3 @@
1
- source "http://rubygems.org"
1
+ source :rubygems
2
2
 
3
- # Specify your gem's dependencies in table_warnings.gemspec
4
3
  gemspec
data/README.rdoc CHANGED
@@ -5,9 +5,9 @@ NOTE: only for activerecord right now because it uses <tt>count(:conditions => [
5
5
  ==How to define warning signs
6
6
 
7
7
  class AutomobileMake < ActiveRecord::Base
8
- warn_if_blanks_in :name
9
- warn_if_blanks_in :fuel_efficiency
10
- warn_unless_size_is :hundreds
8
+ warn_if_blanks :name
9
+ warn_if_blanks :fuel_efficiency
10
+ warn_unless_size :hundreds
11
11
  end
12
12
 
13
13
  ==How to see warnings for the table
data/Rakefile CHANGED
@@ -1,5 +1,5 @@
1
- require 'bundler'
2
- Bundler::GemHelper.install_tasks
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
3
 
4
4
  require 'rake'
5
5
  require 'rake/testtask'
@@ -11,12 +11,7 @@ end
11
11
 
12
12
  task :default => :test
13
13
 
14
- require 'rake/rdoctask'
15
- Rake::RDocTask.new do |rdoc|
16
- version = TableWarnings::VERSION
17
-
18
- rdoc.rdoc_dir = 'rdoc'
19
- rdoc.title = "table_warnings #{version}"
20
- rdoc.rdoc_files.include('README*')
21
- rdoc.rdoc_files.include('lib/**/*.rb')
14
+ require 'yard'
15
+ YARD::Rake::YardocTask.new do |y|
16
+ y.options << '--no-private'
22
17
  end
@@ -0,0 +1,17 @@
1
+ module TableWarnings
2
+ class Arbitrary
3
+ attr_reader :table
4
+ attr_reader :blk
5
+
6
+ def initialize(table, blk)
7
+ @table = table
8
+ @blk = blk
9
+ end
10
+
11
+ def messages
12
+ if messages = table.instance_eval(&blk)
13
+ [messages].flatten.select { |message| message.present? }
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ module TableWarnings
2
+ class Blank < Exclusive
3
+ def message(column)
4
+ if column.nulls?(conditions) or (column.string? and column.blanks?(conditions))
5
+ if conditions.empty?
6
+ "There are blanks in the #{column.name.inspect} column."
7
+ else
8
+ "There are blanks with the condition #{conditions.inspect} in the #{column.name.inspect} column."
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,70 @@
1
+ module TableWarnings
2
+ class Column
3
+ attr_reader :table
4
+ attr_reader :name
5
+
6
+ def initialize(table, name)
7
+ @table = table
8
+ @name = name.to_s
9
+ end
10
+
11
+ def nulls?(conditions)
12
+ table.where(conditions).where(name => nil).count > 0
13
+ end
14
+
15
+ def string?
16
+ table.columns_hash[name].try(:type) == :string
17
+ end
18
+
19
+ def blank?(conditions)
20
+ table.where(conditions).where(["LENGTH(TRIM(#{table.quoted_table_name}.#{name})) = 0"]).count > 0
21
+ end
22
+
23
+ def values_outside?(min, max, conditions)
24
+ t = table.arel_table
25
+ range_conditions = if min and max
26
+ t[name].lt(min).or(t[name].gt(max))
27
+ elsif min
28
+ t[name].lt(min)
29
+ elsif max
30
+ t[name].lt(max)
31
+ else
32
+ raise RuntimeError, "Either max or min or both should be defined"
33
+ end
34
+ table.where(conditions).where(range_conditions.and(t[name].not_eq(nil))).count > 0
35
+ end
36
+
37
+ def min
38
+ table.minimum(name)
39
+ end
40
+
41
+ def max
42
+ table.maximum(name)
43
+ end
44
+
45
+ # select zip_codes.* from zip_codes left join egrid_subregions on `egrid_subregions`.`abbreviation` = zip_codes.`egrid_subregion_abbreviation` where `egrid_subregions`.`abbreviation` is null
46
+ # t.project('COUNT(*)').join(a_t, Arel::Nodes::OuterJoin).on(a_t[assoc.association_primary_key].eq(t[assoc.foreign_key])).where(a_t[assoc.klass.primary_key].eq(nil))
47
+ def nonexistent_owners?(conditions)
48
+ relation = table.includes(association.name).where(
49
+ table.arel_table[association.foreign_key].not_eq(nil).and( # not this query's job
50
+ association.klass.arel_table[association.klass.primary_key].eq(nil)) # columns in the right table are set to NULL if they don't exist
51
+ )
52
+ if conditions.empty?
53
+ relation.count > 0
54
+ else
55
+ relation.where(conditions).count > 0
56
+ end
57
+ end
58
+
59
+ def association
60
+ return @association.first if @association.is_a?(Array) # ruby needs an easy way to memoize things that might be false or nil
61
+ @association = table.reflect_on_all_associations(:belongs_to).select do |assoc|
62
+ assoc.foreign_key == name
63
+ end
64
+ if @association.many?
65
+ raise ArgumentError, "More than one association on #{table.name} uses foreign key #{name.inspect}"
66
+ end
67
+ @association.first
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,2 @@
1
+ ENV['TABLE_WARNINGS_DEBUG'] = 'true'
2
+ ENV['TABLE_WARNINGS_STRICT'] = 'true'
@@ -0,0 +1,31 @@
1
+ module TableWarnings
2
+ class Exclusive
3
+ attr_reader :table
4
+ attr_reader :scout
5
+ attr_reader :conditions
6
+
7
+ def initialize(table, matcher, options = {})
8
+ @table = table
9
+ @conditions = options[:conditions] || {}
10
+ @scout = Scout.new table, matcher, options
11
+ end
12
+
13
+ def exclusives(columns)
14
+ columns.select { |column| scout.exclusive? column }
15
+ end
16
+
17
+ def matches(columns)
18
+ columns.select { |column| scout.match? column }
19
+ end
20
+
21
+ def covers(columns)
22
+ columns.select { |column| scout.cover? column }
23
+ end
24
+
25
+ def messages(columns)
26
+ columns.map do |column|
27
+ message column
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ module TableWarnings
2
+ class NonexistentOwner < Exclusive
3
+ def initialize(table, matcher, options = {})
4
+ super
5
+ scout.enable_association_check!
6
+ @allow_null_query = options[:allow_null]
7
+ end
8
+
9
+ def message(column)
10
+ if column.nonexistent_owners?(conditions) or (not allow_null? and column.nulls?(conditions))
11
+ if conditions.empty?
12
+ "Foreign keys#{null_warning} refer to nonexistent values in #{column.name.inspect}"
13
+ else
14
+ "Foreign keys#{null_warning} refer to nonexistent values in #{column.name.inspect} given #{conditions.inspect}"
15
+ end
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def allow_null?
22
+ @allow_null_query
23
+ end
24
+
25
+ def null_warning
26
+ if not allow_null?
27
+ ' are nil and/or'
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,13 @@
1
+ module TableWarnings
2
+ class Null < Exclusive
3
+ def message(column)
4
+ if column.nulls?(conditions)
5
+ if conditions.empty?
6
+ "There are NULLs in the #{column.name.inspect} column."
7
+ else
8
+ "There are NULLs with the conditions #{conditions.inspect} in the #{column.name.inspect} column."
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,32 @@
1
+ module TableWarnings
2
+ class Range < Exclusive
3
+ attr_reader :max
4
+ attr_reader :min
5
+
6
+ def initialize(table, matcher, options = {})
7
+ super
8
+ @min = options[:min]
9
+ @max = options[:max]
10
+ @allow_null_query = options[:allow_null]
11
+ if min and max and min > max
12
+ raise ArgumentError, "Min #{min.inspect} must be less than max #{max.inspect}"
13
+ end
14
+ end
15
+
16
+ def message(column)
17
+ if column.values_outside?(min, max, conditions) or (not allow_null? and column.nulls?(conditions))
18
+ if conditions.empty?
19
+ "Unexpected range for #{column.name.inspect}. Min: #{column.min.inspect} (expected #{min.inspect}) Max: #{column.max.inspect} (expected #{max.inspect})"
20
+ else
21
+ "Unexpected range for #{column.name.inspect} in #{conditions.inspect}. Min: #{column.min.inspect} (expected #{min.inspect}) Max: #{column.max.inspect} (expected #{max.expect})"
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def allow_null?
29
+ @allow_null_query
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,38 @@
1
+ module TableWarnings
2
+ class Registry
3
+ attr_reader :warnings
4
+
5
+ def initialize
6
+ @warnings = {}
7
+ @warnings_mutex = Mutex.new
8
+ end
9
+
10
+ def add_warning(table, warning)
11
+ @warnings_mutex.synchronize do
12
+ warnings[table.to_s] ||= []
13
+ warnings[table.to_s] << warning
14
+ end
15
+ end
16
+
17
+ def warnings_for(table)
18
+ k = table.to_s
19
+ if warnings.has_key?(k)
20
+ warnings[k].dup
21
+ else
22
+ []
23
+ end
24
+ end
25
+
26
+ def exclusive(table)
27
+ warnings_for(table).select do |warning|
28
+ warning.respond_to? :exclusives
29
+ end
30
+ end
31
+
32
+ def nonexclusive(table)
33
+ warnings_for(table).reject do |warning|
34
+ warning.respond_to? :exclusives
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,85 @@
1
+ module TableWarnings
2
+ class Scout
3
+ attr_reader :table
4
+ attr_reader :matcher
5
+ attr_reader :conditions
6
+
7
+ def initialize(table, matcher, options = {})
8
+ @table = table
9
+ @matcher = matcher
10
+ @positive_query = (options[:negative] != true)
11
+ @conditions_query = case options[:conditions]
12
+ when String
13
+ true
14
+ when Hash, Array
15
+ !options[:conditions].empty?
16
+ else
17
+ false
18
+ end
19
+ end
20
+
21
+ def exclusive?(column)
22
+ match?(column) and unambiguous?
23
+ end
24
+
25
+ def match?(column)
26
+ if association_check? and not column.association
27
+ return false
28
+ end
29
+ if positive?
30
+ cover? column
31
+ else
32
+ not cover?(column)
33
+ end
34
+ end
35
+
36
+ def cover?(column)
37
+ column_name = column.name
38
+ if association_check?
39
+ associations.any? do |a|
40
+ a.foreign_key == column_name
41
+ end
42
+ else
43
+ case matcher
44
+ when Regexp
45
+ !!(column_name =~ matcher)
46
+ else
47
+ column_name.to_s == matcher.to_s
48
+ end
49
+ end
50
+ end
51
+
52
+ def enable_association_check!
53
+ @association_check_query = true
54
+ end
55
+
56
+ private
57
+
58
+ def association_check?
59
+ @association_check_query
60
+ end
61
+
62
+ def associations
63
+ @associations ||= case matcher
64
+ when Regexp
65
+ table.reflect_on_all_associations(:belongs_to).select do |a|
66
+ a.foreign_key =~ matcher
67
+ end
68
+ else
69
+ [ table.reflect_on_association(matcher) ]
70
+ end
71
+ end
72
+
73
+ def positive?
74
+ @positive_query
75
+ end
76
+
77
+ def conditions?
78
+ @conditions_query
79
+ end
80
+
81
+ def unambiguous?
82
+ positive? and not matcher.is_a?(Regexp)
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,55 @@
1
+ module TableWarnings
2
+ class Size
3
+ attr_reader :table
4
+ attr_reader :approximate_size
5
+ attr_reader :conditions
6
+
7
+ def initialize(table, approximate_size, options = {})
8
+ @table = table
9
+ @approximate_size = approximate_size
10
+ @conditions = options[:conditions] || {}
11
+ end
12
+
13
+ def messages
14
+ current_count = effective_count
15
+ unless allowed_size.include? current_count
16
+ if conditions.empty?
17
+ "Table is not of expected size (expected: #{allowed_size.to_s}, actual: #{current_count})"
18
+ else
19
+ "Table count with conditions #{conditions.inspect} is not of expected size (expected: #{allowed_size.to_s}, actual: #{current_count})"
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def effective_count
27
+ if conditions.empty?
28
+ table.count
29
+ else
30
+ table.where(conditions).count
31
+ end
32
+ end
33
+
34
+ def allowed_size
35
+ case approximate_size
36
+ when :few
37
+ 1..10
38
+ when :dozens, :tens
39
+ 10..100
40
+ when :hundreds
41
+ 100..1_000
42
+ when :thousands
43
+ 1_000..99_000
44
+ when :hundreds_of_thousands
45
+ 100_000..1_000_000
46
+ when :millions
47
+ 1_000_000..1_000_000_000
48
+ when Range
49
+ approximate_size
50
+ when Numeric
51
+ approximate_size..approximate_size
52
+ end
53
+ end
54
+ end
55
+ end