table_warnings 0.0.7 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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