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 +1 -0
- data/CHANGELOG +14 -0
- data/Gemfile +1 -2
- data/README.rdoc +3 -3
- data/Rakefile +5 -10
- data/lib/table_warnings/arbitrary.rb +17 -0
- data/lib/table_warnings/blank.rb +13 -0
- data/lib/table_warnings/column.rb +70 -0
- data/lib/table_warnings/debug.rb +2 -0
- data/lib/table_warnings/exclusive.rb +31 -0
- data/lib/table_warnings/nonexistent_owner.rb +31 -0
- data/lib/table_warnings/null.rb +13 -0
- data/lib/table_warnings/range.rb +32 -0
- data/lib/table_warnings/registry.rb +38 -0
- data/lib/table_warnings/scout.rb +85 -0
- data/lib/table_warnings/size.rb +55 -0
- data/lib/table_warnings/version.rb +1 -1
- data/lib/table_warnings.rb +134 -24
- data/table_warnings.gemspec +6 -5
- data/test/helper.rb +94 -16
- data/test/test_combinations.rb +85 -0
- data/test/test_warn_if_nonexistent_owner.rb +60 -0
- data/test/test_warn_if_nonexistent_owner_except.rb +67 -0
- data/test/test_warn_if_nulls_except.rb +120 -0
- data/test/test_warn_if_nulls_in.rb +114 -0
- data/test/test_warn_unless_range.rb +18 -0
- metadata +105 -25
- data/lib/table_warnings/config.rb +0 -11
- data/lib/table_warnings/warning/arbitrary.rb +0 -13
- data/lib/table_warnings/warning/blank.rb +0 -25
- data/lib/table_warnings/warning/size.rb +0 -34
- data/lib/table_warnings/warning.rb +0 -30
- data/test/test_table_warnings.rb +0 -64
data/.gitignore
CHANGED
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
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
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
2
|
-
|
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 '
|
15
|
-
Rake::
|
16
|
-
|
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,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
|