rails_console_pro 0.1.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.
- checksums.yaml +7 -0
- data/.editorconfig +12 -0
- data/.rspec +4 -0
- data/.rspec_status +240 -0
- data/.rubocop.yml +26 -0
- data/CHANGELOG.md +24 -0
- data/CONTRIBUTING.md +76 -0
- data/LICENSE.txt +22 -0
- data/QUICK_START.md +112 -0
- data/README.md +124 -0
- data/Rakefile +13 -0
- data/config/database.yml +3 -0
- data/docs/ASSOCIATION_NAVIGATION.md +85 -0
- data/docs/EXPORT.md +95 -0
- data/docs/FORMATTING.md +86 -0
- data/docs/MODEL_STATISTICS.md +72 -0
- data/docs/OBJECT_DIFFING.md +87 -0
- data/docs/SCHEMA_INSPECTION.md +60 -0
- data/docs/SQL_EXPLAIN.md +70 -0
- data/lib/generators/rails_console_pro/install_generator.rb +16 -0
- data/lib/generators/rails_console_pro/templates/rails_console_pro.rb +44 -0
- data/lib/rails_console_pro/active_record_extensions.rb +113 -0
- data/lib/rails_console_pro/association_navigator.rb +273 -0
- data/lib/rails_console_pro/base_printer.rb +74 -0
- data/lib/rails_console_pro/color_helper.rb +36 -0
- data/lib/rails_console_pro/commands/base_command.rb +17 -0
- data/lib/rails_console_pro/commands/diff_command.rb +135 -0
- data/lib/rails_console_pro/commands/explain_command.rb +118 -0
- data/lib/rails_console_pro/commands/export_command.rb +16 -0
- data/lib/rails_console_pro/commands/schema_command.rb +20 -0
- data/lib/rails_console_pro/commands/stats_command.rb +93 -0
- data/lib/rails_console_pro/commands.rb +34 -0
- data/lib/rails_console_pro/configuration.rb +219 -0
- data/lib/rails_console_pro/diff_result.rb +56 -0
- data/lib/rails_console_pro/error_handler.rb +60 -0
- data/lib/rails_console_pro/explain_result.rb +47 -0
- data/lib/rails_console_pro/format_exporter.rb +403 -0
- data/lib/rails_console_pro/global_methods.rb +42 -0
- data/lib/rails_console_pro/initializer.rb +176 -0
- data/lib/rails_console_pro/model_validator.rb +219 -0
- data/lib/rails_console_pro/paginator.rb +204 -0
- data/lib/rails_console_pro/printers/active_record_printer.rb +30 -0
- data/lib/rails_console_pro/printers/collection_printer.rb +34 -0
- data/lib/rails_console_pro/printers/diff_printer.rb +97 -0
- data/lib/rails_console_pro/printers/explain_printer.rb +151 -0
- data/lib/rails_console_pro/printers/relation_printer.rb +25 -0
- data/lib/rails_console_pro/printers/schema_printer.rb +188 -0
- data/lib/rails_console_pro/printers/stats_printer.rb +129 -0
- data/lib/rails_console_pro/pry_commands.rb +241 -0
- data/lib/rails_console_pro/pry_integration.rb +9 -0
- data/lib/rails_console_pro/railtie.rb +29 -0
- data/lib/rails_console_pro/schema_inspector_result.rb +43 -0
- data/lib/rails_console_pro/serializers/active_record_serializer.rb +18 -0
- data/lib/rails_console_pro/serializers/array_serializer.rb +31 -0
- data/lib/rails_console_pro/serializers/base_serializer.rb +25 -0
- data/lib/rails_console_pro/serializers/diff_serializer.rb +24 -0
- data/lib/rails_console_pro/serializers/explain_serializer.rb +35 -0
- data/lib/rails_console_pro/serializers/relation_serializer.rb +25 -0
- data/lib/rails_console_pro/serializers/schema_serializer.rb +121 -0
- data/lib/rails_console_pro/serializers/stats_serializer.rb +24 -0
- data/lib/rails_console_pro/services/column_stats_calculator.rb +64 -0
- data/lib/rails_console_pro/services/index_analyzer.rb +110 -0
- data/lib/rails_console_pro/services/stats_calculator.rb +40 -0
- data/lib/rails_console_pro/services/table_size_calculator.rb +43 -0
- data/lib/rails_console_pro/stats_result.rb +66 -0
- data/lib/rails_console_pro/version.rb +6 -0
- data/lib/rails_console_pro.rb +14 -0
- data/lib/tasks/rails_console_pro.rake +10 -0
- data/rails_console_pro.gemspec +60 -0
- metadata +240 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Commands
|
|
5
|
+
class ExplainCommand < BaseCommand
|
|
6
|
+
def execute(relation_or_model, *args)
|
|
7
|
+
relation = build_relation(relation_or_model, *args)
|
|
8
|
+
return nil unless relation
|
|
9
|
+
|
|
10
|
+
execute_explain(relation)
|
|
11
|
+
rescue => e
|
|
12
|
+
RailsConsolePro::ErrorHandler.handle(e, context: :explain)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def build_relation(relation_or_model, *args)
|
|
18
|
+
if ModelValidator.valid_model?(relation_or_model)
|
|
19
|
+
args.empty? ? relation_or_model.all : relation_or_model.where(*args)
|
|
20
|
+
elsif relation_or_model.respond_to?(:to_sql)
|
|
21
|
+
relation_or_model
|
|
22
|
+
else
|
|
23
|
+
puts pastel.red("Error: Cannot explain #{relation_or_model.class}")
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def execute_explain(relation)
|
|
29
|
+
sql = relation.to_sql
|
|
30
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
|
31
|
+
|
|
32
|
+
explain_output = fetch_explain_output(sql)
|
|
33
|
+
relation.load
|
|
34
|
+
execution_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - start_time
|
|
35
|
+
|
|
36
|
+
indexes_used, recommendations = analyze_explain_output(explain_output, sql, execution_time)
|
|
37
|
+
statistics = build_statistics(relation)
|
|
38
|
+
|
|
39
|
+
ExplainResult.new(
|
|
40
|
+
sql: sql,
|
|
41
|
+
explain_output: explain_output,
|
|
42
|
+
execution_time: execution_time,
|
|
43
|
+
indexes_used: indexes_used,
|
|
44
|
+
recommendations: recommendations,
|
|
45
|
+
statistics: statistics
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def fetch_explain_output(sql)
|
|
50
|
+
connection = ActiveRecord::Base.connection
|
|
51
|
+
adapter_name = connection.adapter_name.downcase
|
|
52
|
+
|
|
53
|
+
if adapter_name.include?('postgresql')
|
|
54
|
+
explain_sql = "EXPLAIN (ANALYZE, BUFFERS, VERBOSE) #{sql}"
|
|
55
|
+
raw_explain = connection.execute(explain_sql)
|
|
56
|
+
raw_explain.values.flatten.join("\n")
|
|
57
|
+
elsif adapter_name.include?('mysql')
|
|
58
|
+
explain_sql = "EXPLAIN #{sql}"
|
|
59
|
+
connection.execute(explain_sql).to_a
|
|
60
|
+
else
|
|
61
|
+
connection.exec_query("EXPLAIN #{sql}").to_a
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def analyze_explain_output(explain_output, sql, execution_time)
|
|
66
|
+
indexes_used = []
|
|
67
|
+
recommendations = []
|
|
68
|
+
|
|
69
|
+
case explain_output
|
|
70
|
+
when String
|
|
71
|
+
analyze_postgresql_output(explain_output, recommendations, indexes_used)
|
|
72
|
+
when Array
|
|
73
|
+
analyze_mysql_output(explain_output, recommendations, indexes_used)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
add_performance_recommendations(sql, execution_time, recommendations)
|
|
77
|
+
[indexes_used, recommendations]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def analyze_postgresql_output(explain_output, recommendations, indexes_used)
|
|
81
|
+
recommendations << "Sequential scan detected - consider adding an index" if explain_output.include?("Seq Scan")
|
|
82
|
+
|
|
83
|
+
explain_output.scan(/Index (?:Scan|Only Scan) using (\w+)/) do |match|
|
|
84
|
+
indexes_used << match[0]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def analyze_mysql_output(explain_output, recommendations, indexes_used)
|
|
89
|
+
explain_output.each do |row|
|
|
90
|
+
next unless row.is_a?(Hash)
|
|
91
|
+
|
|
92
|
+
key = row['key'] || row[:key]
|
|
93
|
+
indexes_used << key if key
|
|
94
|
+
|
|
95
|
+
type = (row['type'] || row[:type]).to_s.downcase
|
|
96
|
+
if type == 'all'
|
|
97
|
+
table = row['table'] || row[:table]
|
|
98
|
+
recommendations << "Full table scan on #{table} - consider adding an index"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def add_performance_recommendations(sql, execution_time, recommendations)
|
|
104
|
+
recommendations << "Query took over 100ms - consider optimization" if execution_time > 100
|
|
105
|
+
recommendations << "LIKE query detected - ensure you're not using leading wildcards (%value)" if sql.downcase.include?('like')
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def build_statistics(relation)
|
|
109
|
+
{
|
|
110
|
+
"Total Rows" => relation.count,
|
|
111
|
+
"Tables Involved" => relation.klass.table_name,
|
|
112
|
+
"Database Adapter" => ActiveRecord::Base.connection.adapter_name.capitalize
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Commands
|
|
5
|
+
class ExportCommand < BaseCommand
|
|
6
|
+
def execute(data, file_path, format: nil)
|
|
7
|
+
return nil if data.nil?
|
|
8
|
+
|
|
9
|
+
FormatExporter.export_to_file(data, file_path, format: format)
|
|
10
|
+
rescue => e
|
|
11
|
+
RailsConsolePro::ErrorHandler.handle(e, context: :export)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Commands
|
|
5
|
+
class SchemaCommand < BaseCommand
|
|
6
|
+
def execute(model_class)
|
|
7
|
+
error_message = ModelValidator.validate_for_schema(model_class)
|
|
8
|
+
if error_message
|
|
9
|
+
puts pastel.red("Error: #{error_message}")
|
|
10
|
+
return nil
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
SchemaInspectorResult.new(model_class)
|
|
14
|
+
rescue => e
|
|
15
|
+
RailsConsolePro::ErrorHandler.handle(e, context: :schema)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Commands
|
|
5
|
+
class StatsCommand < BaseCommand
|
|
6
|
+
def execute(model_class)
|
|
7
|
+
return nil if model_class.nil?
|
|
8
|
+
|
|
9
|
+
error_message = ModelValidator.validate_for_stats(model_class)
|
|
10
|
+
if error_message
|
|
11
|
+
puts pastel.red("Error: #{error_message}")
|
|
12
|
+
return nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
execute_stats(model_class)
|
|
16
|
+
rescue => e
|
|
17
|
+
RailsConsolePro::ErrorHandler.handle(e, context: :stats)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def execute_stats(model_class)
|
|
23
|
+
connection = model_class.connection
|
|
24
|
+
# Get table name directly since model is already validated
|
|
25
|
+
table_name = begin
|
|
26
|
+
model_class.table_name
|
|
27
|
+
rescue => e
|
|
28
|
+
# If we can't get table name, we can't calculate all stats
|
|
29
|
+
puts pastel.yellow("Warning: Could not get table name for #{model_class.name}: #{e.message}")
|
|
30
|
+
return nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Double-check table exists (defensive check)
|
|
34
|
+
unless ModelValidator.has_table?(model_class)
|
|
35
|
+
puts pastel.yellow("Warning: Table does not exist for #{model_class.name}")
|
|
36
|
+
return nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Record count (safe with error handling)
|
|
40
|
+
record_count = safe_count(model_class)
|
|
41
|
+
safe_count_proc = -> { safe_count(model_class) }
|
|
42
|
+
|
|
43
|
+
# Growth rate (only if created_at exists)
|
|
44
|
+
growth_rate = if ModelValidator.has_timestamp_column?(model_class)
|
|
45
|
+
Services::StatsCalculator.calculate_growth_rate(model_class, safe_count_proc)
|
|
46
|
+
else
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Table size (database-specific)
|
|
51
|
+
table_size = Services::TableSizeCalculator.calculate(connection, table_name)
|
|
52
|
+
|
|
53
|
+
# Index usage (safe with error handling)
|
|
54
|
+
index_usage = Services::IndexAnalyzer.analyze(connection, table_name)
|
|
55
|
+
|
|
56
|
+
# Column statistics (only for smaller tables)
|
|
57
|
+
column_stats = if ModelValidator.large_table?(model_class)
|
|
58
|
+
{} # Skip for large tables to avoid performance issues
|
|
59
|
+
else
|
|
60
|
+
Services::ColumnStatsCalculator.calculate(
|
|
61
|
+
model_class,
|
|
62
|
+
connection,
|
|
63
|
+
table_name,
|
|
64
|
+
safe_count_proc,
|
|
65
|
+
config
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
StatsResult.new(
|
|
70
|
+
model: model_class,
|
|
71
|
+
record_count: record_count,
|
|
72
|
+
growth_rate: growth_rate,
|
|
73
|
+
table_size: table_size,
|
|
74
|
+
index_usage: index_usage,
|
|
75
|
+
column_stats: column_stats
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def safe_count(model_class)
|
|
80
|
+
model_class.count
|
|
81
|
+
rescue ActiveRecord::StatementInvalid => e
|
|
82
|
+
raise e # Re-raise StatementInvalid so it can be caught by stats method
|
|
83
|
+
rescue => e
|
|
84
|
+
0 # Return 0 for other errors
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def config
|
|
88
|
+
RailsConsolePro.config
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
# Command methods for schema inspection and SQL explain
|
|
5
|
+
# Thin facade that delegates to command classes
|
|
6
|
+
module Commands
|
|
7
|
+
extend self
|
|
8
|
+
|
|
9
|
+
# Schema inspection command
|
|
10
|
+
def schema(model_class)
|
|
11
|
+
SchemaCommand.new.execute(model_class)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# SQL explain command
|
|
15
|
+
def explain(relation_or_model, *args)
|
|
16
|
+
ExplainCommand.new.execute(relation_or_model, *args)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Export data to file (works with any exportable object)
|
|
20
|
+
def export(data, file_path, format: nil)
|
|
21
|
+
ExportCommand.new.execute(data, file_path, format: format)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Model statistics command
|
|
25
|
+
def stats(model_class)
|
|
26
|
+
StatsCommand.new.execute(model_class)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Object comparison command
|
|
30
|
+
def diff(object1, object2)
|
|
31
|
+
DiffCommand.new.execute(object1, object2)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
# Configuration class for Enhanced Console Printer
|
|
5
|
+
class Configuration
|
|
6
|
+
# Color scheme presets
|
|
7
|
+
COLOR_SCHEMES = {
|
|
8
|
+
dark: {
|
|
9
|
+
header: :bright_blue,
|
|
10
|
+
footer: :bright_blue,
|
|
11
|
+
border: :dim,
|
|
12
|
+
attribute_key: :blue,
|
|
13
|
+
attribute_value_nil: :white,
|
|
14
|
+
attribute_value_numeric: :bright_blue,
|
|
15
|
+
attribute_value_boolean: :green,
|
|
16
|
+
attribute_value_time: :blue,
|
|
17
|
+
attribute_value_string: :white,
|
|
18
|
+
error: :red,
|
|
19
|
+
success: :green,
|
|
20
|
+
warning: :yellow,
|
|
21
|
+
info: :cyan
|
|
22
|
+
},
|
|
23
|
+
light: {
|
|
24
|
+
header: :bright_cyan,
|
|
25
|
+
footer: :bright_cyan,
|
|
26
|
+
border: :dim,
|
|
27
|
+
attribute_key: :bright_blue,
|
|
28
|
+
attribute_value_nil: :black,
|
|
29
|
+
attribute_value_numeric: :blue,
|
|
30
|
+
attribute_value_boolean: :green,
|
|
31
|
+
attribute_value_time: :cyan,
|
|
32
|
+
attribute_value_string: :black,
|
|
33
|
+
error: :red,
|
|
34
|
+
success: :green,
|
|
35
|
+
warning: :yellow,
|
|
36
|
+
info: :cyan
|
|
37
|
+
},
|
|
38
|
+
custom: {} # Will be populated by user
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
# Feature toggles
|
|
42
|
+
attr_accessor :enabled
|
|
43
|
+
attr_accessor :schema_command_enabled
|
|
44
|
+
attr_accessor :explain_command_enabled
|
|
45
|
+
attr_accessor :navigate_command_enabled
|
|
46
|
+
attr_accessor :stats_command_enabled
|
|
47
|
+
attr_accessor :diff_command_enabled
|
|
48
|
+
attr_accessor :active_record_printer_enabled
|
|
49
|
+
attr_accessor :relation_printer_enabled
|
|
50
|
+
attr_accessor :collection_printer_enabled
|
|
51
|
+
attr_accessor :export_enabled
|
|
52
|
+
|
|
53
|
+
# Color customization
|
|
54
|
+
attr_accessor :color_scheme
|
|
55
|
+
attr_accessor :colors
|
|
56
|
+
|
|
57
|
+
# Style customization
|
|
58
|
+
attr_accessor :max_depth
|
|
59
|
+
attr_accessor :show_sql_by_default
|
|
60
|
+
attr_accessor :show_welcome_message
|
|
61
|
+
attr_accessor :border_char
|
|
62
|
+
attr_accessor :header_width
|
|
63
|
+
|
|
64
|
+
# Type-specific colors (for schema printer)
|
|
65
|
+
attr_accessor :type_colors
|
|
66
|
+
|
|
67
|
+
# Validator colors (for schema printer)
|
|
68
|
+
attr_accessor :validator_colors
|
|
69
|
+
|
|
70
|
+
# Pagination settings
|
|
71
|
+
attr_accessor :pagination_enabled
|
|
72
|
+
attr_accessor :pagination_threshold
|
|
73
|
+
attr_accessor :pagination_page_size
|
|
74
|
+
|
|
75
|
+
# Stats calculation settings
|
|
76
|
+
attr_accessor :stats_large_table_threshold
|
|
77
|
+
attr_accessor :stats_skip_distinct_threshold
|
|
78
|
+
|
|
79
|
+
def initialize
|
|
80
|
+
# Default feature toggles - all enabled
|
|
81
|
+
@enabled = true
|
|
82
|
+
@schema_command_enabled = true
|
|
83
|
+
@explain_command_enabled = true
|
|
84
|
+
@navigate_command_enabled = true
|
|
85
|
+
@stats_command_enabled = true
|
|
86
|
+
@diff_command_enabled = true
|
|
87
|
+
@active_record_printer_enabled = true
|
|
88
|
+
@relation_printer_enabled = true
|
|
89
|
+
@collection_printer_enabled = true
|
|
90
|
+
@export_enabled = true
|
|
91
|
+
|
|
92
|
+
# Default color scheme
|
|
93
|
+
@color_scheme = :dark
|
|
94
|
+
@colors = COLOR_SCHEMES[:dark].dup
|
|
95
|
+
|
|
96
|
+
# Default style options
|
|
97
|
+
@max_depth = 10
|
|
98
|
+
@show_sql_by_default = false
|
|
99
|
+
@show_welcome_message = true
|
|
100
|
+
@border_char = "─"
|
|
101
|
+
@header_width = 60
|
|
102
|
+
|
|
103
|
+
# Default type colors
|
|
104
|
+
@type_colors = {
|
|
105
|
+
integer: :bright_blue,
|
|
106
|
+
bigint: :bright_blue,
|
|
107
|
+
decimal: :bright_blue,
|
|
108
|
+
float: :bright_blue,
|
|
109
|
+
string: :green,
|
|
110
|
+
text: :green,
|
|
111
|
+
datetime: :cyan,
|
|
112
|
+
timestamp: :cyan,
|
|
113
|
+
date: :cyan,
|
|
114
|
+
time: :cyan,
|
|
115
|
+
boolean: :magenta,
|
|
116
|
+
json: :yellow,
|
|
117
|
+
jsonb: :yellow
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# Default validator colors
|
|
121
|
+
@validator_colors = {
|
|
122
|
+
'PresenceValidator' => :red,
|
|
123
|
+
'UniquenessValidator' => :magenta,
|
|
124
|
+
'LengthValidator' => :cyan,
|
|
125
|
+
'FormatValidator' => :yellow,
|
|
126
|
+
'NumericalityValidator' => :blue,
|
|
127
|
+
'InclusionValidator' => :green,
|
|
128
|
+
'ExclusionValidator' => :red,
|
|
129
|
+
'ConfirmationValidator' => :cyan,
|
|
130
|
+
'AcceptanceValidator' => :green
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# Default pagination settings
|
|
134
|
+
@pagination_enabled = true
|
|
135
|
+
@pagination_threshold = 10 # Automatically paginate collections with 10+ items
|
|
136
|
+
@pagination_page_size = 5 # Show 5 records per page
|
|
137
|
+
|
|
138
|
+
# Default stats calculation settings
|
|
139
|
+
@stats_large_table_threshold = 10_000 # Consider table large if it has 10k+ records
|
|
140
|
+
@stats_skip_distinct_threshold = 10_000 # Skip distinct count for tables with 10k+ records
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Set color scheme (dark, light, or custom)
|
|
144
|
+
def color_scheme=(scheme)
|
|
145
|
+
@color_scheme = scheme.to_sym
|
|
146
|
+
if COLOR_SCHEMES.key?(@color_scheme) && @color_scheme != :custom
|
|
147
|
+
@colors = COLOR_SCHEMES[@color_scheme].dup
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Set a custom color
|
|
152
|
+
def set_color(key, value)
|
|
153
|
+
@colors = @colors.dup unless @colors.frozen?
|
|
154
|
+
@colors[key.to_sym] = value.to_sym
|
|
155
|
+
@color_scheme = :custom
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Get a color value
|
|
159
|
+
def get_color(key)
|
|
160
|
+
@colors[key.to_sym] || :white
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Set type color
|
|
164
|
+
def set_type_color(type, color)
|
|
165
|
+
@type_colors = @type_colors.dup if @type_colors.frozen?
|
|
166
|
+
@type_colors[type.to_sym] = color.to_sym
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Get type color
|
|
170
|
+
def get_type_color(type)
|
|
171
|
+
@type_colors[type.to_sym] || :white
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Set validator color
|
|
175
|
+
def set_validator_color(validator_type, color)
|
|
176
|
+
@validator_colors = @validator_colors.dup if @validator_colors.frozen?
|
|
177
|
+
@validator_colors[validator_type.to_s] = color.to_sym
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Get validator color
|
|
181
|
+
def get_validator_color(validator_type)
|
|
182
|
+
@validator_colors[validator_type.to_s] || :white
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Disable all features
|
|
186
|
+
def disable_all
|
|
187
|
+
@enabled = false
|
|
188
|
+
@schema_command_enabled = false
|
|
189
|
+
@explain_command_enabled = false
|
|
190
|
+
@navigate_command_enabled = false
|
|
191
|
+
@stats_command_enabled = false
|
|
192
|
+
@diff_command_enabled = false
|
|
193
|
+
@active_record_printer_enabled = false
|
|
194
|
+
@relation_printer_enabled = false
|
|
195
|
+
@collection_printer_enabled = false
|
|
196
|
+
@export_enabled = false
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Enable all features
|
|
200
|
+
def enable_all
|
|
201
|
+
@enabled = true
|
|
202
|
+
@schema_command_enabled = true
|
|
203
|
+
@explain_command_enabled = true
|
|
204
|
+
@navigate_command_enabled = true
|
|
205
|
+
@stats_command_enabled = true
|
|
206
|
+
@diff_command_enabled = true
|
|
207
|
+
@active_record_printer_enabled = true
|
|
208
|
+
@relation_printer_enabled = true
|
|
209
|
+
@collection_printer_enabled = true
|
|
210
|
+
@export_enabled = true
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Reset to defaults
|
|
214
|
+
def reset
|
|
215
|
+
initialize
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
# Value object for object comparison results
|
|
5
|
+
class DiffResult
|
|
6
|
+
attr_reader :object1, :object2, :differences, :identical, :object1_type,
|
|
7
|
+
:object2_type, :timestamp
|
|
8
|
+
|
|
9
|
+
def initialize(object1:, object2:, differences: {}, identical: false,
|
|
10
|
+
object1_type: nil, object2_type: nil, timestamp: Time.current)
|
|
11
|
+
@object1 = object1
|
|
12
|
+
@object2 = object2
|
|
13
|
+
@differences = differences
|
|
14
|
+
@identical = identical
|
|
15
|
+
@object1_type = object1_type || object1.class.name
|
|
16
|
+
@object2_type = object2_type || object2.class.name
|
|
17
|
+
@timestamp = timestamp
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def ==(other)
|
|
21
|
+
other.is_a?(self.class) && other.object1 == object1 && other.object2 == object2
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def has_differences?
|
|
25
|
+
!identical && differences.any?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def different_types?
|
|
29
|
+
object1_type != object2_type
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def diff_count
|
|
33
|
+
differences.size
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Export to JSON
|
|
37
|
+
def to_json(pretty: true)
|
|
38
|
+
FormatExporter.to_json(self, pretty: pretty)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Export to YAML
|
|
42
|
+
def to_yaml
|
|
43
|
+
FormatExporter.to_yaml(self)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Export to HTML
|
|
47
|
+
def to_html(style: :default)
|
|
48
|
+
FormatExporter.to_html(self, title: "Diff Comparison", style: style)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Export to file
|
|
52
|
+
def export_to_file(file_path, format: nil)
|
|
53
|
+
FormatExporter.export_to_file(self, file_path, format: format)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
# Unified error handler for consistent error handling across the gem
|
|
5
|
+
module ErrorHandler
|
|
6
|
+
extend self
|
|
7
|
+
include ColorHelper
|
|
8
|
+
|
|
9
|
+
def handle(error, context: nil)
|
|
10
|
+
case error
|
|
11
|
+
when ActiveRecord::ConfigurationError
|
|
12
|
+
handle_configuration_error(error, context)
|
|
13
|
+
when ActiveRecord::StatementInvalid
|
|
14
|
+
handle_sql_error(error, context)
|
|
15
|
+
when ArgumentError
|
|
16
|
+
handle_argument_error(error, context)
|
|
17
|
+
when NameError
|
|
18
|
+
handle_name_error(error, context)
|
|
19
|
+
else
|
|
20
|
+
handle_generic_error(error, context)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def handle_configuration_error(error, context)
|
|
27
|
+
puts pastel.red.bold("❌ Configuration Error: #{error.message}")
|
|
28
|
+
puts pastel.yellow("💡 Tip: Check that all associations exist in your models")
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def handle_sql_error(error, context)
|
|
33
|
+
puts pastel.red.bold("❌ SQL Error: #{error.message}")
|
|
34
|
+
puts pastel.yellow("💡 Tip: Check your query syntax and table/column names")
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def handle_argument_error(error, context)
|
|
39
|
+
puts pastel.red.bold("❌ Error: #{error.message}")
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def handle_name_error(error, context)
|
|
44
|
+
message = error.message.include?('uninitialized constant') ?
|
|
45
|
+
"Could not find model or class" : error.message
|
|
46
|
+
puts pastel.red.bold("❌ Error: #{message}")
|
|
47
|
+
puts pastel.yellow("💡 Tip: Make sure the model name is correct and loaded")
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def handle_generic_error(error, context)
|
|
52
|
+
puts pastel.red.bold("❌ Error: #{error.message}")
|
|
53
|
+
if Rails.env.development? || ENV['RAILS_CONSOLE_PRO_DEBUG']
|
|
54
|
+
puts pastel.dim(error.backtrace.first(3).join("\n"))
|
|
55
|
+
end
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
# Value object for SQL explain results
|
|
5
|
+
class ExplainResult
|
|
6
|
+
attr_reader :sql, :explain_output, :execution_time, :indexes_used,
|
|
7
|
+
:recommendations, :statistics
|
|
8
|
+
|
|
9
|
+
def initialize(sql:, explain_output:, execution_time: nil,
|
|
10
|
+
indexes_used: [], recommendations: [], statistics: nil)
|
|
11
|
+
@sql = sql
|
|
12
|
+
@explain_output = explain_output
|
|
13
|
+
@execution_time = execution_time
|
|
14
|
+
@indexes_used = Array(indexes_used)
|
|
15
|
+
@recommendations = Array(recommendations)
|
|
16
|
+
@statistics = statistics || {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def slow_query?
|
|
20
|
+
execution_time && execution_time > 100
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def has_indexes?
|
|
24
|
+
indexes_used.any?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Export to JSON
|
|
28
|
+
def to_json(pretty: true)
|
|
29
|
+
FormatExporter.to_json(self, pretty: pretty)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Export to YAML
|
|
33
|
+
def to_yaml
|
|
34
|
+
FormatExporter.to_yaml(self)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Export to HTML
|
|
38
|
+
def to_html(style: :default)
|
|
39
|
+
FormatExporter.to_html(self, title: "SQL Explain Analysis", style: style)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Export to file
|
|
43
|
+
def export_to_file(file_path, format: nil)
|
|
44
|
+
FormatExporter.export_to_file(self, file_path, format: format)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|