attribute-stats 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +122 -0
- data/lib/attribute-stats.rb +1 -0
- data/lib/attribute-stats/railtie.rb +9 -0
- data/lib/attribute-stats/tasks/attribute_usage.rake +15 -0
- data/lib/attribute-stats/tasks/dormant.rake +15 -0
- data/lib/attribute-stats/tasks/migration.rake +13 -0
- data/lib/attribute-stats/tasks/unused_attributes.rake +15 -0
- data/lib/attribute-stats/version.rb +3 -0
- data/lib/entities/attribute_info.rb +23 -0
- data/lib/entities/table_data.rb +50 -0
- data/lib/formatters/hash_formatter.rb +49 -0
- data/lib/formatters/json_formatter.rb +20 -0
- data/lib/formatters/tabular_formatter.rb +124 -0
- data/lib/stats_generation/generate_migration.rb +151 -0
- data/lib/stats_generation/set_attribute_stats.rb +93 -0
- data/lib/stats_generation/set_dormant_tables.rb +39 -0
- data/lib/stats_generation/stats_generator.rb +127 -0
- data/lib/stats_generation/terminal.rb +29 -0
- data/rakefile.rb +4 -0
- metadata +176 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 02a8733b9a9fee7f8391d8855ee3d1a9106f1196
|
4
|
+
data.tar.gz: 2b07627c92d58fa5ac459ed1fbb8ab27493d1390
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bcdfba6645eea57014decabf2e8ba5ee935cf6fa3285c545622a59b8e2c13c87512b628824d551b0be8bfc7f95e3fa113043facc720025084371f2a9676bdac0
|
7
|
+
data.tar.gz: f6f439d0aeac841c04846708bdd90a40550afeda98710a1d497f8f5a0ce9968f27a3f304c8ef24e8d15dd39647a9b45a2c51251322bee2459c7290a28ac280cb
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Steve Hodges
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
Attribute Stats gives you insight into which persisted attributes are actually used in your Rails models. Whether you're joining an existing project or have been using it for years, get a quick look at the landscape of the database.
|
2
|
+
|
3
|
+
It helps you find smells in your project:
|
4
|
+
- Attributes which have never had data set (indicates a potentially forgotten attribute)
|
5
|
+
- Tables which haven't been updated for X years (indicates a potentially unused or legacy model)
|
6
|
+
- Attributes used by very few objects in your table (is this being used? Should it be an attribute?)
|
7
|
+
- Attributes which are all set to the default value (if they're all default, is this really being used?)
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
[![Gem Version](https://badge.fury.io/rb/attribute-stats.svg)](https://badge.fury.io/rb/attribute-stats)
|
12
|
+
|
13
|
+
Add `gem 'attribute-stats'` to your Gemfile, bundle, and follow the Usage instructions below.
|
14
|
+
|
15
|
+
View [attribute-stats on RubyGems](https://rubygems.org/gems/attribute-stats) for more info, or just read the [gemspec](attribute-stats.gemspec).
|
16
|
+
|
17
|
+
## Usage
|
18
|
+
|
19
|
+
### Programmatic
|
20
|
+
|
21
|
+
You can use `AttributeStats` from within Rails (or a Rails console):
|
22
|
+
|
23
|
+
irb:1> stats = AttributeStats::StatsGenerator.new()
|
24
|
+
irb:2> stats.attribute_usage
|
25
|
+
=> # a list of your attributes and what % of records have a value set
|
26
|
+
irb:3> stats.unused_attributes
|
27
|
+
=> # a list of your attributes which have no value set in the database
|
28
|
+
irb:4> stats.dormant_tables
|
29
|
+
=> # a list of your tables which have not been updated in awhile
|
30
|
+
irb:5> stats.migration
|
31
|
+
=> # sample migration up/down to remove your unused attributes
|
32
|
+
irb:6> stats.set_formatter :json
|
33
|
+
irb:7> stats.migration # returns json instead of a hash
|
34
|
+
|
35
|
+
### Rake Tasks
|
36
|
+
|
37
|
+
Rake tasks are available once you've installed the gem in your Gemfile and bundled:
|
38
|
+
|
39
|
+
* rake db:stats:dormant_tables
|
40
|
+
* rake db:stats:attribute_usage
|
41
|
+
* rake db:stats:unused_attributes
|
42
|
+
* rake attribute-stats:generate_migration
|
43
|
+
|
44
|
+
Each allows you to change the output to JSON if you'd like to pipe it into another application.
|
45
|
+
|
46
|
+
---
|
47
|
+
|
48
|
+
#### rake db:stats:unused_attributes
|
49
|
+
Lists all attributes which are unused (have a nil or empty value).
|
50
|
+
|
51
|
+
**Argument Options:**
|
52
|
+
1. consider_defaults_unused: true or false (default: false). This option considers attributes set to the databse default value to be unused.
|
53
|
+
2. format: tabular, json (default: tabular)
|
54
|
+
3. verbose: true, false (default: true)
|
55
|
+
|
56
|
+
i.e. `rake db:stats:unused_attributes[true,json,false]`
|
57
|
+
|
58
|
+
---
|
59
|
+
|
60
|
+
#### rake db:stats:attribute_usage
|
61
|
+
Lists usage statistics for all attributes (count and percent which are unused). Note: this does a full scan of your tables, so it will be slow for tables with a lot of data.
|
62
|
+
|
63
|
+
**Argument Options:**
|
64
|
+
1. consider_defaults_unused: true or false (default: false). This option considers attributes set to the databse default value to be unused.
|
65
|
+
2. format: tabular, json (default: tabular)
|
66
|
+
3. verbose: true, false (default: true)
|
67
|
+
|
68
|
+
i.e. `rake db:stats:attribute_usage[true,json,false]`
|
69
|
+
|
70
|
+
---
|
71
|
+
|
72
|
+
#### rake db:stats:dormant_tables
|
73
|
+
Lists tables which have not been updated in the past X months (default is 3.months.ago)
|
74
|
+
|
75
|
+
**Argument Options:**
|
76
|
+
1. date_expression: A Rails date expression, i.e. '3.months.ago'. Tables with updated_at values after that date are not considered dormant.
|
77
|
+
2. format: tabular, json (default: tabular)
|
78
|
+
3. verbose: true, false (default: true)
|
79
|
+
|
80
|
+
i.e. `rake db:stats:dormant_tables['1.year.ago',json,false]`
|
81
|
+
|
82
|
+
---
|
83
|
+
|
84
|
+
#### rake attribute-stats:migration
|
85
|
+
Generates a sample migration syntax to remove all unused attributes. (This is just output, not saved to disk. See the TODO below.)
|
86
|
+
|
87
|
+
**Argument Options:**
|
88
|
+
1. consider_defaults_unused: true or false (default: false). This option considers attributes set to the databse default value to be unused.
|
89
|
+
|
90
|
+
*TODO: actually save that generated file to the db/migrate path of the host Rails app.*
|
91
|
+
|
92
|
+
## Caveats
|
93
|
+
|
94
|
+
The gem does not support:
|
95
|
+
|
96
|
+
1. Detection of unset encrypted attributes. `attribute-stats` works by searching for empty values using SQL. If you are encrypting data before storing it in the database, a value of `nil` might have a value in the database which, decrypted, equals `nil`.
|
97
|
+
1. Custom table names. If you are defining a model called House, and configure the model to use the table 'domiciles' instead of 'houses', `attribute-stats` skips analysis of the table. *(TODO: fix)*
|
98
|
+
1. Tables not associated with a model. If your database has tables which do not correspond with a model (see point #2), `attribute-stats` skips analysis of that table. *(TODO: fix)*
|
99
|
+
|
100
|
+
## Compatability
|
101
|
+
|
102
|
+
The gem is tested and works with Rails version 4.2 through 5.2, with `mysql2`, `sqlite3`, and `postgresql` database adapters. You can view all tested dependency sets in [Appraisals](Appraisals)
|
103
|
+
|
104
|
+
*Due to changes in ActiveRecord between 4.1 and 4.2, the gem breaks in versions below 4.2. In a future version of `attribute-stats`, I intend to add support to previous versions of Rails (at least back to 4.0).*
|
105
|
+
|
106
|
+
## Testing the gem
|
107
|
+
|
108
|
+
To test the gem against the current version of Rails (in [Gemfile.lock](Gemfile.lock)) and sqlite:
|
109
|
+
|
110
|
+
1. `bundle install`
|
111
|
+
2. `bundle exec rspec`
|
112
|
+
|
113
|
+
Or, you can run tests for all supported Rails versions and supported databases:
|
114
|
+
|
115
|
+
1. Add your database config at `spec/database.yml` pointing to empty local mysql and postgres test databases (see [database.yml.sample](spec/database.yml.sample))
|
116
|
+
1. `gem install appraisal`
|
117
|
+
1. `bundle exec appraisal install` *(this Generates gemfiles for all permutations of our dependencies, so you'll see lots of bundler output))*
|
118
|
+
1. `bundle exec appraisal rspec`. *(This runs rspec for each dependency permutation. If one fails, appraisal exits immediately and does not test permutations it hasn't gotten to yet. Tests are not considered passing until all 12 permutations are passing)*
|
119
|
+
|
120
|
+
If you only want to test a certain dependency set, such as Rails 5.2 for MySQL: `bundle exec appraisals rails-5-2-mysql`. In this case, you would *not* need to configure postgresql in your database.yml, nor have postgres running on your machine.
|
121
|
+
|
122
|
+
You can view all available dependency sets in [Appraisals](Appraisals)
|
@@ -0,0 +1 @@
|
|
1
|
+
Dir[File.join(__dir__, '**', '*.rb')].each{|f| require f }
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'attribute-stats'
|
2
|
+
namespace :db do
|
3
|
+
namespace :stats do
|
4
|
+
desc "Usage statistics for all attributes [options CONSIDER_DEFAULTS_UNUSED: false, FORMAT: json, tabular, VERBOSE: false]"
|
5
|
+
task :attribute_usage, [:consider_defaults_unused,:format,:verbose] => :environment do |task, args|
|
6
|
+
args.with_defaults(consider_defaults_unused: 'false', format: 'tabular', verbose: 'true')
|
7
|
+
options = {
|
8
|
+
consider_defaults_unused: args[:consider_defaults_unused].downcase != 'false',
|
9
|
+
formatter: args[:format],
|
10
|
+
verbose: args[:verbose].downcase != 'false',
|
11
|
+
source: :cli }
|
12
|
+
AttributeStats::StatsGenerator.new(options).attribute_usage
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'attribute-stats'
|
2
|
+
namespace :db do
|
3
|
+
namespace :stats do
|
4
|
+
desc "View tables not updated in the past months [options DATE_EXPRESSION: X.months.ago, FORMAT: json, tabular, VERBOSE: false]"
|
5
|
+
task :dormant_tables, [:date_expression,:format,:verbose] => :environment do |task, args|
|
6
|
+
args.with_defaults(date_expression: '3.months.ago', format: 'tabular', verbose: 'true')
|
7
|
+
options = {
|
8
|
+
dormant_table_age: args[:date_expression],
|
9
|
+
formatter: args[:format],
|
10
|
+
verbose: args[:verbose] != 'false',
|
11
|
+
source: :cli }
|
12
|
+
AttributeStats::StatsGenerator.new(options).dormant_tables
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'attribute-stats'
|
2
|
+
namespace :'attribute-stats' do
|
3
|
+
desc "Generate sample migration to remove unused attributes (and optionally those using default values) [options CONSIDER_DEFAULTS_UNUSED: false, FORMAT: json, tabular, VERBOSE: false]"
|
4
|
+
task :migration, [:consider_defaults_unused,:format,:verbose] => :environment do |task, args|
|
5
|
+
args.with_defaults(consider_defaults_unused: 'false', format: 'tabular', verbose: 'true')
|
6
|
+
options = {
|
7
|
+
consider_defaults_unused: args[:consider_defaults_unused].downcase != 'false',
|
8
|
+
formatter: args[:format],
|
9
|
+
verbose: args[:verbose].downcase != 'false',
|
10
|
+
source: :cli }
|
11
|
+
AttributeStats::StatsGenerator.new(options).migration
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'attribute-stats'
|
2
|
+
namespace :db do
|
3
|
+
namespace :stats do
|
4
|
+
desc "View attributes with no data in the database (and optionally those using default values) [options CONSIDER_DEFAULTS_UNUSED: false, FORMAT: json, tabular, VERBOSE: false]"
|
5
|
+
task :unused_attributes, [:consider_defaults_unused,:format,:verbose] => :environment do |task, args|
|
6
|
+
args.with_defaults(consider_defaults_unused: 'false', format: 'tabular', verbose: 'true')
|
7
|
+
options = {
|
8
|
+
consider_defaults_unused: args[:consider_defaults_unused].downcase != 'false',
|
9
|
+
formatter: args[:format],
|
10
|
+
verbose: args[:verbose].downcase != 'false',
|
11
|
+
source: :cli }
|
12
|
+
AttributeStats::StatsGenerator.new(options).unused_attributes
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module AttributeStats
|
2
|
+
class AttributeInfo
|
3
|
+
attr_reader :count, :usage_percent, :name, :empty
|
4
|
+
|
5
|
+
def initialize(attribute_name)
|
6
|
+
@name = attribute_name
|
7
|
+
end
|
8
|
+
|
9
|
+
def set_usage(record_count, table_total_record_count)
|
10
|
+
@count = record_count
|
11
|
+
@usage_percent = (record_count / table_total_record_count.to_f).round(5)
|
12
|
+
@empty = record_count == 0
|
13
|
+
end
|
14
|
+
|
15
|
+
def set_emptyness(is_empty)
|
16
|
+
@empty = is_empty
|
17
|
+
end
|
18
|
+
|
19
|
+
def empty?
|
20
|
+
@empty
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module AttributeStats
|
2
|
+
class TableData
|
3
|
+
attr_reader :count, :table_name, :attributes, :name, :last_updated, :model
|
4
|
+
def initialize(model)
|
5
|
+
@model = model
|
6
|
+
@name = model.name
|
7
|
+
@table_name = model.table_name
|
8
|
+
@attributes = []
|
9
|
+
@dormant = false
|
10
|
+
@count = 0
|
11
|
+
@column_names = model.columns.map(&:name)
|
12
|
+
end
|
13
|
+
|
14
|
+
def column_names
|
15
|
+
@column_names
|
16
|
+
end
|
17
|
+
|
18
|
+
def make_dormant(last_updated)
|
19
|
+
@dormant = true
|
20
|
+
last_updated = last_updated.to_datetime unless last_updated.nil?
|
21
|
+
@last_updated = last_updated
|
22
|
+
end
|
23
|
+
|
24
|
+
def dormant?
|
25
|
+
@dormant
|
26
|
+
end
|
27
|
+
|
28
|
+
def set_count(total_record_count)
|
29
|
+
@count = total_record_count
|
30
|
+
end
|
31
|
+
|
32
|
+
def attribute_for(attribute_name)
|
33
|
+
unless attribute = attributes.detect{|a| a.name == attribute_name }
|
34
|
+
attribute = AttributeInfo.new(attribute_name)
|
35
|
+
@attributes << attribute
|
36
|
+
end
|
37
|
+
attribute
|
38
|
+
end
|
39
|
+
|
40
|
+
def unused_attributes
|
41
|
+
@unused_attributes ||= begin
|
42
|
+
attrs = []
|
43
|
+
@attributes.each do |attribute|
|
44
|
+
attrs << attribute.name if attribute.empty?
|
45
|
+
end
|
46
|
+
attrs
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module AttributeStats
|
2
|
+
class HashFormatter
|
3
|
+
|
4
|
+
def initialize(options: {}, table_info: {}, migration: [])
|
5
|
+
@options, @table_info, @migration = options, table_info, migration
|
6
|
+
end
|
7
|
+
|
8
|
+
def output_all_attributes
|
9
|
+
output = []
|
10
|
+
@table_info.each do |table_info|
|
11
|
+
table_info.attributes.each do |attribute|
|
12
|
+
output << {
|
13
|
+
model: table_info.name,
|
14
|
+
attribute: attribute.name,
|
15
|
+
count: attribute.count,
|
16
|
+
usage_percent: attribute.usage_percent
|
17
|
+
}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
output
|
21
|
+
end
|
22
|
+
|
23
|
+
def output_dormant_tables
|
24
|
+
output = []
|
25
|
+
@table_info.each do |table_info|
|
26
|
+
output << table_info.table_name if table_info.dormant?
|
27
|
+
end
|
28
|
+
output
|
29
|
+
end
|
30
|
+
|
31
|
+
def output_unused_attributes
|
32
|
+
output = []
|
33
|
+
@table_info.each do |table_info|
|
34
|
+
table_info.attributes.sort_by(&:name).each do |attribute|
|
35
|
+
next unless attribute.empty?
|
36
|
+
output << {
|
37
|
+
model: table_info.name,
|
38
|
+
attribute: attribute.name
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
output
|
43
|
+
end
|
44
|
+
|
45
|
+
def output_migration
|
46
|
+
@migration.data
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module AttributeStats
|
2
|
+
class JSONFormatter < HashFormatter
|
3
|
+
|
4
|
+
def output_all_attributes
|
5
|
+
super.to_json
|
6
|
+
end
|
7
|
+
|
8
|
+
def output_dormant_tables
|
9
|
+
super.to_json
|
10
|
+
end
|
11
|
+
|
12
|
+
def output_unused_attributes
|
13
|
+
super.to_json
|
14
|
+
end
|
15
|
+
|
16
|
+
def output_migration
|
17
|
+
super.to_json
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'hirb'
|
2
|
+
|
3
|
+
module AttributeStats
|
4
|
+
class TabularFormatter
|
5
|
+
|
6
|
+
def initialize(options: {}, table_info: {}, migration: nil)
|
7
|
+
@options, @table_info, @migration = options, table_info, migration
|
8
|
+
@buffer = ''
|
9
|
+
end
|
10
|
+
|
11
|
+
def output_all_attributes
|
12
|
+
attribute_results = []
|
13
|
+
@table_info.each do |table_info|
|
14
|
+
table_info.attributes.sort_by(&:usage_percent).each do |attribute|
|
15
|
+
attribute_results << {
|
16
|
+
model: table_info.name,
|
17
|
+
set_count: attribute.count,
|
18
|
+
set_percent: (attribute.usage_percent * 100).round(1).to_s.rjust(5),
|
19
|
+
attribute_name: attribute.name
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
if attribute_results.empty?
|
24
|
+
puts "No attributes found"
|
25
|
+
else
|
26
|
+
print_table attribute_results, title: 'Attributes Utilization'
|
27
|
+
end
|
28
|
+
@buffer
|
29
|
+
end
|
30
|
+
|
31
|
+
def output_dormant_tables
|
32
|
+
output = []
|
33
|
+
@table_info.each do |table_info|
|
34
|
+
date = table_info.last_updated
|
35
|
+
output << {
|
36
|
+
model: table_info.name,
|
37
|
+
last_updated: date.nil? ? 'Never Updated' : table_info.last_updated.to_date.to_s(:long)
|
38
|
+
} if table_info.dormant?
|
39
|
+
end
|
40
|
+
if output.empty?
|
41
|
+
puts "No dormant tables"
|
42
|
+
else
|
43
|
+
print_table output, title: ['Dormant Tables', "No updated_ats after #{@options[:dormant_table_age].to_date.to_s(:long)}"]
|
44
|
+
end
|
45
|
+
@buffer
|
46
|
+
end
|
47
|
+
|
48
|
+
def output_unused_attributes
|
49
|
+
output = []
|
50
|
+
@table_info.each do |table_info|
|
51
|
+
table_info.attributes.sort_by(&:name).each do |attribute|
|
52
|
+
output << {
|
53
|
+
model: table_info.name,
|
54
|
+
attribute_name: attribute.name
|
55
|
+
} if attribute.empty?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
unused_values = ['Nil', 'Empty']
|
59
|
+
unused_values << 'Default Values' if @options[:consider_defaults_unused]
|
60
|
+
if output.empty?
|
61
|
+
puts "No unused attributes (good for you!)"
|
62
|
+
else
|
63
|
+
print_table output, title: ['Unused Attributes', unused_values.join(', ')]
|
64
|
+
end
|
65
|
+
@buffer
|
66
|
+
end
|
67
|
+
|
68
|
+
def output_migration
|
69
|
+
print_section_header "Migration Syntax to delete unused attributes"
|
70
|
+
@migration.to_migration_file
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def puts(*values)
|
76
|
+
@buffer << values.join("\n")
|
77
|
+
@buffer << "\n"
|
78
|
+
end
|
79
|
+
|
80
|
+
def print_table_header(table_info)
|
81
|
+
line = '-'*(table_info.name.length+4)
|
82
|
+
puts '', line, " #{table_info.name} ", line
|
83
|
+
end
|
84
|
+
|
85
|
+
def print_section_header(text)
|
86
|
+
line = '#'*(text.length+4)
|
87
|
+
puts '', line, " #{text} ", line
|
88
|
+
end
|
89
|
+
|
90
|
+
def print_table(data, title: nil)
|
91
|
+
column_names = data.first.keys
|
92
|
+
output = Hirb::Helpers::AutoTable.render(data,
|
93
|
+
fields: header_order(column_names),
|
94
|
+
headers: formatted_headers(column_names))
|
95
|
+
output_table_title(title, output) unless title.blank?
|
96
|
+
puts output
|
97
|
+
end
|
98
|
+
|
99
|
+
def output_table_title(title_rows, table_output)
|
100
|
+
header_line = table_output.split(/\n/).first
|
101
|
+
puts '', header_line
|
102
|
+
|
103
|
+
Array(title_rows).each do |title|
|
104
|
+
if header_line.length > title.length+4
|
105
|
+
puts "| #{title.center(header_line.length - 4)} |"
|
106
|
+
else
|
107
|
+
puts "| #{title}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def header_order(column_names)
|
113
|
+
[:model, :table_name, :attribute_name,
|
114
|
+
:last_updated, :set_count, :set_percent] & column_names
|
115
|
+
end
|
116
|
+
|
117
|
+
def formatted_headers(column_names)
|
118
|
+
column_names.inject({}) do |hash, name|
|
119
|
+
hash[name] = name.to_s.titleize
|
120
|
+
hash
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
module AttributeStats
|
2
|
+
class GenerateMigration
|
3
|
+
|
4
|
+
def initialize(table_info: [], options: {})
|
5
|
+
@table_info, @options = table_info, options
|
6
|
+
@up_buffer = []
|
7
|
+
@down_buffer = []
|
8
|
+
@table_info.each do |table_info|
|
9
|
+
add_migrations_for_table_to_buffer(table_info)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def data
|
14
|
+
{
|
15
|
+
up_commands: @up_buffer,
|
16
|
+
down_commands: @down_buffer,
|
17
|
+
warning: warning
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_migration_file
|
22
|
+
output = "class RemoveUnusedAttributes < ActiveRecord::Migration"
|
23
|
+
output << "[#{Rails::VERSION::STRING}]" if Rails::VERSION::MAJOR >= 5
|
24
|
+
output << "\n#{warning_to_width}"
|
25
|
+
output << <<-OUTPUT
|
26
|
+
def up
|
27
|
+
# #{@up_buffer.join("\n # ")}
|
28
|
+
end
|
29
|
+
|
30
|
+
def down
|
31
|
+
# #{@down_buffer.join("\n # ")}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
OUTPUT
|
35
|
+
output
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_json
|
39
|
+
{
|
40
|
+
up_commands: @up_buffer,
|
41
|
+
down_commands: @down_buffer,
|
42
|
+
warning: warning
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
# This is a modified version of the ActiveRecord::SchemaDumper#table method.
|
49
|
+
# Unfortunately, Rails generators cannot accept multiple tables in a single RemoveXXXFromXXX
|
50
|
+
# migration (maybe I should submit a PR to Rails?)
|
51
|
+
# ActiveRecord::SchemaDumper cannot be reused for this case, so I had to extract it.
|
52
|
+
def add_migrations_for_table_to_buffer(table_info)
|
53
|
+
@connection ||= ActiveRecord::Base.connection
|
54
|
+
@types ||= @connection.native_database_types
|
55
|
+
|
56
|
+
column_specs = []
|
57
|
+
@connection.columns(table_info.table_name).each do |column|
|
58
|
+
next unless column.name.to_s.in? table_info.unused_attributes
|
59
|
+
column_specs << column_spec_for(column)
|
60
|
+
end
|
61
|
+
|
62
|
+
quoted_table_name = @connection.quote_table_name(table_info.table_name)
|
63
|
+
column_specs.each do |colspec|
|
64
|
+
down_syntax = [quoted_table_name, colspec[:name], ":#{colspec[:type]}", colspec[:options].presence].compact.join(', ')
|
65
|
+
@down_buffer << "add_column #{down_syntax}"
|
66
|
+
@up_buffer << "remove_column #{quoted_table_name}, #{colspec[:name]}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def warning_to_width(target_width=78)
|
71
|
+
line_start = " # "
|
72
|
+
output = ""
|
73
|
+
warning.split("\n").each do |line|
|
74
|
+
line_buffer = "\n#{line_start}"
|
75
|
+
line.split(/ +/).each do |word|
|
76
|
+
if word.length + line_buffer.length > target_width
|
77
|
+
output << line_buffer
|
78
|
+
line_buffer = "\n#{line_start}"
|
79
|
+
end
|
80
|
+
line_buffer << "#{word} "
|
81
|
+
end
|
82
|
+
output << line_buffer+"\n#{line_start}"
|
83
|
+
end
|
84
|
+
output + "\n"
|
85
|
+
end
|
86
|
+
|
87
|
+
def column_spec_for(column)
|
88
|
+
# Rails 5.2
|
89
|
+
# SchemaDumper returns an array like: [:text, {:limit=>"255"}]
|
90
|
+
# Rails 5.1
|
91
|
+
# connection.column_spec returns an array like: [:text, {:limit=>"255"}]
|
92
|
+
# prior to Rails 5.1
|
93
|
+
# connection.column_spec returns a hash like: {:name=>"\"line_2\"", :type=>"string", :limit=>"limit: 255"}
|
94
|
+
if Rails::VERSION::MAJOR >= 5 && Rails::VERSION::MINOR == 2
|
95
|
+
column_spec_for_5_2(column)
|
96
|
+
elsif Rails::VERSION::MAJOR >= 5 && Rails::VERSION::MINOR >= 1
|
97
|
+
column_spec_for_5_1(column)
|
98
|
+
elsif Rails::VERSION::MAJOR >= 5 && Rails::VERSION::MINOR == 0
|
99
|
+
column_spec_for_5_0(column)
|
100
|
+
else
|
101
|
+
column_spec_for_4(column)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def column_spec_for_5_2(column)
|
106
|
+
# Unfortunately Rails has moved this logic from Connection to a private method on SchemaDumper...
|
107
|
+
# Rails really doesn't want people using this stuff!
|
108
|
+
colspec = ActiveRecord::ConnectionAdapters::SchemaDumper
|
109
|
+
.send(:new, @connection, {})
|
110
|
+
.send(:column_spec, column)
|
111
|
+
spec = {
|
112
|
+
name: @connection.quote_column_name(column.name),
|
113
|
+
type: colspec[0]
|
114
|
+
}
|
115
|
+
spec[:options] = colspec[1].map{|k,v| "#{k}: #{v}"}.join(', ') if colspec[1].is_a?(Hash)
|
116
|
+
spec
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
def column_spec_for_5_1(column)
|
121
|
+
colspec = @connection.column_spec(column)
|
122
|
+
spec = {
|
123
|
+
name: @connection.quote_column_name(column.name),
|
124
|
+
type: colspec[0],
|
125
|
+
}
|
126
|
+
spec[:options] = colspec[1].map{|k,v| "#{k}: #{v}"}.join(', ') if colspec[1].is_a?(Hash)
|
127
|
+
spec
|
128
|
+
end
|
129
|
+
|
130
|
+
def column_spec_for_5_0(column)
|
131
|
+
colspec = @connection.column_spec(column)
|
132
|
+
colspec[:options] = colspec.except(:type, :name).values.join(', ')
|
133
|
+
colspec
|
134
|
+
end
|
135
|
+
|
136
|
+
def column_spec_for_4(column)
|
137
|
+
colspec = @connection.column_spec(column, @types)
|
138
|
+
colspec[:options] = colspec.except(:type, :name).values.join(', ')
|
139
|
+
colspec
|
140
|
+
end
|
141
|
+
|
142
|
+
def warning
|
143
|
+
<<-WARNING
|
144
|
+
BUYER BEWARE/CAVEAT EMPTOR. Review this migration carefully!
|
145
|
+
This migration code was autogenerated by analyzing database columns which are empty at the time the script was executed.
|
146
|
+
IT IS YOUR RESPONSIBILITY to verify that these attributes are indeed unused in your application before running this migration. For your protection, the generated commands are COMMENTED OUT.
|
147
|
+
If you were to uncomment the below commands and migrate your database, it is extremely likely that your application will break due to references to the removed attributes in your application code.
|
148
|
+
WARNING
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require_relative 'terminal'
|
2
|
+
module AttributeStats
|
3
|
+
class SetAttributeStats
|
4
|
+
include Terminal
|
5
|
+
|
6
|
+
def initialize(table_info: [], options: {})
|
7
|
+
@table_info, @options = table_info, options
|
8
|
+
end
|
9
|
+
|
10
|
+
def set_counts
|
11
|
+
@query_method = :count
|
12
|
+
set_stats
|
13
|
+
end
|
14
|
+
|
15
|
+
def set_empty
|
16
|
+
@query_method = :exists?
|
17
|
+
set_stats
|
18
|
+
end
|
19
|
+
|
20
|
+
def set_stats
|
21
|
+
@table_info.each_with_index do |t,index|
|
22
|
+
@table_count = index
|
23
|
+
fetch_table_stats(t)
|
24
|
+
end
|
25
|
+
erase_line if @options[:verbose]
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def fetch_table_stats(table)
|
32
|
+
@current_table = table
|
33
|
+
@current_model = table.model
|
34
|
+
set_table_count
|
35
|
+
print("Scanning #{in_color(table.name,@table_count)} ") if @options[:verbose]
|
36
|
+
return if @current_table.count == 0
|
37
|
+
set_attribute_counts
|
38
|
+
erase_line if @options[:verbose]
|
39
|
+
end
|
40
|
+
|
41
|
+
def set_table_count
|
42
|
+
@current_table.set_count(@current_model.all.count)
|
43
|
+
end
|
44
|
+
|
45
|
+
def set_attribute_counts
|
46
|
+
attribs = @current_model.columns.dup
|
47
|
+
attribs.reject!{|c| ["id","created_at","updated_at"].include? c.name } unless @options[:defaults]
|
48
|
+
|
49
|
+
attribs.each do |column_specification|
|
50
|
+
attribute_info = @current_table.attribute_for(column_specification.name)
|
51
|
+
if @query_method == :count
|
52
|
+
next if attribute_info.count
|
53
|
+
record_count = attribute_query(column_specification).count
|
54
|
+
empty = record_count == 0
|
55
|
+
attribute_info.set_usage record_count, @current_table.count
|
56
|
+
else
|
57
|
+
next unless attribute_info.empty.nil?
|
58
|
+
empty = !attribute_query(column_specification).exists?
|
59
|
+
attribute_info.set_emptyness empty
|
60
|
+
end
|
61
|
+
next unless @options[:verbose]
|
62
|
+
print empty ? red('E') : green('.')
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def attribute_query(column_specification)
|
67
|
+
query = @current_model.where.not(column_specification.name => nil)
|
68
|
+
query = safe_add_not_empty_query(query, column_specification)
|
69
|
+
query = add_condition_for_default_value(query, column_specification) if @options[:consider_defaults_unused]
|
70
|
+
query
|
71
|
+
end
|
72
|
+
|
73
|
+
def add_condition_for_default_value(query, attrib)
|
74
|
+
default_value = @current_model.columns_hash[attrib.name].default
|
75
|
+
return query if default_value.blank?
|
76
|
+
query.where.not(attrib.name => default_value)
|
77
|
+
end
|
78
|
+
|
79
|
+
# In addition to normal string attributes, this allows us to query serialized columns.
|
80
|
+
# If you serialize an attribute as an array or hash or whatever, Rails still stores empty
|
81
|
+
# array/hashes as an empty string in the db. Using the more rails-y
|
82
|
+
# where.not(serialized_attribute_name => '')
|
83
|
+
# results in a SerializationTypeMismatch error. So we construct the subquery by hand.
|
84
|
+
def safe_add_not_empty_query(query, column_specification)
|
85
|
+
return query unless ([:text, :string].include?(column_specification.type))
|
86
|
+
query.where.not("#{quoted_column_name(column_specification.name)} = ''")
|
87
|
+
end
|
88
|
+
|
89
|
+
def quoted_column_name(column_name)
|
90
|
+
ActiveRecord::Base.connection.quote_column_name column_name
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative 'terminal'
|
2
|
+
|
3
|
+
module AttributeStats
|
4
|
+
class SetDormantTables
|
5
|
+
include Terminal
|
6
|
+
|
7
|
+
def initialize(table_info: [], options: {})
|
8
|
+
@table_info, @options = table_info, options
|
9
|
+
end
|
10
|
+
|
11
|
+
def call
|
12
|
+
@table_info.each_with_index do |table_data,index|
|
13
|
+
@table_count = index
|
14
|
+
set_dormant_table(table_data)
|
15
|
+
end
|
16
|
+
erase_line if @options[:verbose]
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def set_dormant_table(table_data)
|
23
|
+
query_column = (['updated_at', 'created_at'] & table_data.column_names)[0]
|
24
|
+
return if query_column.nil?
|
25
|
+
print("Scanning #{in_color(table_data.name,@table_count)} ") if @options[:verbose]
|
26
|
+
updated_at = table_data.model.maximum(query_column)
|
27
|
+
table_data.make_dormant(updated_at) if updated_at.nil? || updated_at <= dormant_table_age
|
28
|
+
erase_line if @options[:verbose]
|
29
|
+
end
|
30
|
+
|
31
|
+
def dormant_table_age
|
32
|
+
return @options[:dormant_table_age] if @options[:dormant_table_age].respond_to?(:strftime)
|
33
|
+
# Safely generate Rails duration (instead of risker eval)
|
34
|
+
parts = @options[:dormant_table_age].split('.')
|
35
|
+
duration_expression = parts[0].to_i.send(parts[1]).send(parts[2])
|
36
|
+
@options[:dormant_table_age] = duration_expression
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module AttributeStats
|
2
|
+
class StatsGenerator
|
3
|
+
attr_reader :options, :table_info, :migration
|
4
|
+
|
5
|
+
DEFAULT_OPTIONS = {
|
6
|
+
consider_defaults_unused: true,
|
7
|
+
defaults: false,
|
8
|
+
formatter: :hash,
|
9
|
+
source: :code,
|
10
|
+
dormant_table_age: '3.months.ago',
|
11
|
+
verbose: false }
|
12
|
+
|
13
|
+
def initialize(opts={})
|
14
|
+
@options = DEFAULT_OPTIONS.merge(opts)
|
15
|
+
end
|
16
|
+
|
17
|
+
def attribute_usage
|
18
|
+
fetch_attribute_usage
|
19
|
+
output formatter.output_all_attributes
|
20
|
+
end
|
21
|
+
|
22
|
+
def dormant_tables
|
23
|
+
fetch_dormant_tables
|
24
|
+
output formatter.output_dormant_tables
|
25
|
+
end
|
26
|
+
|
27
|
+
def unused_attributes
|
28
|
+
fetch_empty_attributes
|
29
|
+
output formatter.output_unused_attributes
|
30
|
+
end
|
31
|
+
|
32
|
+
def migration
|
33
|
+
generate_migration
|
34
|
+
output formatter.output_migration
|
35
|
+
end
|
36
|
+
|
37
|
+
def set_formatter(formatter_type)
|
38
|
+
formatter_was = options[:formatter]
|
39
|
+
case formatter_type.to_s.downcase
|
40
|
+
when 'json'
|
41
|
+
options[:formatter] = :json
|
42
|
+
when 'tabular'
|
43
|
+
options[:formatter] = :tabular
|
44
|
+
when 'hash'
|
45
|
+
options[:formatter] = :hash
|
46
|
+
end
|
47
|
+
@formatter = nil unless options[:formatter] == formatter_was
|
48
|
+
options[:formatter]
|
49
|
+
end
|
50
|
+
|
51
|
+
def inspect
|
52
|
+
"StatsGenerator(results: #{table_info.to_s[0..200]}, options: #{options})"
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def formatter
|
58
|
+
@formatter ||= begin
|
59
|
+
output = case options[:formatter].to_s.downcase
|
60
|
+
when 'json'
|
61
|
+
JSONFormatter
|
62
|
+
when 'hash'
|
63
|
+
HashFormatter
|
64
|
+
else
|
65
|
+
TabularFormatter
|
66
|
+
end.new(options: options, table_info: table_info, migration: @migration)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def output(value)
|
71
|
+
return if value.nil?
|
72
|
+
if options[:source] == :cli || value.is_a?(String)
|
73
|
+
puts value
|
74
|
+
else
|
75
|
+
value
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def fetch_attribute_usage
|
80
|
+
@fetch_attribute_usage ||= begin
|
81
|
+
initialize_tables
|
82
|
+
attribute_stats_setter.set_counts
|
83
|
+
true
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def fetch_empty_attributes
|
88
|
+
@fetch_empty_attributes ||= begin
|
89
|
+
initialize_tables
|
90
|
+
attribute_stats_setter.set_empty
|
91
|
+
true
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def attribute_stats_setter
|
96
|
+
@attribute_stats_setter ||= SetAttributeStats.new(table_info: table_info, options: options)
|
97
|
+
end
|
98
|
+
|
99
|
+
def fetch_dormant_tables
|
100
|
+
@fetched_dormant_tables ||= begin
|
101
|
+
initialize_tables
|
102
|
+
SetDormantTables.new(table_info: table_info, options: options).call
|
103
|
+
true
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def generate_migration
|
108
|
+
fetch_empty_attributes
|
109
|
+
@migration ||= GenerateMigration.new(table_info: table_info, options: options)
|
110
|
+
end
|
111
|
+
|
112
|
+
def initialize_tables
|
113
|
+
return unless table_info.nil?
|
114
|
+
@table_info = []
|
115
|
+
tables.sort.each{|table| setup_table_and_model(table) }
|
116
|
+
end
|
117
|
+
|
118
|
+
def setup_table_and_model(table)
|
119
|
+
@table_info << TableData.new(table.classify.constantize)
|
120
|
+
rescue NameError => ex
|
121
|
+
end
|
122
|
+
|
123
|
+
def tables
|
124
|
+
ActiveRecord::Base.connection.data_sources
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module AttributeStats
|
2
|
+
module Terminal
|
3
|
+
# https://en.wikipedia.org/wiki/ANSI_escape_code#Escape_sequences
|
4
|
+
START_OF_NEXT_LINE = "\e[1E"
|
5
|
+
LINE_ABOVE = "\e[1A"
|
6
|
+
CLEAR_LINE_TO_RIGHT = "\e[K"
|
7
|
+
RED = "\e[31m"
|
8
|
+
GREEN = "\e[32m"
|
9
|
+
|
10
|
+
def erase_line
|
11
|
+
print START_OF_NEXT_LINE, LINE_ABOVE, CLEAR_LINE_TO_RIGHT
|
12
|
+
end
|
13
|
+
|
14
|
+
RESET_COLOR = "\e[0m"
|
15
|
+
|
16
|
+
def in_color(text, index=0)
|
17
|
+
code = (31..37).to_a[index % 7]
|
18
|
+
"\e[#{code}m#{text}#{RESET_COLOR}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def red(text)
|
22
|
+
print RED, text, RESET_COLOR
|
23
|
+
end
|
24
|
+
|
25
|
+
def green(text)
|
26
|
+
print GREEN, text, RESET_COLOR
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/rakefile.rb
ADDED
metadata
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: attribute-stats
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Steve Hodges
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-09-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.2'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '6'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '4.2'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '6'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: hirb
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0.7'
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0.7'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: appraisal
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.2'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '2.2'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: bundler
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '1.9'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '1.9'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: rake
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '10.0'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '10.0'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: rspec
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '3'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '3'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: sqlite3
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '1.0'
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '1.0'
|
117
|
+
description: |
|
118
|
+
Attribute Stats gives you insight into which attributes are actually used in your Rails models. Whether you're joining an existing project or have been using it for years, get quick info.
|
119
|
+
|
120
|
+
The stats help you find smells in your project:
|
121
|
+
|
122
|
+
Attributes which have never had data set (indicates a potentially forgotten attribute); Tables which haven't been updated for X years (indicates a potentially unused or legacy model); Attributes used by very few objects in your table (is this being used? Should it be an attribute?)
|
123
|
+
|
124
|
+
It also generates sample Rails database migration code to allow you to drop the database columns.
|
125
|
+
email:
|
126
|
+
- sjhodges@gmail.com
|
127
|
+
executables: []
|
128
|
+
extensions: []
|
129
|
+
extra_rdoc_files: []
|
130
|
+
files:
|
131
|
+
- LICENSE
|
132
|
+
- README.md
|
133
|
+
- lib/attribute-stats.rb
|
134
|
+
- lib/attribute-stats/railtie.rb
|
135
|
+
- lib/attribute-stats/tasks/attribute_usage.rake
|
136
|
+
- lib/attribute-stats/tasks/dormant.rake
|
137
|
+
- lib/attribute-stats/tasks/migration.rake
|
138
|
+
- lib/attribute-stats/tasks/unused_attributes.rake
|
139
|
+
- lib/attribute-stats/version.rb
|
140
|
+
- lib/entities/attribute_info.rb
|
141
|
+
- lib/entities/table_data.rb
|
142
|
+
- lib/formatters/hash_formatter.rb
|
143
|
+
- lib/formatters/json_formatter.rb
|
144
|
+
- lib/formatters/tabular_formatter.rb
|
145
|
+
- lib/stats_generation/generate_migration.rb
|
146
|
+
- lib/stats_generation/set_attribute_stats.rb
|
147
|
+
- lib/stats_generation/set_dormant_tables.rb
|
148
|
+
- lib/stats_generation/stats_generator.rb
|
149
|
+
- lib/stats_generation/terminal.rb
|
150
|
+
- rakefile.rb
|
151
|
+
homepage: https://github.com/stevehodges/attribute_stats
|
152
|
+
licenses:
|
153
|
+
- MIT
|
154
|
+
metadata: {}
|
155
|
+
post_install_message:
|
156
|
+
rdoc_options: []
|
157
|
+
require_paths:
|
158
|
+
- lib
|
159
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
160
|
+
requirements:
|
161
|
+
- - ">="
|
162
|
+
- !ruby/object:Gem::Version
|
163
|
+
version: 2.2.0
|
164
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
165
|
+
requirements:
|
166
|
+
- - ">="
|
167
|
+
- !ruby/object:Gem::Version
|
168
|
+
version: '0'
|
169
|
+
requirements: []
|
170
|
+
rubyforge_project:
|
171
|
+
rubygems_version: 2.6.14.1
|
172
|
+
signing_key:
|
173
|
+
specification_version: 4
|
174
|
+
summary: Statistics about attribute usage, unused attributes, and dormant tables in
|
175
|
+
Rails projects
|
176
|
+
test_files: []
|