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 +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
|